# DF-2664 — VERDICT

**Status: REPRODUCED (Low, contained).** The vendored hammer2 LZ4
decompressor (`sys/vfs/hammer2/hammer2_lz4.c`, upstream LZ4 **r97**)
reads one byte past the declared source buffer when called with
`inputSize == 0`, violating the documented "never reads outside of
input buffer" contract (`hammer2_lz4.h:62-65`). The call is reachable
in-kernel from on-media data (`compressed_size == 0` passes the
KKASSERT at `hammer2_strategy.c:199` **even on INVARIANTS kernels**),
but the impact in this caller is fully contained: no panic, no leak,
no corruption — the decode always errors and the caller zeroes the
output. Fix (restore the upstream r96 / v1.9.4 guard) validated.

## 1. Root cause (path:line)

* `sys/vfs/hammer2/hammer2_lz4.c:407` — only `outputSize==0` is
  rejected before the main loop; there is no `inputSize==0` guard.
* `sys/vfs/hammer2/hammer2_lz4.c:418` — `token = *ip++;` executes with
  `iend == ip` when `inputSize==0`: a 1-byte OOB read at `source[0]`.
* The decoder cannot make progress afterwards (`ip(=src+1) > iend` ⇒
  the exact-end condition `ip+length != iend` at `:447` always fails ⇒
  `_output_error` at `:515`), so the byte is **never emitted** — no
  information crosses to the output.

## 2. Provenance — vendored version pinned, guard history traced

* `LZ4_decompress_generic` in `hammer2_lz4.c:372-517` is a verbatim
  logic copy of **upstream LZ4 r97** (`lz4.c` @ svn trunk@97, commit
  `16c0942822`, 2013-06-10): identical statements, identical bounds
  checks; only comments/const differ. r97 is the revision that
  de-genericized the decoder out of `lz4_decoder.h`.
* **r96** (`lz4_decoder.h`, commit `cd3bcd0043`, 2013-05-27) still had
  `if unlikely(!inputSize) goto _output_error;   // A correctly formed
  null-compressed LZ4 must have at least one byte (token=0)` — the
  r97 reorganization **dropped it**.
* **v1.9.4** (current upstream) has it back:
  `if (unlikely(srcSize==0)) { return -1; }` (`lib/lz4.c:1983`).
* DragonFly imported r97 the same day it landed (hammer2 LZ4 support,
  June 2013) and never re-synced — 13 years of upstream hardening are
  absent (see the pass-2 divergence cross-reference in the audit JSON).

## 3. Proof (what was run, on the guest)

Guest: `DragonFly dfbsd 6.5-DEVELOPMENT #0 … X86_64_GENERIC`
(INVARIANTS ON), stock `#0` kernel, cc 8.3.

### A. Unit proof — `run.log`, `run.2.log`

`lz4_zero_input_harness.c` compiles the **verbatim** in-tree decoder
(macros + `LZ4_decompress_generic` + `LZ4_decompress_safe`) and:

* **Test A**: source placed flush at the end of an RW page with a
  `PROT_NONE` guard page after; `LZ4_decompress_safe(src, dst, 0, 65536)`
  → **SIGSEGV at `src` (delta = 0)** inside the decoder — the token
  fetch read `source[0]`, past the declared (empty) input buffer.
* **Test A2 (control)**: `inputSize=1`, `{0x00}` → returns 0 (the
  canonical null stream). Only the `inputSize==0` case is broken.
* **Tests B/C (negative proof)**: 2,000,000 randomized + adversarial
  (all-0xFF / all-0x00 / structured tokens with offsets 0,1,2,7,8,
  0xFFFF / 0xFF-extension runs / valid-shaped streams + byte-flip and
  truncation mutations) with **honest** `(inputSize, outputSize)`:
  sources end flush against guard pages (any read at `iend[0..]`
  faults), destinations carry canaries past `oend` (any write past
  `oend` detected), leading guard pages catch before-dest accesses.
  Result: **0 faults, 0 canary corruptions** across all runs — the
  r97 decoder is memory-safe on LP64 when given honest sizes. The
  heap-OOB in DF-0805 is purely the caller's failure to bound
  `compressed_size`.

### B. In-kernel reachability + containment — `run_kernel.log`

`hammer2_trigger.sh` (adapted from the DF-0805 forger family; only
change: on-disk 4-byte `compressed_size` set to `0x00000000`):

```
[trigger] current size field:  1c 01 00 00
[trigger] overwriting field with 0x00000000 (compressed_size = 0)
[trigger] reading the file as maxx … → 65535 bytes of 0x00
dmesg: READ PATH: Error during decompression.bio 0000000000000000/1024
[trigger] DONE — no panic, image detached cleanly
```

* The `inputSize==0` call executed in-kernel (exactly one dmesg error
  line), **without** tripping the INVARIANTS KKASSERT — unlike
  DF-0805's oversized value, `0` is within the asserted bound.
* The speculative token byte (`data[4]`) sits inside the block content
  / dio allocation; the decode fails; the caller bzeros
  (`hammer2_strategy.c:206-216`); the unprivileged reader gets zeros.
  Guest healthy throughout.

## 4. Exploit chain

None — and none possible in this caller: the 1-byte speculative read
is (a) within the backing allocation for every realistic block layout,
(b) never copied to user-visible output (error path zeroes everything),
(c) read-only. Impact ceiling: none observable; this is a contract
violation / hardening defect. (A *different* caller that sized the
source allocation to exactly `inputSize` bytes would get a 1-byte heap
OOB read — the API contract exists precisely for that.)

## 5. Fix — `fix.diff` (validated)

One guard, restoring upstream r96 / v1.9.4 semantics:

```diff
     if unlikely(outputSize==0) goto _output_error;
     // Empty output buffer
+    if ((endOnInput) && unlikely(inputSize==0)) goto _output_error;
```

* `git apply --check` clean against `sys/vfs/hammer2/hammer2_lz4.c`
  (never applied to the read-only tree).
* **Validation level: unit (verbatim decoder copy)** — deliberate: in
  the kernel the bad behavior (silent 1-byte speculative read) has no
  observable runtime delta vs. the fixed behavior (both produce the
  dmesg error + zeroed output), so a kernel rebuild cannot distinguish
  baseline from fixed; the unit harness is the only level where the
  delta is observable.
* `fix_run.log`: fixed build returns `-1` for `inputSize==0` with **no
  fault** ("FIX VALIDATED"), Test A2 control still passes, and all
  2,000,000 fuzz iterations stay green — the guard breaks nothing.

## 6. Attempts / notes

3 verification stages (unit, unit-fix, in-kernel), all first-try.
Guest left healthy (no panic, no reset needed); artifacts and temp
images cleaned.

**Bottom line:** real, upstream-corroborated contract violation,
reproducible in-kernel even on INVARIANTS kernels, fully contained by
the hammer2 caller — Low severity, one-line fix, fix validated.
