β¬’ DragonFlyBSD Kernel Audit
← dashboard
DF-0265

Missing distance bounds validation in inflate: heap OOB read when windowBits<15

Field Value
ID DF-0265
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:A/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:C
CWE CWE-125 Out-of-bounds Read
File sys/net/zlib.c
Lines 4824-5085
Area net
Confidence certain
Discovered 2026-06-30
Reported pending

Summary

The in-kernel zlib is version 1.0.4 from December 1997 (FILEVERSION 971210) β€” 28 years old, predating ALL modern zlib security hardening. The inflate code computes back-reference source pointers from decoded distance codes without checking that the distance does not exceed the allocated window size. DEFLATE allows distances up to 32768 bytes, but inflateInit2_ permits windowBits as low as 8 (256-byte window). A malicious compressed stream using large distance codes against a small window causes a heap buffer over-read of up to ~32KB.

Root cause

Distance code range: cpdist[30] (line 4149) allows distances up to 32768 bytes.

Window size: inflateInit2_ (line 3180) allows windowBits 8..15. Window allocation is 1 << wbits bytes (line 3350).

Missing check: The decoded distance is used directly in pointer arithmetic at line 4825 (inflate_codes COPY case):

s->end - (c->sub.copy.dist - (q - s->window))

and line 5076 (inflate_fast):

r = s->end - e

No check exists that dist <= (1 << s->wbits). Modern zlib validates dist <= state->dmax β€” this check is entirely absent in 1.0.4.

Threat model & preconditions

  • Attacker position: Authenticated PPP peer (via netgraph7_deflate compression). The PPP peer negotiates windowBits < 15 during CCP, then sends a compressed frame containing a large distance code.
  • Impact: Kernel heap info leak (up to ~32KB of adjacent heap contents per malicious packet) and/or kernel panic (OOB read crossing into unmapped memory).
  • Required config: PPP with deflate compression enabled. netgraph7_deflate uses raw DEFLATE mode (-windowBits, nowrap=1), bypassing zlib header validation entirely.

Short-term: Add distance validation before the COPY case:

if (c->sub.copy.dist > (1 << s->wbits)) {
    z->msg = "invalid distance too far back";
    c->mode = BADCODE;
    r = Z_DATA_ERROR;
    LEAVE;
}

Long-term: Replace this 28-year-old zlib with the modern sys/contrib/zlib/ already present in the tree, which has 28 years of security fixes including this one.

References

  • CVE-2018-25032 (modern zlib heap buffer overflow)
  • This zlib version: 1.0.4, FILEVERSION 971210
  • Modern zlib fix: if (dist > state->dmax) check in inflate.c

Timeline

  • 2026-06-30 Discovered during automated audit.

PoC verification

Evidence pack

findings/poc/DF-0265 Β· 15 files
FileTypeDescriptionSize
harness.c trigger-source guard-page allocator + inflate driver; links the verbatim audited zlib.c and catches the window underflow as SIGSEGV 11.1 KB view raw
zlib.c audit-source-copy byte-identical copy of sys/net/zlib.c (the vulnerable code under test) 172.9 KB view raw
zlib.h audit-source-copy byte-identical copy of sys/net/zlib.h 42.4 KB view raw
kshim.h build-shim userspace stubs for kernel-only MODULE_VERSION/MAX tokens; -include'd, does NOT touch inflate code 842 B view raw
gen_stream.py stream-generator host-side script that produced the crafted raw-DEFLATE stream embedded in harness.c 1.6 KB view raw
build.sh build-script exact cc command 480 B view raw
run.sh run-script exact run command (arg = windowBits) 362 B view raw
build.log build-log final successful build, full output 13 B view raw
run.log run-log decisive vulnerable run (wbits=-8): SIGSEGV -255 bytes before window 709 B view raw
run.control.log run-log control run (wbits=-15): clean Z_STREAM_END, total_out=916 573 B view raw
run_stress.log run-log 3 repeat runs of the vulnerable config (deterministic) 1.9 KB view raw
env.txt environment uname, cc version, kldstat, module availability (ng_deflate NOT built) 748 B view raw
fix.diff suggested-fix git-apply-able unified diff: dist>window check in inflate_codes AND inflate_fast 1.6 KB view raw
VERDICT.md verdict full narrative analysis: root cause, reproduction, reachability, fix verification 7.7 KB ↓ raw
README.md readme human reproduce instructions 3.0 KB ↓ raw
README.md readme human reproduce instructions
↓ download raw

