# DF-2655 — hammer2 ZLIB decompression failure returns zero-filled SUCCESS

`sys/vfs/hammer2/hammer2_strategy.c:229-271` — `hammer2_decompress_ZLIB_callback()`:
when `inflate(Z_FINISH) != Z_STREAM_END` the handler prints
`HAMMER2 ZLIB: Fatal error during decompression.`, bzeros the scratch buffer,
**unconditionally bcopies it into bp->b_data, sets b_resid=0 and never sets
b_error** → the biodone completes the read(2) as SUCCESS with kernel-fabricated
zeros.  The LZ4 sibling (:182-220) has the same missing-b_error pattern AND an
out-of-bounds `bzero(bp->b_data + result, bp->b_bufsize - result)` with a
NEGATIVE `result` (see VERDICT.md — separate, more severe issue flagged).

## Verified on the QEMU guest

DragonFly 6.5-DEVELOPMENT, stock INVARIANTS kernel
(#0 Thu Jul 2 06:02:54 UTC 2026), guest /usr/src == audited tree
(md5 4c0018e3b60e8a23d41863f85f5644d4).

## Demonstrated (stock kernel)

| # | observation | evidence |
|---|---|---|
| 1 | Crafted image mounts with **no check failure**: the corrupted ZLIB payload block is checksum-VALID (XXH64 chain checks + volhdr iCRC32s all recomputed by the patcher) | patch.zlib.log, dump.patched-zlib.log |
| 2 | **PRISTINE** image: zfile reads back bit-identical (md5 876569bd…) | run.a.pristine.log |
| 3 | **PATCHED** image: `read(2)` returns 114688 bytes with exit 0 (SUCCESS) while the entire first 64KB block is **fabricated zeros** (md5 b1e63222…); block 2 intact; dmesg shows the "Fatal error" kprintf — **silent data substitution** | run.b.patched.log |
| 4 | Deterministic: identical fabricated md5 across 3 independent mounts (vn8/vn9/vn10) | run.b.patched.log, run_stock_ab.sh transcript |
| 5 | LZ4 sibling, small failure (result = -9): same silent substitution (zeros + SUCCESS) plus a silent **9-byte OOB write before the 64KB bp->b_data** (mapped heap — no crash) | lz4small-run.log |
| 6 | LZ4 sibling, attacker-chosen magnitude (result = -16381 = -(size-prefix+1)): the OOB `bzero` starts at `bp->b_data - 16381` in an unmapped page → **Fatal trap 12 at memset+0xf0, all CPUs stopped, machine halted at db>** | panic.txt |

Fault-address math (stock run): fault VA 0xfffff80062a22003 = page-aligned
bp->b_data (0xfffff80062a26000) − 16381 = −(prefix+1) → the OOB write length
is **attacker-chosen via the crafted image** (up to the stored block size).

## Fix (validated A/B on rebuilt kernel #1, Tue Oct 20 03:15:00 UTC 2026)

`fix.diff`: on decompression failure both callbacks now set
`B_ERROR | b_error = EIO | b_resid = b_bcount` and return early (the LZ4
early return also removes the negative-result OOB bzero entirely).

| test | stock kernel | fixed kernel |
|---|---|---|
| pristine zfile/lzfile md5 | 876569bd… ✓ | 876569bd… ✓ (bit-identical) |
| zlib-patched read(2) | SUCCESS + zeros | **EIO** (fixrun1.log) |
| lz4-small read(2) | SUCCESS + zeros + 9B OOB | **EIO** (fixrun2.log) |
| lz4-big read(2) | **kernel panic, machine halted** | **EIO, no panic** (fixrun2.log) |

## Reproduce (in the DragonFly guest, as root)

Copy this directory to `/root/df2655`, then:

    ./build.sh                     # cc h2zsub.c + kernel icrc32.c + xxhash.c
    ./mkimage.sh                   # newfs, setcomp zlib:6, files, patch, checks
    ./run_stock_ab.sh              # A pristine / B+C patched (substitution)
    ./run_lz4.sh small             # LZ4 sibling, |result| = 9
    ./run_lz4.sh big               # WARNING: panics + halts the stock kernel

Fixed-kernel validation: apply fix.diff in /usr/src,
`make nativekernel KERNCONF=X86_64_GENERIC && make installkernel`, reboot,
re-run the same tests (expect EIO everywhere, pristine bit-identical).

## Attack preconditions

Crafted hammer2 media (image or disk): the attacker must control the on-media
payload of a compressed block and recompute its XXH64 — i.e. a malicious
image/disk producer, or media corruption + an attacker who can fix checksums.
Mounting requires root on this config (vfs.usermount=0), matching the
Medium "malicious media / integrity" class of DF-2606.  There is no
unprivileged-local or remote trigger.

## LZ4 OOB write — flag for a NEW finding

Observations 5-6 (kernel heap OOB write before bp->b_data, attacker-chosen
length, panic on unmapped start) are a distinct memory-safety defect at
hammer2_strategy.c:215-216, beyond DF-2655's substitution claim.  It is fully
evidenced in this pack (panic.txt, lz4small-run.log) and covered by the same
fix.diff hunk; the orchestrator should file it as its own finding (High:
kernel heap OOB write via crafted media; zero-content, controlled length).

## Files

    h2zsub.c             recursive walker/patcher (dirent->inum->inode->DATA,
                         corrupts payload, fixes XXH64 chain + volhdr iCRCs)
    build.sh mkimage.sh run_stock_ab.sh run_lz4.sh    exact runnable commands
    dump.pristine.log dump.patched-zlib.log dump.patched-lz4.log
    patch.zlib.log patch.lz4.log patch.lz4.small.log
    run.a.pristine.log run.b.patched.log     stock-kernel A/B
    lz4small-run.log                         LZ4 sibling substitution (stock)
    panic.txt                               LZ4-big OOB write -> Fatal trap 12
    fixrun1.log fixrun2.log                  fixed-kernel A/B validation
    k1build.log k1install.log                fix-kernel build (BUILD_RC=0)
    mkfs.log mkimage.log content md5s
    fix.diff VERDICT.md manifest.json verdict.json env.txt
