Summary
During any clone/fetch, gix-pack sizes its delta-resolution Tree directly from the attacker-controlled 32-bit object count in the 12-byte pack header, before any object body is read. The backing allocation uses Vec::reserve_exact (not try_reserve), so a header claiming a huge object count forces a multi-hundred-gigabyte reservation that aborts the process via handle_alloc_error. This is a pre-authentication, ~12-byte remote denial of service against any application built on gix/gix-pack that clones or fetches from an untrusted remote.
Root Cause
write_data_iter_to_stream (gix-pack/src/index/write/mod.rs:123) does:
let (anticipated_num_objects, upper_bound) = entries.size_hint();
let worst_case_num_objects_after_thin_pack_resolution = upper_bound.unwrap_or(anticipated_num_objects);
let mut tree = Tree::with_capacity(worst_case_num_objects_after_thin_pack_resolution)?;
Tree::with_capacity (gix-pack/src/cache/delta/tree.rs:51) calls exact_vec(num_objects/2) twice, and exact_vec (gix-pack/src/lib.rs:100-104) is:
fn exact_vec<T>(capacity: usize) -> Vec<T> {
let mut v = Vec::new();
v.reserve_exact(capacity); // aborts on failure; does NOT return Err
v
}
reserve_exact aborts via handle_alloc_error on failure — it does not return Err — so the Result return type of with_capacity does NOT guard the allocation. The upper_bound derives from size_hint(): BytesToEntriesIter::size_hint returns Some(objects_left) where objects_left = the raw num_objects: u32 read from the pack header (gix-pack/src/data/header.rs:18), and LookupRefDeltaObjectsIter::size_hint doubles it. So worst_case = 2 * num_objects.
Impact
num_objects = 0xFFFFFFFF yields two reserve_exact(~4.29e9) allocations of Vec<Item<TreeEntry>> (~76 bytes/element ≈ 326 GB each), attempted before any entry body is decompressed. On default-overcommit Linux (vm.overcommit_memory=0, heuristic mode rejects the oversized single request) and on Windows (commit charge), the allocation fails and the process aborts (SIGABRT). No num_objects sanity check exists before the allocation.
Proof of Concept
Serve an upload-pack whose pack stream begins with a valid header 50 41 43 4B 00 00 00 02 FF FF FF FF ("PACK", version 2, num_objects=0xFFFFFFFF) to a victim running gix clone/fetch. The Tree::with_capacity call attempts ~326 GB reservations and aborts the process before reading any object body.
Attack Chain
- Entry: attacker-served (or MITM-redirected) upload-pack → victim
gix clone/fetch. Guard: none (serving a pack needs no auth). Bypass proof: N/A.
- Check:
data::header::decode (header.rs:12-21) accepts any u32 object count. Guard: header validation. Bypass proof: only "PACK" magic + version (2 or 3) are checked; num_objects has no upper bound.
- Processing:
size_hint() → objects_left as usize → LookupRefDeltaObjectsIter::size_hint doubles it → worst_case = 2 * num_objects (index/write/mod.rs:122).
- Sink:
Tree::with_capacity(2*num_objects) → exact_vec(n/2) → Vec::reserve_exact (lib.rs:102). Guard: alloc_limit_bytes (added by the GHSA-x494-mj8g-cj27 follow-up fix ace687c). Bypass proof: git show ace687ce -- gix-pack/src/index/write/mod.rs shows the param was added to the function signature and forwarded ONLY to resolve() (line 207); the Tree::with_capacity call at line 123 takes no limit and runs earlier.
- Impact:
reserve_exact of hundreds of GB fails and calls handle_alloc_error → process abort. Pre-auth, ~12 attacker bytes.
Bypass Evidence
git show gix-pack-v0.74.2:gix-pack/src/lib.rs confirms exact_vec uses v.reserve_exact(capacity) with no try_reserve and no cap.
git show gix-pack-v0.74.2:gix-pack/src/index/write/mod.rs confirms line 123 Tree::with_capacity(worst_case...)? is unguarded by alloc_limit_bytes.
- The published advisory GHSA-x494-mj8g-cj27 covers the DIFFERENT allocation sink
bytes_to_entries.rs:109 (Vec::with_capacity(decompressed_size)); this Tree::with_capacity sink is untouched by its fix (commit ace687c added alloc_limit_bytes only to resolve()).
Affected Versions
gix-pack <= 0.74.2 (latest release tag gix-pack-v0.74.2; code present on HEAD e3a6fa1). No post-tag fix.
Suggested Fix
Cap worst_case_num_objects_after_thin_pack_resolution against a configurable maximum (like git's transfer.maxPackSize), and change exact_vec to use try_reserve_exact and propagate the error through Tree::with_capacity's existing Result.
Caveat
On kernels with vm.overcommit_memory=1 (always-overcommit) the virtual reservation succeeds and the cheap crash degrades (the attacker must then actually stream billions of entries). Hence severity MEDIUM rather than HIGH. On CI/CD auto-clone (UI:N) severity rises to ~7.5.
Reported by zx (Jace) — GitHub: @manus-use
Summary
During any clone/fetch, gix-pack sizes its delta-resolution
Treedirectly from the attacker-controlled 32-bit object count in the 12-byte pack header, before any object body is read. The backing allocation usesVec::reserve_exact(nottry_reserve), so a header claiming a huge object count forces a multi-hundred-gigabyte reservation that aborts the process viahandle_alloc_error. This is a pre-authentication, ~12-byte remote denial of service against any application built ongix/gix-packthat clones or fetches from an untrusted remote.Root Cause
write_data_iter_to_stream(gix-pack/src/index/write/mod.rs:123) does:Tree::with_capacity(gix-pack/src/cache/delta/tree.rs:51) callsexact_vec(num_objects/2)twice, andexact_vec(gix-pack/src/lib.rs:100-104) is:reserve_exactaborts viahandle_alloc_erroron failure — it does not returnErr— so theResultreturn type ofwith_capacitydoes NOT guard the allocation. Theupper_boundderives fromsize_hint():BytesToEntriesIter::size_hintreturnsSome(objects_left)whereobjects_left= the rawnum_objects: u32read from the pack header (gix-pack/src/data/header.rs:18), andLookupRefDeltaObjectsIter::size_hintdoubles it. Soworst_case = 2 * num_objects.Impact
num_objects = 0xFFFFFFFFyields tworeserve_exact(~4.29e9)allocations ofVec<Item<TreeEntry>>(~76 bytes/element ≈ 326 GB each), attempted before any entry body is decompressed. On default-overcommit Linux (vm.overcommit_memory=0, heuristic mode rejects the oversized single request) and on Windows (commit charge), the allocation fails and the process aborts (SIGABRT). Nonum_objectssanity check exists before the allocation.Proof of Concept
Serve an upload-pack whose pack stream begins with a valid header
50 41 43 4B 00 00 00 02 FF FF FF FF("PACK", version 2, num_objects=0xFFFFFFFF) to a victim runninggix clone/fetch. TheTree::with_capacitycall attempts ~326 GB reservations and aborts the process before reading any object body.Attack Chain
gix clone/fetch. Guard: none (serving a pack needs no auth). Bypass proof: N/A.data::header::decode(header.rs:12-21) accepts any u32 object count. Guard: header validation. Bypass proof: only "PACK" magic + version (2 or 3) are checked;num_objectshas no upper bound.size_hint()→objects_left as usize→LookupRefDeltaObjectsIter::size_hintdoubles it →worst_case = 2 * num_objects(index/write/mod.rs:122).Tree::with_capacity(2*num_objects)→exact_vec(n/2)→Vec::reserve_exact(lib.rs:102). Guard:alloc_limit_bytes(added by the GHSA-x494-mj8g-cj27 follow-up fix ace687c). Bypass proof:git show ace687ce -- gix-pack/src/index/write/mod.rsshows the param was added to the function signature and forwarded ONLY toresolve()(line 207); theTree::with_capacitycall at line 123 takes no limit and runs earlier.reserve_exactof hundreds of GB fails and callshandle_alloc_error→ process abort. Pre-auth, ~12 attacker bytes.Bypass Evidence
git show gix-pack-v0.74.2:gix-pack/src/lib.rsconfirmsexact_vecusesv.reserve_exact(capacity)with notry_reserveand no cap.git show gix-pack-v0.74.2:gix-pack/src/index/write/mod.rsconfirms line 123Tree::with_capacity(worst_case...)?is unguarded byalloc_limit_bytes.bytes_to_entries.rs:109(Vec::with_capacity(decompressed_size)); thisTree::with_capacitysink is untouched by its fix (commit ace687c addedalloc_limit_bytesonly toresolve()).Affected Versions
gix-pack <= 0.74.2(latest release tag gix-pack-v0.74.2; code present on HEAD e3a6fa1). No post-tag fix.Suggested Fix
Cap
worst_case_num_objects_after_thin_pack_resolutionagainst a configurable maximum (like git'stransfer.maxPackSize), and changeexact_vecto usetry_reserve_exactand propagate the error throughTree::with_capacity's existingResult.Caveat
On kernels with
vm.overcommit_memory=1(always-overcommit) the virtual reservation succeeds and the cheap crash degrades (the attacker must then actually stream billions of entries). Hence severity MEDIUM rather than HIGH. On CI/CD auto-clone (UI:N) severity rises to ~7.5.Reported by zx (Jace) — GitHub: @manus-use