# DF-2617 VERDICT — zero data_off silent NULL in hammer2_chain_load_data

**Status: REPRODUCED (panic x2 + unkillable hang) · Impact: dos (kernel NULL
dereference panic from `mount`; permanent unkillable wedge from `ls`) ·
Fix: validated on guest (kernel #1 v3) — all three triggers fail cleanly,
legit filesystem still mounts and lists.**

## Root cause (source trace)

`hammer2_chain_load_data()` (sys/vfs/hammer2/hammer2_chain.c:920) short-circuits
when the blockref has no media offset:

```c
938:	if ((chain->bref.data_off & ~HAMMER2_OFF_MASK_RADIX) == 0)
939:		return;
```

For blockref types that REQUIRE a media data block — INODE, INDIRECT, DATA,
media DIRENT (>64-byte names), FREEMAP_NODE, FREEMAP_LEAF — this leaves
`chain->data == NULL` **and `chain->error == 0`**.  `hammer2_chain_lock()`
returns 0 (chain.c:881), so every `chain->error` guard downstream of a
lock/lookup is disarmed.  (Contrast `hammer2_chain_alloc()` chain.c:185-192,
which computes `chain->bytes = 0` for radix 0 and *documents* the embedded
case — the only legitimate radix-0 users are embedded dirents, whose names
live in the bref itself (`hammer2_chain_dirent_test()` chain.c:5786-5795
reads `bref.check.buf` for names <= 64 bytes), and freshly allocated
(HAMMER2_CHAIN_INITIAL) chains whose data_off is 0 until
hammer2_chain_modify() allocates.)

## Sinks verified on the guest (stock INVARIANTS kernel #0)

| # | Sink | Trigger | Observation |
|---|------|---------|-------------|
| 1 | vfsops.c:1309-1324 — after `if (schain->error)` at 1285 passes silently: `ripdata = &schain->data->ipdata;` then `ripdata->meta.pfs_clid` (+0x90) / `pfs_type` | P1: sroot INODE bref `data_off=0` in volhdr, `mount -o ro /dev/vn0@testvol` | **Fatal trap 12**, fault VA **0x90**, `Stopped at hammer2_vfs_mount+0x1118: movq 0x90(%r8),%rax` (panic.txt) |
| 2 | vfsops.c:1555-1561 (`hammer2_update_pmps` PFS-label scan) — `chain->error` guard disarmed, `ripdata = &chain->data->ipdata;` → `hammer2_pfsalloc()` reads `meta.pfs_type` (+0x87) | P2: PFS "testvol" INODE bref `data_off=0` inside sroot's blockset, `mount` | **Fatal trap 12**, fault VA **0x87**, `Stopped at hammer2_pfsalloc+0x5ef: movzbl 0x87(%r13),%eax` (panic2.txt) |
| 3 | chain.c:2524-2529 — `hammer2_chain_lookup()` INDORECT-parent case: `kprintf("hammer2: unexpected NULL data")` then `while (1) tsleep(parent, 0, "xxx", 0);` (no PCATCH, no timeout) | H: first INDIRECT under PFS root `data_off=0`, mount OK, then `ls /mnt/h2` (readdir uses `HAMMER2_LOOKUP_ALWAYS`, descends into the indirect, re-enters lookup with it as parent) | mount RC=0; console: `hammer2: unexpected NULL data on 0xfffff80119504500`; `ls` PID 864 state **D5**, wchan **h2coll**, **`kill -9` ineffective (UNKILLABLE=yes)**; xop backend thread **`h2xop-testvol.23` asleep forever on wchan `xxx`**; mount unusable (run_H.log, dmesg_H.txt) |

The finding's cited sink chain.c:2496 (`parent->data->ipdata.meta.op_flags`
for INODE lookup parents) is the same deref family as sinks 1/2 — in the
mount flows it is shadowed by the vfsops derefs (which run first), and it is
reached generically whenever a corrupt INODE is a lookup parent (e.g. a
corrupted subdirectory).  Both chain.c:2496 and 2524 are protected by the
fix because `hammer2_chain_lookup()` checks `parent->error` at chain.c:2473
*before* the blockref-array switch at 2482 — once load_data arms the error.

## PoC

`forge_df2617.py` (host, python3) builds three malformed images from
`base2617.img` (guest `mkbase2617.sh`: newfs_hammer2 -L testvol, 33 files,
sync, umount — forces INDIRECTs in the PFS root blockset):