DF-0265 β€” PoC: heap OOB read in zlib 1.0.4 inflate when windowBits < 15

Verbatim copy of the audited sys/net/zlib.c (and sys/net/zlib.h), plus a userspace harness that links them unmodified and drives inflate with a crafted raw-DEFLATE stream. A guard page placed before every zlib allocation surfaces the sliding-window underflow as a SIGSEGV β€” conclusive proof of the missing distance-bounds check.

Build

./build.sh          # cc -O2 -Wall -include kshim.h -o harness harness.c zlib.c

kshim.h only stubs out the kernel-only MODULE_VERSION / MAX tokens that zlib.c references at file scope; it does not touch the inflate code under test (the file is byte-identical to sys/net/zlib.c).

Run

./run.sh            # vulnerable config: windowBits=-8 (256-byte window)
./run.sh -15        # control:           windowBits=-15 (32KiB window), no OOB

Expected (bug present), windowBits=-8

[harness] *** SIGSEGV/SIGBUS during inflate at 0x800480f01 ***
[harness] nearest alloc #2 [user=0x800481000 size=256 (0x100)]: fault is -255 bytes (BEFORE-start (underflow into guard page))
[harness] RESULT: OOB READ CONFIRMED -- inflate dereferenced memory outside an allocation ...

The fault address is 255 bytes before the 256-byte sliding window β€” i.e. inflate read s->end - (dist - (q - s->window)) with dist far larger than the window and underflowed past s->window into the guard page.

Expected, windowBits=-15 (control)

[harness] inflate returned 1, msg=(null), total_out=916

The 32 KiB window swallows the stream's distance (658) cleanly β€” same code, same stream, no OOB. This isolates the cause to the missing dist <= window check at small windowBits.

With the fix applied (see fix.diff)

windowBits=-8 : inflate returned -3, msg=invalid distance too far back   # rejected, no OOB
windowBits=-15: inflate returned 1,  total_out=916                       # still decodes cleanly

Files

file purpose
harness.c guard-page allocator + inflate driver; identifies the faulting allocation
zlib.c,zlib.h verbatim copies of the audited sys/net/zlib.c / sys/net/zlib.h
kshim.h userspace stubs for the kernel-only MODULE_VERSION / MAX tokens (-include)
build.sh,run.sh exact reproduce commands
gen_stream.py how the crafted raw-DEFLATE stream embedded in harness.c was produced
build.log final successful build output
run.log decisive vulnerable run (wbits=-8), full output
run.control.log control run (wbits=-15), full output
run_stress.log 3 repeat runs (deterministic)
env.txt guest uname / cc / kldstat / module availability
fix.diff git-apply-able unified diff adding the distance-bounds check
VERDICT.md full narrative analysis
manifest.json machine-readable artifact catalog

See VERDICT.md for the full root-cause analysis, reachability on the audited kernel, and the threat model.

VERDICT.md verdict full narrative analysis: root cause, reproduction, reachability, fix verification
↓ download raw

DF-0265 β€” Verdict: REPRODUCED (latent on the audited kernel config)

Verdict: REPRODUCED β€” the missing distance-bounds check in sys/net/zlib.c (zlib 1.0.4, FILEVERSION 971210) is real and exploitable, confirmed deterministically by linking the exact, unmodified audited source into a userspace harness and catching a sliding-window underflow on a guard page. The vulnerable kernel consumer (netgraph7_deflate) is not compiled into the audited X86_64_GENERIC kernel and has no loadable ng_deflate.ko on the guest, so the defect is latent on the shipped master DEV kernel; it becomes live the moment an operator builds/loads NETGRAPH7_DEFLATE.


1. The bug, line by line

