Summary
When resolving a thin pack during fetch, gix-pack computes an OFS_DELTA base offset with pack_offset.checked_sub(base_distance).expect("distance to be in range of pack"). base_distance is an unvalidated leb64 varint read directly from the attacker-controlled entry header. A crafted base_distance larger than pack_offset makes checked_sub return None, and the .expect() panics — a remote denial of service on any gix/gix-pack fetch into a non-empty repository.
Root Cause
LookupRefDeltaObjectsIter::next (gix-pack/src/data/input/lookup_ref_delta_objects.rs:129-136):
if self.inserted_entries_length_in_bytes != 0 {
if let Header::OfsDelta { base_distance } = entry.header {
let base_pack_offset = entry
.pack_offset
.checked_sub(base_distance)
.expect("distance to be in range of pack");
base_distance comes from leb64_from_read in the entry-header decoder (gix-pack/src/data/entry/decode.rs) with no validation against pack_offset. Every sibling conversion guards this — e.g. index/write/mod.rs:176 uses verified_base_pack_offset(..).ok_or(Error::…)?. Only this iterator uses .expect().
Impact
A malicious remote serving a thin pack triggers checked_sub → None → .expect() panic. The crate is built with panic = "unwind" and there is no catch_unwind in the gix-pack/gix-protocol/gix runtime read path, so the panic unwinds to the fetch caller, aborting the fetch/process = DoS.
Proof of Concept
Against a victim repo with at least one referenceable object, serve a thin pack containing (a) a REF_DELTA whose base is an object the victim already holds (so inserted_entries_length_in_bytes != 0), followed by (b) an OFS_DELTA entry whose leb64 base_distance exceeds its pack_offset. The .expect() panics.
Attack Chain
- Entry: attacker-served upload-pack → victim
fetch into an existing repo → thin pack. Guard: none.
- Gate: a REF_DELTA whose
base_id is an object the victim already has → self.lookup.try_find succeeds → track_change → inserted_entries_length_in_bytes != 0. Guard: if inserted_entries_length_in_bytes != 0 (line 129). Bypass proof: satisfied by normal thin-pack behaviour; the base exists because the victim has prior history.
- Sink: a subsequent OFS_DELTA with leb64
base_distance > pack_offset → checked_sub returns None → .expect(...) panics (lines 133-136). Guard: should be .ok_or(Error::…)? like sibling sites. Bypass proof: no comparison of base_distance to pack_offset before the checked_sub; the varint is read unvalidated.
- Impact: panic unwinds on the fetch thread → aborts the fetch/process = remote DoS.
Bypass Evidence
git show gix-pack-v0.74.2:gix-pack/src/data/input/lookup_ref_delta_objects.rs confirms the .expect() at lines 133-136 on the latest tag.
git log -- gix-pack/src/data/input/lookup_ref_delta_objects.rs shows no prior DoS-advisory commit touched this file (the GHSA-x494-mj8g-cj27 panic fixes b69f0a6/5850141e edited only data/entry/decode.rs).
- Sibling guarded site confirmed at
index/write/mod.rs:176 (verified_base_pack_offset(..).ok_or(..)?).
- Reachability:
receive_pack.rs:184-189 passes thin_pack_base_object_lookup = Some(...) → bundle/write/mod.rs wraps the stream in LookupRefDeltaObjectsIter.
Affected Versions
gix-pack <= 0.74.2 (latest release tag gix-pack-v0.74.2; code present on HEAD). No post-tag fix.
Suggested Fix
Replace .expect("distance to be in range of pack") with .ok_or(Error::...)?, matching the sibling verified_base_pack_offset guard used everywhere else.
Precondition Note
Does NOT fire on a fresh clone (victim has no objects → REF_DELTA lookup returns None → gate stays false). Fires on a fetch into a repo with ≥1 referenceable object — the normal thin-pack case. Hence severity MEDIUM.
Reported by zx (Jace) — GitHub: @manus-use
Summary
When resolving a thin pack during
fetch, gix-pack computes an OFS_DELTA base offset withpack_offset.checked_sub(base_distance).expect("distance to be in range of pack").base_distanceis an unvalidated leb64 varint read directly from the attacker-controlled entry header. A craftedbase_distancelarger thanpack_offsetmakeschecked_subreturnNone, and the.expect()panics — a remote denial of service on anygix/gix-packfetch into a non-empty repository.Root Cause
LookupRefDeltaObjectsIter::next(gix-pack/src/data/input/lookup_ref_delta_objects.rs:129-136):base_distancecomes fromleb64_from_readin the entry-header decoder (gix-pack/src/data/entry/decode.rs) with no validation againstpack_offset. Every sibling conversion guards this — e.g.index/write/mod.rs:176usesverified_base_pack_offset(..).ok_or(Error::…)?. Only this iterator uses.expect().Impact
A malicious remote serving a thin pack triggers
checked_sub→None→.expect()panic. The crate is built withpanic = "unwind"and there is nocatch_unwindin the gix-pack/gix-protocol/gix runtime read path, so the panic unwinds to the fetch caller, aborting the fetch/process = DoS.Proof of Concept
Against a victim repo with at least one referenceable object, serve a thin pack containing (a) a REF_DELTA whose base is an object the victim already holds (so
inserted_entries_length_in_bytes != 0), followed by (b) an OFS_DELTA entry whose leb64base_distanceexceeds itspack_offset. The.expect()panics.Attack Chain
fetchinto an existing repo → thin pack. Guard: none.base_idis an object the victim already has →self.lookup.try_findsucceeds →track_change→inserted_entries_length_in_bytes != 0. Guard:if inserted_entries_length_in_bytes != 0(line 129). Bypass proof: satisfied by normal thin-pack behaviour; the base exists because the victim has prior history.base_distance > pack_offset→checked_subreturnsNone→.expect(...)panics (lines 133-136). Guard: should be.ok_or(Error::…)?like sibling sites. Bypass proof: no comparison ofbase_distancetopack_offsetbefore thechecked_sub; the varint is read unvalidated.Bypass Evidence
git show gix-pack-v0.74.2:gix-pack/src/data/input/lookup_ref_delta_objects.rsconfirms the.expect()at lines 133-136 on the latest tag.git log -- gix-pack/src/data/input/lookup_ref_delta_objects.rsshows no prior DoS-advisory commit touched this file (the GHSA-x494-mj8g-cj27 panic fixes b69f0a6/5850141e edited onlydata/entry/decode.rs).index/write/mod.rs:176(verified_base_pack_offset(..).ok_or(..)?).receive_pack.rs:184-189passesthin_pack_base_object_lookup = Some(...)→bundle/write/mod.rswraps the stream inLookupRefDeltaObjectsIter.Affected Versions
gix-pack <= 0.74.2(latest release tag gix-pack-v0.74.2; code present on HEAD). No post-tag fix.Suggested Fix
Replace
.expect("distance to be in range of pack")with.ok_or(Error::...)?, matching the siblingverified_base_pack_offsetguard used everywhere else.Precondition Note
Does NOT fire on a fresh clone (victim has no objects → REF_DELTA lookup returns None → gate stays false). Fires on a fetch into a repo with ≥1 referenceable object — the normal thin-pack case. Hence severity MEDIUM.
Reported by zx (Jace) — GitHub: @manus-use