* **P1** volhdr sroot INODE bref `data_off = 0`
* **P2** PFS "testvol" INODE bref `data_off = 0` (CHECK_NONE the sroot bref)
* **H**  first INDIRECT bref under the PFS root `data_off = 0` (CHECK_NONE
  sroot + PFS inode brefs)

Ancestor CHECK_NONE (methods=0x00) + volume-header CRC32C recompute per
DF-2616/DF-2620 technique; only volhdr copy #0 carries the magic in these
images (the forger patches every copy that does).

Threat model: mount of a crafted hammer2 image — root, or unprivileged with
`vfs.usermount=1` and an owned device (per the finding).

## Fix (fix.diff — 3 hunks, validated by in-guest rebuild)

1. **hammer2_chain.c load_data**: in the zero-data_off early return, set
   `chain->error = HAMMER2_ERROR_EIO` + kprintf for every type except
   embedded DIRENT and CHAIN_INITIAL chains.  This arms every downstream
   `chain->error` / `parent->error` guard (chain.c:2473, vfsops.c:1285/1427/1555).
2. **hammer2_vfsops.c:1390 label scan**: skip errored chains instead of
   `strcmp()`-ing `chain->data->ipdata.filename` (caught as Fatal trap 12
   at VA 0x100, `strcmp+0x10`, during fix-validation iteration v2).
3. **hammer2_iocom.c:313 hammer2_update_spans**: skip errored chains instead
   of reading `ripdata->meta.pfs_clid` (caught as Fatal trap 12 at VA 0x90,
   `hammer2_autodmsg+0x273` on the iocom thread, during fix-validation
   iteration v2).

Coordination with DF-2616: **orthogonal, not subsuming.**  DF-2616's
`hammer2_bad_data_off()` treats `(data_off & ~MASK_RADIX) == 0` as the legal
"embedded, no media block" case and returns 0; DF-2617 covers exactly that
case for data-requiring types.  Both hunks can coexist: DF-2616 rejects bad
geometry when an offset IS present, DF-2617 rejects the absent offset for
types that require one.

Residual (documented, not fixed here): other `chain->data->ipdata` consumers
reachable only from other operation paths (e.g. xops.c:403/539 on unlink,
vfsops.c:2251 recovery scan) still deref without checking chain->error —
with hunk 1 they now fail only if individually guarded; upstream should
sweep all `->data->` derefs of lookup results for error checks.

## Fix validation (guest, kernel #1 v3, uname in env.txt)

| Trigger | Stock kernel #0 | Patched kernel #1 |
|---|---|---|
| P1 `mount` | Fatal trap 12 @ vfs_mount+0x1118 (VA 0x90) | `hammer2_chain_load_data: illegal zero data_off on bref type 1` → `hammer2_mount: error I/O Error reading super-root` → **mount: Invalid argument (EINVAL)**, guest up (fix_run.log) |
| P2 `mount` | Fatal trap 12 @ pfsalloc+0x5ef (VA 0x87) | kprintf + `I/O error scanning PFS labels` + `PFS label I/O error` → **EINVAL**, guest up |
| H `mount`+`ls` | unkillable D5/h2coll ls, kthread wchan `xxx` | mount OK, `ls` returns (empty/short listing), **no hang, no `xxx` wchan**, umount clean |
| control base2617.img | (baseline: mounts, 33 files) | **mounts, 33 files listed**, umount clean (fix_control.log) |
| root fs (vbd0s1d@ROOT) | boots | boots, hammer2 root fs normal across 3 rebuild/reboot cycles |

## Honest classification notes

* The panic and the hang both require mounting a crafted image.  On the
  stock guest `vfs.usermount=0`, so root is needed; the finding's threat
  model (vfs.usermount=1 + owned device) applies unchanged.
* Sink 3 is a permanent, unkillable kernel-side wedge: the sleeping xop
  thread holds the chain topology lock, so every subsequent operation on
  the mount piles up; the mount can never be unmounted; the only recovery
  is a reboot.  That is a full local DoS of the machine's storage
  subsystem, not just of one directory.
* Sink 1/2 are read-only NULL derefs at fixed near-NULL VAs (0x90/0x87) —
  not directly exploitable for code execution on this kernel config (no
  way to map page 0), hence DoS classification, matching the finding's
  Medium severity.
