Skip to content

gix-pack: reachable assertion panic on valid pack version 3 header aborts clone/fetch

Moderate
Byron published GHSA-633h-mqjc-8rwg Sep 25, 2026

Software

gix-pack

Affected versions

<= 0.74.2

Patched versions

<= 0.75.0

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

  1. Entry: attacker serves a pack beginning 50 41 43 4B 00 00 00 03 .... Guard: header::decode version check. Bypass proof: header.rs:14 returns Ok(Version::V3, ...) for byte value 3; only versions ≠ 2,3 error.
  2. Sink: new_from_header runs assert_eq!(version, V2) (bytes_to_entries.rs:58). Guard: should be a graceful UnsupportedVersion error. Bypass proof: it is assert_eq! (not debug_assert_eq!) → panics on V3 in release.
  3. 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.rs confirms version 3 → Ok(Version::V3, ...) on the latest tag.
  • git show gix-pack-v0.74.2:gix-pack/src/data/input/bytes_to_entries.rs confirms the assert_eq!(version, Version::V2, ...) at line 58 (and debug_assert_eq! at line 133, proving the distinction is deliberate).
  • The GHSA-x494-mj8g-cj27 panic-fix commits b69f0a6/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

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H

CVE ID

No known CVE

Weaknesses

Reachable Assertion

The product contains an assert() or similar statement that can be triggered by an attacker, which leads to an application exit or other behavior that is more severe than necessary. Learn more on MITRE.

Credits