# 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_()` sets `state->window = Z_NULL` (`hammer2_zlib_inflate.c:200`).
- `inflateReset2()` may also reset it to `Z_NULL` (`:175`).
- The only other references to `state->window` are `kfree()` (`:174`, `:1047`)
  and the **writes** inside `updatewindow()` (`:382`, `:389`, `:392`).
- **There is no `kmalloc()` / `ZALLOC()` for `state->window` anywhere in the
  file** (`grep -n 'window' hammer2_zlib_inflate.c` shows only NULL sets, frees,
  and the derefs). Upstream zlib 1.2.8 has an explicit
  `if (state->window == Z_NULL) { state->window = ZALLOC(...); }` block inside
  `updatewindow()` — this block was lost when zlib was vendored into HAMMER2.
  The header claims `ZLIB_VERSION "1.2.8"` (`hammer2_zlib.h:42`) but the code is
  not: the allocation was dropped. The sibling `hammer2_zlib_deflate.c:254`
  *does* correctly `kmalloc()` 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:

```c
/* 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`):

```c
+    /* 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 via `kldload`; real trigger via unprivileged `cat`.
- **Patched (`#1`, `Sat Jul 11 10:26:05 UTC 2026`, built from same source +
  `fix.diff` only):**
  - 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 `cat` as `maxx` → `CAT_EXIT=0`, guest UP, `dmesg` shows the
    graceful `HAMMER2 ZLIB: Fatal error during decompression.` path
    (`hammer2_strategy.c:258-261` handles `ret != Z_STREAM_END` by 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.)
