All vulnerabilities
GHSA-x862-c2wj-4mwr

gix-pack: uncapped Tree::with_capacity from pack-header object count causes OOM/process abort on clone/fetch

gix-pack
CVSS
6.5Medium
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed inNone listedNo source names a fixed version yet
Published25 Sep 2026Updated 25 Sep 2026GitHub

What fixes it

No source in this snapshot names a fixed version.

Description

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

  1. Entry: attacker-served (or MITM-redirected) upload-pack → victim gix clone/fetch. Guard: none (serving a pack needs no auth). Bypass proof: N/A.
  2. 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.
  3. Processing: size_hint() → objects_left as usize → LookupRefDeltaObjectsIter::size_hint doubles it → worst_case = 2 * num_objects (index/write/mod.rs:122).
  4. 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 ace687ce). 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.
  5. 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 ace687ce 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 e3a6fa15). 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

Text from GitHub Advisory Database

Affected versions 1

  • gix-pack, other<= 0.74.2GitHub