Missing sliding-window allocation in inflate updatewindow() β NULL-pointer-write panic on malformed HAMMER2 zlib block
Summary
hammer2_zlib_inflate.c updatewindow() lines 373-392: MISSING allocation block. Upstream zlib 1.2.8 has if(state->window==Z_NULL){state->window=ZALLOC(strm,1U<<wbits,...)} β ABSENT from vendored copy. state->window=Z_NULL at init :200 never allocated anywhere (grep only kfree/NULL refs no kmalloc unlike deflate.c:254 which correctly kmalloc its window). Comments at :352 "If window does not exist yet, create it" :541 "goto inf_leave will create one" confirm allocation was lost during vendoring. Reachability: hammer2_strategy.c:257 inflate(&strm,Z_FINISH) on attacker-controlled on-disk bytes. Malformed/truncated/over-length zlib stream: inflate produces output but mode<CHECK (LEN/COPY/MATCH/LIT) inf_leave guard :1018-1019 TRUE updatewindow called copy>0 zmemcpy(state->window=NULL,...) writes through NULL = panic. Clean stream reaches DONE skips updatewindow = dormant under normal reads. Trigger: crafted HAMMER2 image ZLIB-compressed block truncated/over-length mount then cat file. Fix: restore kmalloc(1U<<wbits,M_INTWAIT) allocation block.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0842 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df0842_harness.c | trigger-source | kmod harness calling real in-kernel z_inflate* on a 20-byte truncated zlib stream -> NULL-window write | 4.8 KB | view raw |
| Makefile | build-spec | kmod makefile (bsd.kmod.mk) | 148 B | β download |
| make_crafted_image.py | exploit-trigger | generates a crafted HAMMER2 image whose ZLIB block decompresses to 200KB (>avail_out) -> unprivileged cat panics | 2.3 KB | view raw |
| build.sh | build-script | exact kmod build command | 358 B | view raw |
| run.sh | run-script | exact kldload invocation | 581 B | view raw |
| build.log | build-log | full kmod build output on #0 baseline (BUILD_EXIT=0) | 1.1 KB | view raw |
| run.log | run-log | decisive baseline run: harness panic, fatal trap 12, VA=0x0, memcpy+0xfa | 943 B | view raw |
| boot_realtrigger.log | run-log | real unprivileged cat-by-maxx panic on #0 (identical RIP 0xffffffff80bcac8a) | 693 B | view raw |
| panic.txt | panic-signature | both panic signatures (harness + real trigger), fault VA=0x0, supervisor write | 4.2 KB | view raw |
| fix.diff | suggested-fix | git-apply-able: restore missing kmalloc(1<<wbits) window allocation in updatewindow() | 1.0 KB | view raw |
| fix_build.log | build-log | full single-fix kernel build (make -j6 nativekernel, NK_DONE rc=0) | 5.6 MB | β download |
| fix_run.log | run-log | patched #1 kernel: harness 'NO PANIC z_inflate returned -5' + real trigger CAT_EXIT=0 graceful | 1.3 KB | view raw |
| env.txt | environment | uname/cc/sysctls for #0 baseline and #1 fix-validation kernels | 528 B | view raw |
| VERDICT.md | verdict | full narrative: root cause, trigger, reachability chain, impact, fix validation | 9.3 KB | β raw |
| README.md | readme | human-facing summary + build/run instructions | 4.1 KB | β 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-0842 β Missing sliding-window allocation in inflate updatewindow()
Severity: Medium (local DoS / NULL-pointer-write kernel panic)
CWE: CWE-476 (NULL Pointer Dereference)
File: sys/vfs/hammer2/zlib/hammer2_zlib_inflate.c (vendored zlib inflate)
Kernel: DragonFly 6.5-DEVELOPMENT, X86_64_GENERIC (HAMMER2 is the root filesystem)
The bug
HAMMER2 ships a vendored copy of zlib whose inflate never allocates the
sliding window. state->window is set to Z_NULL in inflateInit2_()
(:200) and is only ever referenced by kfree() (:174, :1047) and by
updatewindow() (:382, :389, :392) β no kmalloc() exists for it
anywhere in the file. Upstream zlib 1.2.8 has an explicit
if (state->window == Z_NULL) state->window = ZALLOC(...) block inside
updatewindow(); that block was dropped when zlib was vendored into HAMMER2.
(The sibling hammer2_zlib_deflate.c:254 does correctly kmalloc() its
window, confirming the asymmetry.)
When inflate() is driven with a stream that produces output but exits
before the CHECK trailer (e.g. truncated input, or output buffer exhaustion
mid-decode), the inf_leave guard at :1018-1019 is TRUE and calls
updatewindow(), which zmemcpy()s through state->window == NULL β
write to virtual address 0x0 β fatal page fault (trap 12).
Reachability (unprivileged)
read(2) of a HAMMER2 file whose on-disk block is ZLIB-compressed reaches
hammer2_strategy_read_completion() (hammer2_strategy.c:458-484) β
hammer2_decompress_ZLIB_callback() (:229, calls inflate(&strm, Z_FINISH)
at :257). A malformed block (e.g. a ZLIB stream that decompresses past the
16 KB output buffer, or a truncated stream) triggers the NULL-window write.
The root filesystem of the audit guest is itself HAMMER2, so this code path
is live in the default GENERIC kernel. Realistic precondition (per the audit
threat model): an admin mounts / makes mountable an attacker-controlled
HAMMER2 image; the unprivileged user then reads the malformed file.
Reproduction
Two independent triggers, both confirmed against the unpatched #0 kernel:
1. Deterministic harness (df0842_harness.c + Makefile, build.sh/run.sh)
A kernel module that calls the real in-kernel z_inflate* symbols (the
exact functions used by hammer2_strategy.c:257) on a 20-byte truncated zlib
stream that decodes ~1.8 KB of output before the input is exhausted.
kldload β fatal trap 12, fault VA = 0x0, memcpy+0xfa.
2. Real unprivileged filesystem-image trigger (make_crafted_image.py)
A HAMMER2 image is built with setcomp zlib + setcheck none; a data block's
ZLIB stream is replaced with one that decompresses to 200 KB (β« the kernel's
16 KB output buffer). cat <file> as the unprivileged maxx user
panics the kernel at the identical RIP (memcpy+0xfa, VA = 0x0). See
make_crafted_image.py for the generator and VERDICT.md for the full
reachability chain.
Build / run
# harness (deterministic):
ssh dfbsd # root
cd /root/DF-0842 && make SYSDIR=/usr/src/sys KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC
kldload ./df0842_harness.ko # unpatched: panic; fixed: prints "NO PANIC"
# real unprivileged trigger:
# host: python3 make_crafted_image.py base.img crafted.img
# guest(root): vnconfig + mount crafted.img, chmod a+rX
# guest(maxx): cat /h2mnt/zd/big.bin # unpatched: panic; fixed: clean read
Impact
NULL-pointer-write at a fixed address (page 0 is unmapped in the kernel) β local denial of service (kernel panic). No escalation chain: the fault is a write to VA 0x0 which cannot be redirected to an attacker-controlled page. On a default GENERIC kernel (INVARIANTS ON) the panic is immediate.
Fix
fix.diff restores the missing allocation block in updatewindow(),
matching upstream zlib 1.2.8 (using kmalloc(..., C_ZLIB_BUFFER_INFLATE,
M_INTWAIT) to mirror the existing inflate-state and deflate-window
allocations). Validated: on the patched #1 kernel both triggers behave
gracefully (no panic); the malformed block yields a handled decompression
error (HAMMER2 ZLIB: Fatal error during decompression.) instead of a crash.
DF-0842 β VERDICT
Verdict: REPRODUCED (local DoS / NULL-pointer-write panic), fix VALIDATED
Status: reproduced Β· Impact: panic (local DoS) Β· Confidence: certain
Class: CWE-476 NULL Pointer Dereference (missing allocation, vendored zlib)
Root cause (confirmed by source trace + two live reproductions)
HAMMER2's vendored zlib inflate (sys/vfs/hammer2/zlib/hammer2_zlib_inflate.c)
never allocates the sliding window state->window:
inflateInit2_()setsstate->window = Z_NULL(hammer2_zlib_inflate.c:200).inflateReset2()may also reset it toZ_NULL(:175).- The only other references to
state->windowarekfree()(:174,:1047) and the writes insideupdatewindow()(:382,:389,:392). - There is no
kmalloc()/ZALLOC()forstate->windowanywhere in the file (grep -n 'window' hammer2_zlib_inflate.cshows only NULL sets, frees, and the derefs). Upstream zlib 1.2.8 has an explicitif (state->window == Z_NULL) { state->window = ZALLOC(...); }block insideupdatewindow()β this block was lost when zlib was vendored into HAMMER2. The header claimsZLIB_VERSION "1.2.8"(hammer2_zlib.h:42) but the code is not: the allocation was dropped. The siblinghammer2_zlib_deflate.c:254does correctlykmalloc()its own window, confirming the asymmetry is a vendoring defect, not an intentional omission.
The comment at :352 ("If window does not exist yet, create it") and :541
("goto inf_leave will create one and copy") document the intent β but the code
never creates it.
Trigger mechanism
updatewindow() is called from the inf_leave path of inflate() when:
/* hammer2_zlib_inflate.c:1018-1019 */
if (state->wsize || (out != strm->avail_out && state->mode < BAD &&
(state->mode < CHECK || flush != Z_FINISH)))
if (updatewindow(strm, strm->next_out, out - strm->avail_out)) { ... }
On the first inflate() call state->wsize == 0, so the first clause is
FALSE. With flush == Z_FINISH (the HAMMER2 caller,
hammer2_strategy.c:257), the guard reduces to "output was produced AND
mode < CHECK". CHECK/DONE/BAD/MEM are all >= CHECK
(hammer2_zlib_inflate.h:39-44); the decode modes HEAD β¦ LIT are < CHECK.
So any stream that produces output but exits before the trailer β truncated
input (avail_in exhausted mid-decode) or output-buffer exhaustion (a
stream that decompresses past avail_out) β makes the guard TRUE.
updatewindow() then sets wsize/wnext/whave (:374-378) and immediately
zmemcpy(state->window + ..., ...) (:382/:389/:392) through
state->window == NULL β write to virtual address 0x0 β fatal page fault.
Reachability (unprivileged, default GENERIC)
read(2) as unprivileged user
-> hammer2_vop_strategy / hammer2_strategy_read (hammer2_strategy.c)
-> hammer2_strategy_read_completion (hammer2_strategy.c:458)
switch (HAMMER2_DEC_COMP(focus->bref.methods))
case HAMMER2_COMP_ZLIB: (:480)
-> hammer2_decompress_ZLIB_callback (:229)
inflateInit(&strm_decompress) (:244) window = NULL
strm.avail_in = focus->bytes (:253) on-disk block size
strm.avail_out = bp->b_bufsize (:255) 16 KB
inflate(&strm_decompress, Z_FINISH) (:257)
-> z_inflate() -> inf_leave (mode<CHECK, output) (hammer2_zlib_inflate.c:556,1018)
-> updatewindow() (:366)
-> zmemcpy(state->window == NULL, ...) (:382/389/392)
-> write to VA 0x0 -> fatal trap 12.
HAMMER2 is the root filesystem of the audit guest (vbd0s1d on / (hammer2))
and ships in X86_64_GENERIC (options HAMMER2). The realistic precondition
(per the audit threat model) is an admin mounting / making mountable an
attacker-controlled HAMMER2 image; the unprivileged user then reads the
malformed ZLIB-compressed file. (The sibling finding DF-0265 covered the
other zlib copy sys/net/zlib.c via netgraph7_deflate, which is LATENT β not
in GENERIC. DF-0842 is in HAMMER2, which IS in GENERIC, so it is live.)
Evidence
Reproduction 1 β deterministic harness (df0842_harness.c)
A kernel module calling the real in-kernel z_inflate* symbols (renamed
inflateβz_inflate via hammer2_zlib_zconf.h:50) on a 20-byte truncated
zlib stream that decodes ~1.8 KB before the input is exhausted. On the
unpatched #0 kernel, kldload produces (from boot.log):
DF-0842: inflateInit_ ret=0 ...; now calling z_inflate(Z_FINISH) on a 20-byte truncated stream Fatal user address access from kernel mode from kldload at ffffffff80bcac8a Fatal trap 12: page fault while in kernel mode fault virtual address = 0x0 fault code = supervisor write data, page not present instruction pointer = 0x8:0xffffffff80bcac8a Stopped at memcpy+0xfa: repe movsq (%rsi),%es:(%rdi)
fault code = supervisor write data confirms it is a WRITE through NULL
(the zmemcpy into the unallocated window), and memcpy+0xfa is the
zmemcpy (repe movsq writing through %rdi == NULL).
Reproduction 2 β real unprivileged filesystem trigger (make_crafted_image.py)
A 64 MB HAMMER2 image is created (newfs_hammer2), mounted, and a directory is
marked hammer2 setcomp zlib + hammer2 setcheck none (so the data block's
check is none and its bytes can be freely corrupted without tripping a
metadata CRC). A compressible file is written, the image is unmounted, and the
ZLIB data block is replaced (host-side, make_crafted_image.py) with a stream
that decompresses to 200 KB β far larger than the kernel's 16 KB per-block
output buffer. The image is re-mounted, and the unprivileged maxx user
runs cat /h2mnt/zd/big.bin:
Fatal trap 12: page fault while in kernel mode cpuid = 0; lapic id = 0 fault virtual address = 0x0 fault code = supervisor write data, page not present instruction pointer = 0x8:0xffffffff80bcac8a current process = Idle Stopped at memcpy+0xfa: repe movsq (%rsi),%es:(%rdi)
The RIP (0xffffffff80bcac8a) and the faulting instruction are byte-identical
to the harness panic β confirming both reach the same sink:
updatewindow() β zmemcpy(state->window == NULL).
(current process = Idle indicates the fault is in the HAMMER2 xop strategy
thread performing asynchronous read completion β exactly
hammer2_strategy_read_completion β hammer2_decompress_ZLIB_callback.)
Impact / escalation
A NULL-pointer-WRITE at fixed address 0x0 is a pure local DoS: page 0 is not mapped in the DragonFlyBSD kernel, so the write faults immediately and cannot be redirected to an attacker-controlled page. There is no privilege-escalation chain β the only effect is a kernel panic. (No SMAP/SMEP bypass or slab grooming is relevant here: the primitive is a write to a fixed unmapped address, not a controllable corruption of a live kernel object.)
PoC changes from the (absent) scaffold
There was no finding markdown or PoC scaffold on disk for DF-0842 (only the DB
row). The runner authored the entire evidence pack from the DB claim:
df0842_harness.c + Makefile + build.sh/run.sh (deterministic harness
calling real z_inflate), make_crafted_image.py (real unprivileged
filesystem-image trigger), fix.diff, this VERDICT.md, and the logs.
Fix (fix.diff) β VALIDATED
Restores the missing window allocation in updatewindow(), matching upstream
zlib 1.2.8, using the kernel allocator style already used for the inflate
state (:196) and the deflate window (hammer2_zlib_deflate.c:254):
+ /* if window not allocated yet, allocate it. ... */
+ if (state->window == Z_NULL) {
+ state->window = (unsigned char FAR *)
+ kmalloc(1U << state->wbits,
+ C_ZLIB_BUFFER_INFLATE, M_INTWAIT);
+ if (state->window == Z_NULL) return 1;
+ }
Fix validation (Phase 8, built + booted single-fix kernel)
- Baseline (
#0, unpatched): both triggers panic (fatal trap 12, VA=0x0,memcpy+0xfa) β harness viakldload; real trigger via unprivilegedcat. - Patched (
#1,Sat Jul 11 10:26:05 UTC 2026, built from same source +fix.diffonly): - harness
kldloadβDF-0842: z_inflate returned -5 (NO PANIC -- window was allocated; bug is absent/fixed). avail_out=6384 total_out=1808, guest stays UP, module loads/unloads cleanly. - real trigger
catasmaxxβCAT_EXIT=0, guest UP,dmesgshows the gracefulHAMMER2 ZLIB: Fatal error during decompression.path (hammer2_strategy.c:258-261handlesret != Z_STREAM_ENDby bzeroing the buffer) β no NULL deref, no panic.
fix_status = fixed (clean before/after on both triggers).
Note on the allocation flags
The fix uses plain kmalloc(..., M_INTWAIT) (no M_ZERO), matching upstream
zlib 1.2.8 and the existing deflate-window allocation. The inflate window is
always written via whave/wnext tracking before being read, so uninitialized
contents are not leaked in normal operation. (The unrelated info-leak class of
DF-0266 concerns the other zlib copy sys/net/zlib.c; if desired, adding
M_ZERO here would be defense-in-depth against a future distance-bounds
regression, but it is out of scope for this NULL-deref fix.)
Fix verification
fixedVALIDATED. On the unpatched #0 baseline both triggers panic: the z_inflate kmod harness (kldload) -> 'Fatal trap 12, fault virtual address = 0x0, supervisor write data, Stopped at memcpy+0xfa', and the unprivileged read (maxx cat crafted HAMMER2 image) -> identical 'Fatal trap 12, VA=0x0, RIP 0xffffffff80bcac8a, memcpy+0xfa'. On the single-fix #1 kernel (built from same source + fix.diff, booted bare /boot/kernel/kernel) BOTH triggers behave gracefully: harness -> 'z_inflate returned -5 (NO PANIC -- window was allocated), total_out=1808', module loads/unloads cleanly, guest stays UP; real trigger -> CAT_EXIT=0, guest UP, dmesg shows the handled 'HAMMER2 ZLIB: Fatal error during decompression.' path (hammer2_strategy.c:258 handles ret!=Z_STREAM_END by bzero+continue). Clean before/after => fix closes the bug.
baseline #0 harness: 'DF-0842: inflateInit_ ret=0 ... z_inflate(Z_FINISH) on a 20-byte truncated stream' -> 'Fatal trap 12 ... fault virtual address = 0x0 ... Stopped at memcpy+0xfa'. baseline #0 real (maxx): identical 'Fatal trap 12 ... VA=0x0 ... memcpy+0xfa'. patched #1 harness: 'DF-0842: z_inflate returned -5 (NO PANIC -- window was allocated; bug is absent/fixed). avail_out=6384 total_out=1808', guest UP, no panic. patched #1 real (maxx): CAT_EXIT=0, guest UP, dmesg 'HAMMER2 ZLIB: Fatal error during decompression.' (graceful).
Confirmed kernel references
- sys/vfs/hammer2/zlib/hammer2_zlib_inflate.c:200
- sys/vfs/hammer2/zlib/hammer2_zlib_inflate.c:366
- sys/vfs/hammer2/zlib/hammer2_zlib_inflate.c:373
- sys/vfs/hammer2/zlib/hammer2_zlib_inflate.c:382
- sys/vfs/hammer2/zlib/hammer2_zlib_inflate.c:1018
- sys/vfs/hammer2/zlib/hammer2_zlib_inflate.c:1047
- sys/vfs/hammer2/zlib/hammer2_zlib_deflate.c:254
- sys/vfs/hammer2/hammer2_strategy.c:229
- sys/vfs/hammer2/hammer2_strategy.c:257
- sys/vfs/hammer2/hammer2_strategy.c:458
- sys/vfs/hammer2/hammer2_strategy.c:480
Detail
Exploit chain
none (non-corruption class: NULL-pointer-WRITE at fixed address 0x0). Two independent reproductions on the unpatched #0 kernel: (1) deterministic kmod harness df0842_harness.c calling the REAL in-kernel z_inflate* symbols on a 20-byte truncated zlib stream -> kldload panics at fault VA=0x0, memcpy+0xfa (supervisor write); (2) real UNPRIVILEGED filesystem-image trigger make_crafted_image.py -- a HAMMER2 image with setcomp zlib + setcheck none whose data block's ZLIB stream is replaced with one decompressing to 200KB (>> the 16KB avail_out) -> cat <file> by maxx (uid 1001) panics at the IDENTICAL RIP 0xffffffff80bcac8a (memcpy+0xfa, VA=0x0), confirming both reach the updatewindow()->zmemcpy(NULL) sink. Impact ceiling: pure local DoS (kernel panic); page 0 is unmapped so the write cannot be redirected to an attacker page -- no uid0 chain is derivable, exactly as the task predicted for a fixed-offset NULL deref.
Evidence (decisive lines)
BASELINE #0 harness: "DF-0842: inflateInit_ ret=0 ... now calling z_inflate(Z_FINISH) on a 20-byte truncated stream" / "Fatal trap 12: page fault while in kernel mode" / "fault virtual address = 0x0" / "fault code = supervisor write data, page not present" / "Stopped at memcpy+0xfa: repe movsq (%rsi),%es:(%rdi)". REAL UNPRIVILEGED TRIGGER (maxx cat crafted HAMMER2 image): identical "Fatal trap 12 ... fault virtual address = 0x0 ... instruction pointer = 0x8:0xffffffff80bcac8a ... Stopped at memcpy+0xfa". PATCHED #1 kernel: harness -> "DF-0842: z_inflate returned -5 (NO PANIC -- window was allocated; bug is absent/fixed). avail_out=6384 total_out=1808"; real trigger -> CAT_EXIT=0, guest UP, dmesg "HAMMER2 ZLIB: Fatal error during decompression." (graceful).
PoC changes
No finding markdown or PoC scaffold existed on disk for DF-0842 (only the DB row); the runner authored the entire evidence pack. Created df0842_harness.c (kmod calling real z_inflate* on a truncated stream), Makefile/build.sh/run.sh, make_crafted_image.py (real unprivileged HAMMER2-image trigger: replaces a ZLIB data block with a 200KB-decompressing stream; uses setcheck none so no metadata CRC is tripped), fix.diff, VERDICT.md, README.md, manifest.json, and full logs.
Verified recommended fix
In sys/vfs/hammer2/zlib/hammer2_zlib_inflate.c updatewindow(), immediately after the if (state->wsize == 0){...} init block (~line 378), restore the missing window allocation: if (state->window == Z_NULL) { state->window = (unsigned char FAR *)kmalloc(1U << state->wbits, C_ZLIB_BUFFER_INFLATE, M_INTWAIT); if (state->window == Z_NULL) return 1; }. This matches upstream zlib 1.2.8 and uses the same allocator/bucket as the inflate-state (inflateInit2_ :196) and deflate-window (hammer2_zlib_deflate.c:254) allocations. The finding markdown's proposal was not on disk; this fix is the canonical upstream restoration. Full git-apply-able diff in findings/poc/DF-0842/fix.diff.
Verdict
REPRODUCED (local DoS / NULL-pointer-write panic), fix VALIDATED. The HAMMER2-vendored zlib inflate (sys/vfs/hammer2/zlib/hammer2_zlib_inflate.c) never kmalloc()s state->window: it is set to Z_NULL at inflateInit2_() (:200) and only ever freed (:174/:1047) or written through in updatewindow() (:382/:389/:392) -- no allocation exists anywhere in the file (grep confirms). Upstream zlib 1.2.8 has an explicit if(state->window==Z_NULL) state->window=ZALLOC(...) block inside updatewindow() that was lost during vendoring; the sibling hammer2_zlib_deflate.c:254 does correctly kmalloc its window, confirming the asymmetry. When inflate() is driven with a stream that produces output but exits before the CHECK trailer (truncated input or output-buffer exhaustion), the inf_leave guard at :1018-1019 is TRUE, updatewindow() runs, and zmemcpy(state->window==NULL,...) writes to virtual address 0x0 -> fatal trap 12. Reachability confirmed end-to-end: read(2) of a ZLIB-compressed HAMMER2 file reaches hammer2_strategy.c:480/229 -> inflate(Z_FINISH) at :257; HAMMER2 is the root filesystem in GENERIC so the path is live.
No comments yet.