sys/net/zlib.c is zlib 1.0.4 (header at line 18: ==FILEVERSION 971210==). It is compiled into the kernel via sys/conf/files:

  • net/zlib.c optional mxge pci β€” used by dev/netif/mxge
  • net/zlib.c optional netgraph7_deflate β€” used by netgraph7/deflate

inflateInit2_ (sys/net/zlib.c:3143) clamps windowBits to 8..15 (line 3175) and allocates a window of 1 << wbits bytes in inflate_blocks_new (sys/net/zlib.c:3714-3726: s->window = ZALLOC(z,1,w), s->end = s->window + w). With windowBits=8 the window is 256 bytes.

DEFLATE distance codes, however, can resolve to 1..32768 bytes (cpdist[30] at sys/net/zlib.c:4149, max entry 24577 + extra bits β†’ 32768). Nothing in the decoder rejects a distance larger than the window. Two code paths consume the decoded distance directly in pointer arithmetic with no bounds check:

  1. inflate_codes COPY case (sys/net/zlib.c:4822-4831): c f = (uInt)(q - s->window) < c->sub.copy.dist ? s->end - (c->sub.copy.dist - (q - s->window)) : /* <-- underflow */ q - c->sub.copy.dist; When dist - (q - s->window) > (s->end - s->window) (i.e. dist exceeds the window size plus the current write offset), the subtraction s->end - (dist - (q - s->window)) underflows past s->window. The subsequent copy loop (OUTBYTE(*f++), lines 4834-4838) then reads memory before the window allocation.

  2. inflate_fast (sys/net/zlib.c:5073-5088): same pattern, e = d - (uInt)(q - s->window); r = s->end - e; at lines 5075-5076, followed by *q++ = *r++ copies that read from the underflowed pointer.

Modern zlib validates this. The newer copy in the same tree (sys/vfs/hammer2/zlib/) has the check in both paths: - hammer2_zlib_inflate.c:932: if (copy > state->whave) { "invalid distance too far back"; BAD; } - hammer2_zlib_inffast.c:177: if (dist > dmax) { "invalid distance too far back"; ... }

The 1.0.4 code in sys/net/zlib.c has neither β€” confirmed by reading the full inflate_codes and inflate_fast functions. The finding is accurate.

2. Reproduction

