# DF-2815 VERDICT — REPRODUCED (kernel panic while dumping; unsynchronized dumper global)

## Bottom line

The global `struct dumperinfo dumper` in sys/kern/kern_shutdown.c is written
by `set_dumper()` (kern_shutdown.c:948-961) with no lock and no atomicity:
the EBUSY check is a check-then-act TOCTOU, the registration is a 40-byte
struct copy, and the clear path is an in-place `bzero()`. `dumpsys()`
(kern_shutdown.c:980-983) checks `dumper.dumper != NULL` and then hands the
**global itself** (`&dumper`) to `md_dumpsys()`, whose write loops re-read
`di->priv` for every block (minidump_machdep.c:95,143,348,427). A concurrent
`DIOCGKERNELDUMP` clear (subr_disk.c:1186-1188 → disk_dumpconf(dev,0) →
set_dumper(NULL), no lock, no privilege check on the ioctl itself —
DF-2743) that lands after dumpsys()'s check NULLs `di->priv` mid-dump, and
the next `dev_ddump(NULL, ...)` dereferences `NULL->si_ops` in
`dev_needmplock()` (kern_device.c:119-122) → page fault while dumping.

## Reproduction (MARK-9, stock INVARIANTS kernel)

- churner: separate root process, 4 threads, ioctl(DIOCGKERNELDUMP) mostly
  "set" with a clear every 64th ioctl on /dev/vbd0s1b (the configured dump
  device).
- trigger: single-threaded `rebootdirect` calls `reboot(2)` with
  `RB_AUTOBOOT|RB_DUMP|RB_NOSYNC` (NOSYNC skips the sync/unmount phase so
  the dump starts while the churner is provably alive — the kproc-wait
  phase still shows churn progress markers in the diagnostic runs).
- Result (serial console, see panic.txt / serial.race9.log):

```
Dumping 666 MB:Fatal user address access from kernel mode from rebootdirect at ffffffff8062d3fc
Fatal trap 12: page fault while in kernel mode
cpuid = 0; lapic id = 0
fault virtual address    = 0xa8
current process          = 843 (rebootdirect)
current thread           = pri 31 (CRIT)
panic: page fault
dev_ddump() at dev_ddump+0xc
blk_write() at blk_write+0x9a
```

`dev_ddump+0xc` is `dev_needmplock(dev)` → `dev->si_ops->head.flags` at
offset 0xa8 of a NULL `dev` — i.e. `di->priv == NULL`, the just-bzeroed
global. Fault on cpu0 (the dumping CPU), in the rebooting thread, mid-dump
("Dumping 666 MB:" printed, no progress steps yet). This is the exact
predicted primitive, 1/1 once the churner was kept alive into the dump
window.

Earlier attempts that did NOT reproduce taught the setup constraints
(attempts 1-4): (a) the churner must not be a thread of the rebooting
process — boot() tears that process down first (that is DF-2816, which
panicked the box via naccess instead); (b) the churner must not touch the
root filesystem or console after shutdown starts — the forced unmount
blocks those writes forever and freezes the churn threads before the dump;
(c) `reboot -d`/`reboot -dq` (init-mediated) kills or starves the churner
before dumpsys, so the reboot(2) must be issued directly by an unrelated
single-threaded helper; (d) with the default sync-enabled path the window
is still reachable but far narrower — RB_NOSYNC makes it deterministic.

## Exploit chain / escalation ceiling (rigorously assessed)

- Proven: unprivileged-to-root N/A; requires an open fd on a disk device
  (root; operator group on default 0640 nodes; unprivileged only via
  DF-2743's missing ioctl privilege check in setups where such nodes are
  openable) plus a dump in progress (root `reboot -d` / any panic while
  churning). Demonstrated as root: **kernel panic during the dump path**
  (panic-in-reboot; in the panic path it would be a double panic).
- Code-proven but not run (would be destructive to the guest disk):
  the set-vs-set TOCTOU interleaves two 40-byte struct copies and can mix
  `priv` (device A) with `mediaoffset`/`mediasize`/`blocksize` (device B),
  so a subsequent dump computes `dumplo` (dump_machdep.c:313-318) from B's
  geometry but writes through A's device → kernel memory written at a
  wrong offset on the wrong device: silent disk corruption, and RAM/kernel
  disclosure if the misdirected dump target is user-readable.
- NOT achievable with this bug: user→root. The `dumper` function-pointer
  field can only be `diskdump` (subr_disk.c:931) or NULL — no attacker-
  chosen function pointer, no forged object; `priv` is only a referenced
  cdev. The primitive tops out at NULL-deref (DoS) and destructive dump
  misdirection. uid0 escalation would require a separate function-pointer
  forgery primitive that this race does not provide.

## Fix validation

fix.diff (in this pack; git-apply --check'd against the read-only tree):
a `dumper_lock` spinlock serializing set_dumper()/clear, and dumpsys()
taking a **stack snapshot** of the dumper under the lock and passing the
snapshot to md_dumpsys() (safe: md_dumpsys/minidumpsys only read di fields
— verified by audit of both files). Applied to the guest's /usr/src copy,
`make nativekernel && make installkernel`, rebooted into the patched
kernel, re-ran the exact MARK-9 PoC: the dump completes ("Dump complete")
despite identical churn — see run.patched.log. Baseline (unpatched)
signature (mid-dump page fault at dev_ddump+0xc) is gone.

## Kernel references

- sys/kern/kern_shutdown.c:137 (global), :948-961 (set_dumper), :963-984
  (dumpsys check + &dumper handoff)
- sys/kern/subr_disk.c:1186-1188 (unlocked, unprivileged ioctl entry),
  :915-939 (disk_dumpconf builds the registration)
- sys/kern/kern_device.c:119-122,423+ (dev_needmplock NULL deref sink)
- sys/platform/pc64/x86_64/minidump_machdep.c:95,143,348,427 (per-write
  di->priv re-reads); dump_machdep.c:313-318 (geometry math)
