# DF-2644 — NULL-deref of error'd PFS-root chain in hammer2_xop_inode_flush during sync

**Site:** sys/vfs/hammer2/hammer2_flush.c:1334-1338
(`pmp->pfs_iroot_blocksets[clindex] = chain->data->ipdata.u.blockset;`)

**Reach:** unprivileged-triggerable *kernel-panic condition* on
environmental media failure: any PFS whose root-inode chain cannot be
re-read from media during a filesystem sync (dying/pulled disk, short
read, CRC-failed block evicted from cache, dd-corrupted image) panics
the kernel with a NULL-pointer dereference instead of returning an error
from sync(2).

## Reproduce

Three pieces in this pack:

* `inject.diff` — **environment simulation only** (never a fix): sysctl-
  gated fault injector that fails inode-block reads with EIO
  (`vfs.hammer2.df2644_fail_inode_read`).  Needed because a real read
  error cannot be manufactured on the QEMU guest: `vnconfig -u` is
  refused while the mount holds the device open (VNIOCDETACH checks
  `disk_getopencount() > 1`, sys/dev/disk/vn/vn.c:464-466), and
  truncating the vn backing file yields *short* reads without B_ERROR
  (vnstrategy uses VOP_READ uio, sys/dev/disk/vn/vn.c:342-371).
* `fix.diff` — the actual fix (guard + error propagation).
* `run_df2644_inj.sh` — trigger script (mount, dirty PFS root, drop
  chain data, arm injector, `sync`).

Kernel A = stock + inject.diff (panic expected).
Kernel B = stock + inject.diff + fix.diff (panic must be gone; sync
survives; `hammer2: chain error during flush` messages still present).

```
# on the guest, with kernel A booted:
sh /root/df2644/run_df2644_inj.sh        # -> panic in hammer2_xop_inode_flush
```

Expected stock-kernel signature (serial console / vm.sh log):
`hammer2: chain error during flush` messages followed by a kernel page
fault `cpuid = ...; lapic = ...` with backtrace frames
`hammer2_xop_inode_flush() ... hammer2_flush_core() ...` and the faulting
instruction in the blockset-copy line (small constant offset deref of a
NULL chain->data).

## Why the crash is real and not injector-specific

* `hammer2_inode_chain(ip, clindex, HAMMER2_RESOLVE_ALWAYS)`
  (hammer2_flush.c:1319) returns the iroot chain **locked but error'd**
  when `hammer2_chain_load_data()` hits an I/O error:
  hammer2_chain.c:1003-1008 sets `chain->error = HAMMER2_ERROR_EIO`,
  releases the dio and returns with `chain->data == NULL`.
* `chain->flags & HAMMER2_CHAIN_FLUSH_MASK` (hammer2_flush.c:1322) is
  satisfied by HAMMER2_CHAIN_ONFLUSH, which any dirty child under the
  PFS root sets on the iroot chain (hammer2_chain_setflush stops at the
  inode inflection, hammer2_chain.c:147-166).
* `hammer2_flush()`/`hammer2_flush_core()` handle the error'd chain
  *correctly* (hammer2_flush.c:671-679: report, accumulate, skip) — the
  code even prints "hammer2: chain error during flush".
* `hammer2_xop_inode_flush()` then ignores both the chain error and the
  flush return value and dereferences `chain->data->ipdata.u.blockset`
  at :1336-1337 → NULL-page fault → panic.  (The injector only makes
  the *device* fail, exactly like failing hardware.)

Stage-1 per-inode flushes in the same sync take the graceful path, which
the run log shows as interleaved "unable to fsync inode" messages —
demonstrating the contrast between the handled paths and this one.
