# DF-2815 — kern_shutdown.c: unsynchronized `dumper` global (set/clear TOCTOU, torn copy, clear/swap during active dump)

## What

`sys/kern/kern_shutdown.c` keeps the registered crash-dump device in a
global `struct dumperinfo dumper` (kern_shutdown.c:137) with **zero
serialization**:

- `set_dumper()` (kern_shutdown.c:948-961) does an unlocked
  check-then-write (`if (dumper.dumper != NULL) return EBUSY; dumper = *di;`)
  and `set_dumper(NULL)` `bzero()`s the 40-byte struct in place.
- `dumpsys()` (kern_shutdown.c:980-983) checks `dumper.dumper != NULL`
  and then passes **`&dumper` (the global itself, not a snapshot)** to
  `md_dumpsys()`, whose dump loops re-read `di->priv`, `di->maxiosize`,
  `di->blocksize`, `di->mediasize` on every block write
  (sys/platform/pc64/x86_64/minidump_machdep.c:95,143,348,427 and
  dump_machdep.c:124,144,168,188).

The only writers are reached via `disk_dumpconf()` (sys/kern/subr_disk.c:915):
the `DIOCGKERNELDUMP` ioctl (subr_disk.c:1186-1188 — no privilege check and
no lock; see DF-2743), and `sysctl kern.dumpdev`
(kern_shutdown.c:715-726 — root-gated by `SYSCAP_NOSYSCTL_WR`,
kern_sysctl.c:1446, and serialized only against other sysctl writes via the
oid lock, which the ioctl path does not take).

## Impact

1. **Clear during dump (demonstrated):** a `DIOCGKERNELDUMP` clear landing
   after `dumpsys()` passed its check leaves `di->priv == NULL` for the next
   `dev_ddump()` → `dev_needmplock(NULL)` reads `NULL->si_ops`
   (sys/kern/kern_device.c:119-122) → kernel page fault **while dumping**
   (panic in the reboot/panic path instead of a clean dump/reboot).
2. **Swap during dump:** a concurrent *set* for a different device swaps
   `priv` mid-dump → the remainder of kernel memory is written to a
   different physical device at the current dump offset.
3. **Torn registration (set vs set):** two concurrent sets both pass the
   EBUSY check and interleave their 40-byte struct copies → mixed
   `priv`/`mediaoffset`/`mediasize`/`blocksize` from two devices → a later
   dump computes `dumplo` from one device's geometry but writes through
   another device's `priv` → **silent destructive disk writes at the wrong
   offset on the wrong device** (disk corruption; if the dump target is
   readable by a less-privileged user, kernel-memory disclosure).

Exploit ceiling (assessed, not demonstrated): the `dumper` function-pointer
field can only ever be `diskdump` or `NULL` (only `disk_dumpconf()` builds
registrations), and `priv` is only a referenced cdev — no attacker-chosen
function pointer or forged object is possible, so the primitive tops out at
kernel-panic DoS (proven) plus destructive wrong-device dump writes
(code-proven). Not an LPE on its own.

Privilege needed: an open fd on a disk device (root, or group `operator`
with default 0640 nodes; unprivileged only in combination with DF-2743's
missing ioctl privilege check), plus a dump in progress (root `reboot -d`,
`shutdown -d`, or any kernel panic while churn runs).

## Reproduce

Guest: DragonFly 6.5-DEVELOPMENT x86_64, stock X86_64_GENERIC (INVARIANTS).

1. `sh build.sh` — compiles `churn` and `rebootdirect` in the guest.
2. Baseline: `/root/rebootdirect` alone → serial console shows
   `Dumping ... Dump complete` then reboot.
3. Race: `nohup /root/churn /dev/vbd0s1b 900 3 4 & /root/rebootdirect`
   (mode 3 = mostly-set with a clear every 64th ioctl; see run.sh).
   Expected on the vulnerable kernel: mid-dump
   `Fatal trap 12: page fault ... fault virtual address = 0xa8 ...
   dev_ddump() at dev_ddump+0xc / blk_write()` — the NULL `di->priv`
   dereference — see panic.txt.
4. Patched: apply fix.diff, `make nativekernel && make installkernel`,
   reboot, repeat step 3 → `Dump complete` despite the churn.

Files: churn.c (ioctl churner), rebootdirect.c (single-threaded direct
reboot(2) caller), run.sh, panic.txt (decisive serial-log excerpt),
serial.race9.log (full serial log), run.baseline.log (clean baseline dump),
fix.diff (verified fix: spinlock + snapshot), build.log.

## Notes

- The churn must be a *separate process* from the reboot(2) caller: the
  caller's own process is torn down by `shutdown_cleanup_proc()` (see
  DF-2816) before the dump, and console writes freeze in polled mode, so
  in-loop logging/markers contaminate the experiment.
- With the default sync-enabled reboot the churner reliably survives into
  the dump window only when the reboot(2) caller skips the sync/unmount
  phase; `rebootdirect` uses `RB_NOSYNC|RB_DUMP` to reach `dumpsys()`
  quickly (kern_shutdown.c:393) while the churner is provably alive.
