β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0638

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_ppp forwards every inbound PROT_COMPD (0x00fd) frame directly to the deflate node's decomp hook (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 via m_devget+NG_FWD_NEW_DATA (:618,:622,:366), leaking heap-derived data.
  • No default-config privilege beyond sending PPP frames is required.

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

Timeline

  • 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
  • 2026-07-02 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0638 Β· 12 files
FileTypeDescriptionSize
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
README.md readme build/run/expected
↓ download 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.

VERDICT.md verdict full analysis
↓ download raw

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) and build.sh/run.sh β€” none existed. The base ngctl is 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

fixed

VALIDATED: baseline Z_DATA_ERROR OOB; patched EPIPE guard no OOB.

BEFORE: Z_DATA_ERROR -3. AFTER: EPIPE 32.
↓ fix.diff6.5-DEVELOPMENT #0 module swap

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.