Skip to content

Commit 3a3436a

Browse files
committed
more safety around recursion and invariants when resolving ref-deltas (#301)
1 parent da87c1b commit 3a3436a

File tree

4 files changed

+82
-5
lines changed

4 files changed

+82
-5
lines changed

git-odb/src/store_impls/dynamic/find.rs

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ mod error {
2323
LoadIndex(#[from] crate::store::load_index::Error),
2424
#[error(transparent)]
2525
LoadPack(#[from] std::io::Error),
26+
#[error("Object {} referred to its base object {} by its id but it's not within a multi-index", .id, .base_id)]
27+
ThinPackAtRest {
28+
/// the id of the base object which lived outside of the multi-index
29+
base_id: git_hash::ObjectId,
30+
/// The original object to lookup
31+
id: git_hash::ObjectId,
32+
},
33+
#[error("Reached recursion limit of {} while resolving ref delta bases for {}", .max_depth, .id)]
34+
DeltaBaseRecursionLimit {
35+
/// the maximum recursion depth we encountered.
36+
max_depth: usize,
37+
/// The original object to lookup
38+
id: git_hash::ObjectId,
39+
},
2640
#[error("The base object {} could not be found but is required to decode {}", .base_id, .id)]
2741
DeltaBaseMissing {
2842
/// the id of the base object which failed to lookup
@@ -41,6 +55,25 @@ mod error {
4155
},
4256
}
4357

58+
#[derive(Copy, Clone)]
59+
pub(crate) struct DeltaBaseRecursion<'a> {
60+
pub depth: usize,
61+
pub original_id: &'a git_hash::oid,
62+
}
63+
64+
impl<'a> DeltaBaseRecursion<'a> {
65+
pub fn new(id: &'a git_hash::oid) -> Self {
66+
Self {
67+
original_id: id,
68+
depth: 0,
69+
}
70+
}
71+
pub fn inc_depth(mut self) -> Self {
72+
self.depth += 1;
73+
self
74+
}
75+
}
76+
4477
#[cfg(test)]
4578
mod tests {
4679
use super::*;
@@ -135,11 +168,20 @@ where
135168

136169
fn try_find_cached_inner<'a>(
137170
&self,
138-
id: &oid,
171+
id: &git_hash::oid,
139172
buffer: &'a mut Vec<u8>,
140173
pack_cache: &mut impl DecodeEntry,
141174
snapshot: &mut load_index::Snapshot,
175+
recursion: Option<error::DeltaBaseRecursion<'_>>,
142176
) -> Result<Option<(Data<'a>, Option<Location>)>, Error> {
177+
if let Some(r) = recursion {
178+
if r.depth >= self.max_recursion_depth {
179+
return Err(Error::DeltaBaseRecursionLimit {
180+
max_depth: self.max_recursion_depth,
181+
id: r.original_id.to_owned(),
182+
});
183+
}
184+
}
143185
'outer: loop {
144186
{
145187
let marker = snapshot.marker;
@@ -197,15 +239,33 @@ where
197239
entry_size: r.compressed_size + header_size,
198240
}),
199241
)),
200-
Err(git_pack::data::decode_entry::Error::DeltaBaseUnresolved(base_id)) => {
242+
Err(git_pack::data::decode_entry::Error::DeltaBaseUnresolved(base_id))
243+
if index.is_multi_pack() =>
244+
{
245+
// Only with multi-pack indices it's allowed to jump to refer to other packs within this
246+
// multi-pack. Otherwise this would consistute a thin pack which is only allowed in transit.
247+
let _base_must_exist_in_multi_index =
248+
index.lookup(&base_id).ok_or_else(|| Error::ThinPackAtRest {
249+
base_id,
250+
id: id.to_owned(),
251+
})?;
252+
201253
// special case, and we just allocate here to make it work. It's an actual delta-ref object
202254
// which is sent by some servers that points to an object outside of the pack we are looking
203255
// at right now. With the complexities of loading packs, we go into recursion here. Git itself
204256
// doesn't do a cycle check, and we won't either but limit the recursive depth.
205257
// The whole ordeal isn't efficient due to memory allocation and later mem-copying when trying again.
206258
let mut buf = Vec::new();
207259
let obj_kind = self
208-
.try_find_cached_inner(&base_id, &mut buf, pack_cache, snapshot)
260+
.try_find_cached_inner(
261+
&base_id,
262+
&mut buf,
263+
pack_cache,
264+
snapshot,
265+
recursion
266+
.map(|r| r.inc_depth())
267+
.or_else(|| error::DeltaBaseRecursion::new(id).into()),
268+
)
209269
.map_err(|err| Error::DeltaBaseLookup {
210270
err: Box::new(err),
211271
base_id,
@@ -343,7 +403,7 @@ where
343403
) -> Result<Option<(Data<'a>, Option<Location>)>, Self::Error> {
344404
let id = id.as_ref();
345405
let mut snapshot = self.snapshot.borrow_mut();
346-
self.try_find_cached_inner(id, buffer, pack_cache, &mut snapshot)
406+
self.try_find_cached_inner(id, buffer, pack_cache, &mut snapshot, None)
347407
}
348408

349409
fn location_by_oid(&self, id: impl AsRef<oid>, buf: &mut Vec<u8>) -> Option<Location> {

git-odb/src/store_impls/dynamic/handle.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ pub(crate) mod index_lookup {
114114
})
115115
}
116116

117+
/// Returns true if this is a multi-pack index
118+
pub(crate) fn is_multi_pack(&self) -> bool {
119+
matches!(&self.file, handle::SingleOrMultiIndex::Multi { .. })
120+
}
121+
117122
/// Return true if the given object id exists in this index
118123
pub(crate) fn contains(&self, object_id: &oid) -> bool {
119124
match &self.file {
@@ -221,6 +226,9 @@ impl super::Store {
221226

222227
/// Handle creation
223228
impl super::Store {
229+
/// The amount of times a ref-delta base can be followed when multi-indices are involved.
230+
pub const INITIAL_MAX_RECURSION_DEPTH: usize = 32;
231+
224232
/// Create a new cache filled with a handle to this store, if this store is supporting shared ownership.
225233
///
226234
/// Note that the actual type of `OwnShared` depends on the `parallel` feature toggle of the `git-features` crate.
@@ -243,6 +251,7 @@ impl super::Store {
243251
refresh: RefreshMode::default(),
244252
token: Some(token),
245253
snapshot: RefCell::new(self.collect_snapshot()),
254+
max_recursion_depth: Self::INITIAL_MAX_RECURSION_DEPTH,
246255
}
247256
}
248257

@@ -256,6 +265,7 @@ impl super::Store {
256265
refresh: Default::default(),
257266
token: Some(token),
258267
snapshot: RefCell::new(self.collect_snapshot()),
268+
max_recursion_depth: Self::INITIAL_MAX_RECURSION_DEPTH,
259269
}
260270
}
261271

@@ -336,6 +346,7 @@ where
336346
.into()
337347
},
338348
snapshot: RefCell::new(self.store.collect_snapshot()),
349+
max_recursion_depth: self.max_recursion_depth,
339350
}
340351
}
341352
}

git-odb/src/store_impls/dynamic/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ where
1212
pub(crate) store: S,
1313
/// Defines what happens when there is no more indices to load.
1414
pub refresh: RefreshMode,
15+
/// The maximum recursion depth for resolving ref-delta base objects, that is objects referring to other objects within
16+
/// a pack.
17+
/// Recursive loops are possible only in purposefully crafted packs.
18+
/// This value doesn't have to be huge as in typical scenarios, these kind of objects are rare and chains supposedly are
19+
/// even more rare.
20+
pub max_recursion_depth: usize,
1521

1622
pub(crate) token: Option<handle::Mode>,
1723
snapshot: RefCell<load_index::Snapshot>,

gitoxide-core/src/index/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ pub fn checkout_exclusive(
211211

212212
progress.done(format!(
213213
"Created {} {} files",
214-
entries_for_checkout,
214+
entries_for_checkout.saturating_sub(errors.len() + collisions.len()),
215215
repo.is_none().then(|| "empty").unwrap_or_default()
216216
));
217217

0 commit comments

Comments
 (0)