# DF-2832 — syncer-trigger API UAF vs `vn_syncer_thr_stop` teardown

**File:** `sys/kern/vfs_sync.c`
**Class:** CWE-362 / CWE-416 (race → use-after-free write, kernel heap)
**Severity:** Medium (memcorrupt bucket) · **Confidence:** certain (defect),
window demonstrated on-guest (witness); stock crash not observed (expected).

## What the bug is

`trigger_syncer()`, `trigger_syncer_start()`, `trigger_syncer_stop()` and
`speedup_syncer()` (sys/kern/vfs_sync.c:561-614) load `mp->mnt_syncer_ctx`
**lock-free** and then operate on the `struct syncer_ctx` (atomic add ±2 /
set-bit-0 / `wakeup`). `vn_syncer_thr_stop()` (vfs_sync.c:330-358) publishes
`mp->mnt_syncer_ctx = NULL` and then `hashdestroy()`+`kfree()`s the ctx with
**no synchronization** against those readers. A thread preempted between the
load and the RMW completes an atomic write on freed heap memory (M_TEMP).
The callers' `mp` pointer itself is also unreferenced (dounmount frees the
`struct mount` at vfs_syscalls.c:1107-1117), so `mp->mnt_syncer_ctx` can be a
read of freed M_MOUNT memory.

**Reachability (unprivileged side):** any process performing hammer2
modifying ops under dirty-chain pressure:
`open(O_CREAT|O_WRONLY)` → `ncp_writechk` (vfs_vnops.c:481) →
`VFS_MODIFYING` → `hammer2_vfs_modifying` (hammer2_vfsops.c:2925) →
`hammer2_pfs_memory_wait` (hammer2_vfsops.c:2938) →
`trigger_syncer`/`trigger_syncer_start` (hammer2_vfsops.c:2962/2977).
`hammer2_flush.c:272` (`speedup_syncer`) is a kernel-thread caller.
The privileged side is `umount -f` → `vfs_unmount` → `vn_syncer_thr_stop`
(vfs_vfsops.c:135).

Notable: a thread stalled in `hammer2_pfs_memory_wait` during `open()`
holds **no fd on the mount**, so dounmount's process-kill scan
(`process_uses_mount`, vfs_syscalls.c:727-757) does not match it — the
trigger storm runs all the way through the forced-unmount teardown.

## What is in this pack

| file | what |
|---|---|
| `churn_open.c` | unpriv open/write/unlink churner (drives the stall loop) |
| `dirty_writer.c` | unpriv bulk pwrite churner (keeps dirty chains over the limit) |
| `race_stock2.sh` | stock-kernel race: unpriv storm + `umount -f` cycling |
| `witness_kernel_baseline.c` | guest-only instrumented kernel: parks trigger callers at the exact load→RMW boundary, counts hits, `debug.df2832_stop` invokes the **real** `vn_syncer_thr_stop` |
| `witness_kernel_fixed.c` | same + `fix.diff` interlock + busy-spin park |
| `panic.txt` | baseline witness decisive run: `HIT stale ctx=0x…` + `panic` at `lwkt_gettoken+0x64` |
| `run.fixvalidation.log` | fix-validation run: `parked=3 → FREE`, **no HIT**, no NULL-deref panic |
| `fix.diff` | repo-side fix (mnt_token interlock + NULL guards) |
| `VERDICT.md` | full narrative and honesty notes |

## Reproduce (summary)

1. Stock race (no crash expected; window is ns vs umount's ms):
   `sh race_stock2.sh 10` as root in the guest.
2. Witness proof: build `witness_kernel_baseline.c` as the kernel
   (`cp` over `/usr/src/sys/kern/vfs_sync.c`, `make nativekernel &&
   make installkernel`, reboot), then mount a vn-backed hammer2 fs, run
   6× `churn_open` as nobody, wait for `debug.df2832_parked > 0`, then
   `sysctl debug.df2832_stop=1`. Console shows
   `stop: parked=N` → `thr_stop FREE ctx=…` → `DF2832: HIT stale ctx=…`
   (+ the artifact panic below).
3. Fix validation: build `witness_kernel_fixed.c`, same procedure →
   `parked=N → FREE`, **no HIT**.
