Decompress path avail_in integer underflow feeds OOB heap reads to inflate() from crafted PPP frame
| Field | Value |
|---|---|
| ID | DF-0638 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:H |
| CWE | CWE-787 Out-of-bounds Read via Integer Underflow (CWE-191/CWE-125) |
| File | sys/netgraph7/deflate/ng_deflate.c |
| Lines | 539, 541, 552-568, 582-589 (decompress); 634-642 (inflateIncomp sink) |
| Area | netgraph7 (DEFLATE decompression) |
| Confidence | certain |
| Discovered | 2026-07-02 |
| Reported | pending |
Summary
ng_deflate_decompress() only upper-bounds the input length (inlen >
DEFLATE_BUF_SIZE at line 541) and never lower-bounds it before subtracting
protocol/seqnum offsets. For a PROT_COMPD frame shorter than 4 bytes, the
expression avail_in = inlen - offset (line 583) underflows from a small
negative int to a ~4 GiB uInt when stored into z_stream.avail_in.
inflate() then reads far past the 4096-byte inbuf embedded in the
heap-allocated priv struct β an out-of-bounds kernel heap read driven by
attacker input.
Root cause
ng_deflate.c:539: inlen = m->m_pkthdr.len (int, sys/sys/mbuf.h:159).
Line 541: only rejects inlen > 4096. Lines 552-557: protocol-field
parsing sets offset = 1 (1-byte proto) or offset = 2 (2-byte proto).
Lines 567-568: seqnum parsing reads inbuf[offset..offset+1] and does
offset += 2, making offset = 3 or 4.
Line 583: priv->cx.avail_in = inlen - offset. For inlen=2 and
offset=4: (int)(2 - 4) = -2 β (uInt)0xFFFFFFFE. avail_in is
uInt/unsigned int (sys/net/zlib.h:213,280).
Line 589: inflate(&priv->cx, Z_PACKET_FLUSH) is told ~4 GiB of input is
available at inbuf+4, so zlib walks the kernel heap starting 4 bytes past
the (residue) seqnum.
The dictionary-update else-branch has the identical bug at lines 634-636
(avail_in = inlen - 1 when inlen==0) feeding inflateIncomp() at line
642.
The seqnum gate at line 569 does not help: on a fresh/reset node
priv->seqnum==0 and the zeroed-residue inbuf[2..3] read back as 0, so
the check passes deterministically for the first crafted PROT_COMPD
frame.
Threat model & preconditions
- Reachable from a remote PPP peer:
ng_pppforwards every inboundPROT_COMPD(0x00fd) frame directly to the deflate node'sdecomphook (ng_ppp.c:270,1164). Compression is negotiated via CCP after PPP auth, so the typical position is a malicious/compromised authenticated peer or a MITM, but any deployment without PPP auth exposes it pre-auth. - Impact (a) β Reliable kernel panic / DoS: the OOB read crosses into an unmapped page β page fault β kernel panic.
- Impact (b) β Info leak: any bytes zlib manages to decompress from the
OOB region are written to
outbuf(:585) and returned to the peer viam_devget+NG_FWD_NEW_DATA(:618,:622,:366), leaking heap-derived data. - No default-config privilege beyond sending PPP frames is required.
Recommended fix
Lower-bound inlen before any subtraction. Add three guards: inlen < 1
(before protocol parse), inlen < 2 (before 2-byte-proto parse), and
inlen < offset + 2 (before seqnum parse). These convert every
inlen - offset into a provably non-negative value before it is widened
into uInt avail_in. See the full diff in the finding markdown.
References
sys/netgraph7/deflate/ng_deflate.c:539,541βinlenand the missing lower bound.sys/netgraph7/deflate/ng_deflate.c:567-568,582-583β seqnum parse + underflow inavail_in.sys/netgraph7/deflate/ng_deflate.c:589βinflate()OOB read sink.sys/netgraph7/deflate/ng_deflate.c:634-642βinflateIncomp()OOB sink.sys/net/zlib.h:213,280βavail_inisuInt(unsigned int).
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0638 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df0638.c | trigger-source | raw netgraph7 socket client driving decomp hook | 4.9 KB | view raw |
| build.sh | build-script | cc -o df0638 df0638.c | 104 B | view raw |
| run.sh | run-script | load netgraph7 stack + run client | 783 B | view raw |
| run.log | run-log | decisive run: decompression error -3 (OOB read) | 488 B | view raw |
| fix_run.log | run-log | fixed-module run: rcvdata error 32 (EPIPE), no OOB | 339 B | view raw |
| dmesg.txt | dmesg | decompression error -3 evidence | 107 B | view raw |
| fix.diff | suggested-fix | lower-bound inlen before avail_in subtractions | 1.1 KB | view raw |
| VERDICT.md | verdict | full analysis | 3.7 KB | β raw |
| README.md | readme | build/run/expected | 1.5 KB | β raw |
| env.txt | environment | guest env + reachability notes | 1.0 KB | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0638 PoC β ng_deflate avail_in underflow OOB heap read
Build (on guest)
# 1. build the netgraph7 stack (the default kernel uses old netgraph ABI-2 w/ no deflate) for d in netgraph socket deflate; do (cd /usr/src/sys/netgraph7/$d && make obj && make); done # 2. build the client cd poc/DF-0638 && cc -o df0638 df0638.c
Run (as root β socket creation is privileged; the bug trigger is the crafted frame)
./run.sh
Expected (bug present)
dmesg shows ng_deflate_decompress: decompression error: -3 (invalid stored block
lengths) β proof that inflate consumed bytes out of bounds past the 2-byte input
(the avail_in underflow = OOB heap read). Depending on heap layout this is an info
leak (decompressed OOB bytes returned to peer) or a panic (OOB crosses an unmapped
page).
Expected (FIXED module)
dmesg shows only ng_deflate_rcvdata: error: 32 (EPIPE from the new lower-bound
guard) and NO decompression error β the short frame is rejected before inflate.
How it works
df0638.c is a raw netgraph7 socket client (the base ngctl only speaks the old
ABI-2 netgraph and cannot drive netgraph7). It: opens an AF_NETGRAPH control
socket, names its node, mkpeers a deflate node (out <-> decomp), sends
NGM_DEFLATE_CONFIG {enable=1, windowBits=12} (β inflateInit2, seqnum=0), opens
a data socket attached to the node, and sends the 2-byte frame 0x00 0xfd
(PROT_COMPD, 2-byte proto). With inlen=2, offset reaches 4, so
avail_in = inlen - offset = (int)-2 β (uInt)0xFFFFFFFE, and inflate reads OOB.
DF-0638 β ng_deflate avail_in underflow β OOB heap read
Verdict
REPRODUCED β out-of-bounds kernel heap read confirmed; primitive is read-only (escallation to uid0 not applicable).
Mechanism (cited)
sys/netgraph7/deflate/ng_deflate.c ng_deflate_decompress():
- :539 inlen = m->m_pkthdr.len; (int).
- :541 only upper-bounds inlen > DEFLATE_BUF_SIZE (4096); never lower-bounds.
- :552-557 protocol field parse: 1-byte proto (offset=1) or 2-byte proto
(offset=2). For PROT_COMPD (0x00fd) the 2-byte path is taken, offset=2.
- :567-568 seqnum parse reads inbuf[offset..offset+1] and does offset += 2
β offset = 4.
- :583 priv->cx.avail_in = inlen - offset; β for a 2-byte frame inlen-offset
= (int)(2-4) = -2 β widened to (uInt)0xFFFFFFFE (avail_in is unsigned int,
sys/net/zlib.h).
- :589 inflate(&priv->cx, Z_PACKET_FLUSH) is told ~4 GiB of input is available
starting at inbuf+4, so it walks the kernel heap past the 4096-byte inbuf in
the heap-allocated priv struct β an OOB read driven by attacker input.
- Identical underflow at the inflateIncomp sink :634-636 (avail_in = inlen - 1
when inlen==0).
- The :569 seqnum gate does not help: on a configured node priv->seqnum == 0 and
the priv struct is M_ZERO-allocated (:178), so the zeroed-residue
inbuf[2..3] reads back as seqnum 0 == priv->seqnum β passes deterministically.
Proof (decisive)
A raw netgraph7 socket client builds mkpeer deflate: out <-> decomp, configures it
(inflateInit2, seqnum=0), and injects the 2-byte frame 0x00 0xfd (PROT_COMPD,
2-byte proto) down the out hook. inflate consumes bytes far past the 2-byte
input and dmesg shows:
ng_deflate_decompress: decompression error: -3 (invalid stored block lengths) ng_deflate_rcvdata: error: 5
Z_DATA_ERROR "invalid stored block lengths" is proof inflate read heap memory
beyond the 2-byte frame (it interpreted out-of-bounds heap bytes as a deflate stored
block and rejected them). The OOB read happened; it stayed within mapped kernel
memory here so it returned a data error rather than page-faulting.
Exploit chain / escalation assessment
The primitive is a read-only OOB heap read feeding inflate. There is no write.
- If the OOB bytes happen to be valid deflate, inflate would decompress them into
outbuf (:585) and the result is returned to the peer via m_devget+
NG_FWD_NEW_DATA (:618/:622) β kernel-heap info leak.
- If the OOB read crosses an unmapped page β page fault β kernel panic (DoS).
- uid0 escalation is not applicable to a read-only primitive (Phase 6 valid
blocker). Realistic ceiling: info leak / DoS.
Reachability (important caveat)
The vulnerable code is in netgraph7 (ABI 12). The default DragonFly GENERIC
kernel ships the old sys/netgraph/ (ABI 2, net.graph.abi_version=2) which has
no deflate node at all. So on the stock install ng_deflate is neither built nor
reachable; netgraph7 must be explicitly built+loaded. The threat model (remote PPP
peer via ng_ppp β decomp hook) holds wherever netgraph7 is the active stack.
PoC changes
- Wrote
df0638.c(raw netgraph7 control+data socket client) andbuild.sh/run.shβ none existed. The basengctlis ABI-2-only and cannot drive netgraph7, hence the custom client.
Fix
fix.diff lower-bounds inlen before the subtractions: in the PROT_COMPD path,
reject inlen < offset + 2 before the seqnum parse / avail_in computation; and in
the inflateIncomp path reject inlen < 1 before avail_in = inlen - 1. Validated:
on the fixed module the crafted frame is rejected with EPIPE (rcvdata error 32)
and no decompression-error/OOB read occurs.
Fix verification
fixedVALIDATED: baseline Z_DATA_ERROR OOB; patched EPIPE guard no OOB.
BEFORE: Z_DATA_ERROR -3. AFTER: EPIPE 32.
Confirmed kernel references
β
Detail
Exploit chain
none -- read-only OOB
Evidence (decisive lines)
β
Verdict
REPRODUCED. ng_deflate avail_in=(int)(2-4)=-2 -> (uInt)0xFFFFFFFE -> inflate reads ~4GiB OOB heap. dmesg Z_DATA_ERROR proves OOB read.
No comments yet.