gix-pack: reachable assertion panic on valid pack version 3 header aborts clone/fetch
What fixes it
No source in this snapshot names a fixed version.
Description
Summary
gix-pack's pack-header decoder explicitly accepts pack version 3 (returning Ok((Version::V3, num_objects))), but the very next step asserts the version equals 2 with assert_eq! — which is compiled into release builds. A malicious remote that sends a 12-byte pack header declaring version 3 triggers a panic that aborts the clone/fetch. This is a trivial, deterministic, pre-authentication remote denial of service that fires on every clone/fetch.
Root Cause
data::header::decode (gix-pack/src/data/header.rs:12-21):
let kind = match crate::read_u32(&data[ofs..ofs + N32_SIZE]) {
2 => data::Version::V2,
3 => data::Version::V3, // version 3 is ACCEPTED, not an error
v => return Err(decode::Error::UnsupportedVersion(v)),
};Then BytesToEntriesIter::new_from_header (gix-pack/src/data/input/bytes_to_entries.rs:58):
let (version, num_objects) = crate::data::header::decode(&header_data)?;
assert_eq!(
version,
crate::data::Version::V2,
"let's stop here if we see undocumented pack formats"
);assert_eq! (unlike debug_assert_eq!) is NOT compiled out in release. Note the same file uses debug_assert_eq! at line 133, confirming the line-58 hard assert is a distinct, deliberate release-time panic. The author's own message ("let's stop here") shows an intent to stop gracefully, but a panic was chosen instead of an error return.
Impact
The crate is built with panic = "unwind" and there is no catch_unwind in the runtime read path, so the panic unwinds to the fetch/clone caller → operation/process abort = DoS. It runs first-thing on every clone/fetch (both thin and non-thin branches), before any negotiation-specific state.
Proof of Concept
Serve a pack stream beginning 50 41 43 4B 00 00 00 03 <num_objects> ("PACK", version 3) to a victim gix clone/fetch. header::decode returns Ok((V3, ...)), then the assert_eq!(version, V2) panics.
Attack Chain
- Entry: attacker serves a pack beginning
50 41 43 4B 00 00 00 03 .... Guard:header::decodeversion check. Bypass proof: header.rs:14 returnsOk(Version::V3, ...)for byte value 3; only versions ≠ 2,3 error. - Sink:
new_from_headerrunsassert_eq!(version, V2)(bytes_to_entries.rs:58). Guard: should be a gracefulUnsupportedVersionerror. Bypass proof: it isassert_eq!(notdebug_assert_eq!) → panics on V3 in release. - Impact: panic on the fetch/clone thread → process/operation abort = remote DoS. 12 attacker bytes, no negotiation state, no thin-pack precondition.
Bypass Evidence
git show gix-pack-v0.74.2:gix-pack/src/data/header.rsconfirms version 3 →Ok(Version::V3, ...)on the latest tag.git show gix-pack-v0.74.2:gix-pack/src/data/input/bytes_to_entries.rsconfirms theassert_eq!(version, Version::V2, ...)at line 58 (anddebug_assert_eq!at line 133, proving the distinction is deliberate).- The GHSA-x494-mj8g-cj27 panic-fix commits b69f0a6a/5850141e edited only
data/entry/decode.rs; this pack-header assert is untouched.
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
Return a graceful decode::Error::UnsupportedVersion/dedicated error for any non-V2 version in new_from_header instead of assert_eq!.
--- Reported by zx (Jace) — GitHub: @manus-use
Text from GitHub Advisory Database
Affected versions 1
- gix-pack, other<= 0.74.2GitHub