The harness (harness.c) links the verbatim sys/net/zlib.c in its userspace compile mode (which the file explicitly supports: it #includes "zlib.h" when _KERNEL is undefined and contains the identical distance-handling code that ships in the kernel). It supplies a custom zcalloc/zcfree that mmap's every allocation with a PROT_NONE guard page immediately before the user pointer, then drives inflate with a crafted raw-DEFLATE stream (see gen_stream.py) that contains a distance of ~658 β€” well past a 256-byte window.

Decisive run, windowBits=-8 (256-byte window), run.log:

[harness] 3 allocations made by inflateInit2:
  #2 user=0x800481000 size=256 (0x100)            <-- the sliding window
[harness] calling inflate on 551-byte stream, window=256 bytes...
[harness] *** SIGSEGV/SIGBUS during inflate at 0x800480f01 ***
[harness] nearest alloc #2 [user=0x800481000 size=256 (0x100)]:
         fault is -255 bytes (BEFORE-start (underflow into guard page))
[harness] RESULT: OOB READ CONFIRMED

inflate read 255 bytes before the window start, into the guard page. The same stream with windowBits=-15 (32 KiB window) decompresses cleanly (run.control.log: inflate returned 1, total_out=916) β€” same code, same input, no OOB. This isolates the cause to the missing distance check at small windowBits.

Reproduced 3/3 times (run_stress.log); fully deterministic.

3. Reachability on the audited master DEV kernel

consumer windowBits reachable on X86_64_GENERIC?
dev/netif/mxge/if_mxge.c:675 (firmware inflate) inflateInit β†’ default 15 (32 KiB) safe by construction (distance ≀ 32768 ≀ window); plus the firmware blob is root-loaded from disk, not attacker-controlled.
netgraph7/deflate/ng_deflate.c:270 (PPP deflate) inflateInit2(-windowBits), 8..15 accepted (line 236) NOT compiled in. No ng_deflate.ko on the guest, NETGRAPH7/NETGRAPH7_DEFLATE absent from sys/config/X86_64_GENERIC. kldstat shows only kernel/ehci.ko/xhci.ko.

So on the shipped master DEV kernel the vulnerable path is dead code: sys/net/zlib.c is compiled only for mxge (which uses the safe default window). The defect becomes reachable only if an operator explicitly enables netgraph7_deflate (custom kernel build or a hand-built module, both root-gated).

If netgraph7_deflate is enabled, the threat model in the finding holds: a malicious authenticated PPP peer negotiates windowBits=8 during CCP, the local PPP daemon configures the node via NGM_DEFLATE_CONFIG, and the peer then ships a compressed frame whose DEFLATE stream carries a large distance code. inflate underflows the 256-byte window and either leaks adjacent kernel heap into the decompressed output (returned to the peer) or panics if the underflow crosses into an unmapped page. That is a genuine remote authenticated heap OOB read / DoS β€” the High severity the finding claims β€” but only on a kernel built with that option.

4. Impact classification

  • Code defect: real, confirmed, deterministic OOB read of up to ~32 KiB of kernel heap before the inflate window (and panic when the underflow leaves the slab/page).
  • On the audited kernel (X86_64_GENERIC, master DEV): latent β€” not reachable without enabling NETGRAPH7_DEFLATE. Effective severity on this config: Low (latent hardening gap). The mxge path is safe.
  • On a kernel with NETGRAPH7_DEFLATE enabled: High β€” remote authenticated heap info-leak / DoS via crafted PPP deflate frames, exactly as the finding states.

5. The fix (fix.diff, verified)

Add a dist > window check in both consume sites, mirroring modern zlib's "invalid distance too far back" rejection:

  • inflate_codes DISTEXTβ†’COPY (sys/net/zlib.c:4820): after the distance is fully decoded, if (c->sub.copy.dist > (uInt)(s->end - s->window)) β†’ set BADCODE, Z_DATA_ERROR, LEAVE.
  • inflate_fast (sys/net/zlib.c:5063): after d is computed, if (d > (uInt)(s->end - s->window)) β†’ UNGRAB; UPDATE; return Z_DATA_ERROR.

The check uses s->end - s->window (the actual allocated window size) so it needs no new state. Verified against the harness: with the patch applied, windowBits=-8 returns Z_DATA_ERROR "invalid distance too far back" (no OOB), while windowBits=-15 still decompresses the legitimate stream (total_out=916). The long-term recommendation (replace the 28-year-old sys/net/zlib.c with the modern sys/vfs/hammer2/zlib/ copy already in the tree, or upstream sys/contrib/zlib/) stands; this minimal fix closes the specific OOB read.

This fix matches the finding markdown's ## Recommended fix (the short-term dist > (1 << s->wbits) guard) in intent; the implementation here uses the equivalent (s->end - s->window) and covers both inflate_codes and inflate_fast (the markdown snippet only showed inflate_codes).

Confirmed kernel references

Detail

Exploit chain

Pure out-of-bounds READ (CWE-125), not a write primitive, so no uid0 chain. Trigger: crafted raw-DEFLATE stream with a distance code > (1<<windowBits) fed to inflate opened with windowBits<15. Primitive: inflate underflows s->window and copies up to ~32KB of adjacent kernel heap into the decompressed output; in the netgraph7_deflate path that output is returned to the remote PPP peer (heap info-leak), or the underflow crosses an unmapped page and panics the kernel (DoS). Ceiling = info-leak + panic; no code-exec primitive derivable from a read-only underflow. Not reachable on the audited kernel without enabling netgraph7_deflate.

Evidence (decisive lines)

findings/poc/DF-0265/run.log (decisive): '[harness] *** SIGSEGV/SIGBUS during inflate at 0x800480f01 ***' / 'nearest alloc #2 [user=0x800481000 size=256 (0x100)]: fault is -255 bytes (BEFORE-start (underflow into guard page))' / 'RESULT: OOB READ CONFIRMED'. run.control.log: windowBits=-15 -> 'inflate returned 1, total_out=916' (no OOB, same code+stream). run_stress.log: 3/3 deterministic. fix.diff verified: with patch, windowBits=-8 -> 'inflate returned -3, msg=invalid distance too far back' (no OOB), windowBits=-15 still total_out=916. zlib.c/zlib.h are byte-identical to sys/net/ (cmp confirmed).

PoC changes

The finding shipped with NO PoC folder; I authored the entire evidence pack from scratch. Wrote harness.c (guard-page zcalloc/zcfree via mmap+mprotect, SIGSEGV siglongjmp catcher, allocation tracking, optional windowBits argv), copied sys/net/zlib.c + sys/net/zlib.h byte-identical, added kshim.h (-include'd) to stub the kernel-only MODULE_VERSION/MAX tokens so the verbatim zlib.c builds in userspace without touching the inflate code under test, and gen_stream.py to produce the crafted raw-DEFLATE stream (pat(258)+gap(400)+pat(258) compressed at windowBits=-15 forces the encoder to emit distance ~658). Iterations: fixed MODULE_VERSION build error, fixed NULL z->zalloc hang (NO_ZCFUNCS means the caller must wire zalloc like ng_deflate.c:255), enlarged the stream gap from 1->400 bytes so the distance exceeds window+max_write_offset (the 1-byte gap gave distance 259 which wraps to exactly s->window, no OOB), added the windowBits=-15 control, and verified fix.diff rejects the stream while preserving legitimate decompression.

Verified recommended fix

fix.diff adds 'if (dist > (uInt)(s->end - s->window)) { z->msg="invalid distance too far back"; ...Z_DATA_ERROR; }' in BOTH inflate_codes (sys/net/zlib.c:4820, after DISTEXT) and inflate_fast (sys/net/zlib.c:5063, after d is computed), mirroring modern zlib's hammer2_zlib_inflate.c:932 / hammer2_zlib_inffast.c:177. Verified: patch rejects the malicious stream (Z_DATA_ERROR) and leaves legitimate windowBits=15 decompression intact. Supersedes the finding markdown proposal (which only sketched inflate_codes; this also covers inflate_fast and uses the actual window size s->end-s->window). Long-term: replace the 28-year-old sys/net/zlib.c with the modern sys/vfs/hammer2/zlib/ copy already in the tree.

Verdict

REPRODUCED at the code level. sys/net/zlib.c is zlib 1.0.4 (FILEVERSION 971210); its inflate_codes COPY case (sys/net/zlib.c:4822-4831) and inflate_fast (sys/net/zlib.c:5073-5088) compute the back-reference source as s->end-(dist-(q-s->window)) with NO check that dist <= window size, unlike the modern copy in sys/vfs/hammer2/zlib/ which guards at hammer2_zlib_inflate.c:932 and hammer2_zlib_inffast.c:177. I linked the EXACT, byte-identical audited sys/net/zlib.c into a userspace harness with a guard-page allocator and fed a crafted raw-DEFLATE stream carrying a distance of ~658 against a 256-byte window (windowBits=-8, the mode ng_deflate.c:270 uses): inflate faulted reading 255 bytes BEFORE the window allocation (run.log). The same stream with windowBits=-15 decompresses cleanly (run.control.log, total_out=916), isolating the cause to the missing check. Reproduced 3/3 (run_stress.log). REACHABILITY CAVEAT: on the shipped master DEV kernel the only vulnerable consumer, netgraph7_deflate, is NOT built (no ng_deflate.ko, no NETGRAPH7 in sys/config/X86_64_GENERIC, kldstat shows only kernel/ehci/xhci); the other consumer, if_mxge.c:675, uses inflateInit (default windowBits=15, 32KiB window) which is safe by construction and root-loaded anyway. So the defect is real and exploitable but LATENT on this kernel config; it becomes a remote authenticated heap OOB-read/DoS (the High severity claimed) only if an operator builds/loads NETGRAPH7_DEFLATE.