β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2815

Unserialized global dumper: set_dumper() EBUSY-check TOCTOU / torn struct copy, and clear-or-swap landing during an active dumpsys() β†’ NULL di->priv dereferenced by dev_ddump() (kernel page fault while dumping) and torn-geometry dump misdirection to the wrong device

Field Value
ID DF-2815
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H
CWE CWE-362 (CWE-367 / CWE-667 secondary)
File sys/kern/kern_shutdown.c
Lines 137 (global), 948-961 (writers), 980-983 (handoff)
Area kern
Confidence certain
Discovered 2026-08-31
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

The crash-dump registration is a global struct dumperinfo dumper with zero serialization. set_dumper() does an unlocked check-then-write and set_dumper(NULL) bzeros the 40-byte struct in place. dumpsys() checks dumper.dumper != NULL and then passes &dumper β€” the global itself, not a snapshot β€” to md_dumpsys(), whose dump loops re-read di->priv/ maxiosize/blocksize/mediasize for every block write. The writers are reached via disk_dumpconf() from the DIOCGKERNELDUMP ioctl (no lock; per DF-2743 no privilege check) and from the root-gated kern.dumpdev sysctl (which the ioctl takes no part in). Race (a): two registrations interleave their 40-byte copies β€” mixing priv (device A) with mediaoffset/mediasize (device B): a later dump computes dumplo from B's geometry but writes through A's device (silent destructive disk corruption + disclosure). Race (b): a clear landing after dumpsys()'s check NULLs di->priv mid-dump β†’ dev_ddump(NULL,...) faults. No function-pointer forgery (dumper fn can only be diskdump or NULL) β€” no uid0 route; ceiling is panic DoS (proven) plus destructive dump misdirection (code-proven, not executed to avoid destroying the test disk).

Threat model & preconditions

Attacker with an open fd on a disk device (root; group operator on default 0640 nodes; fully unprivileged when chained with DF-2743 in configurations where disk nodes are openable β€” jails/vkernel hosts with delegated devices) races DIOCGKERNELDUMP against another registration or a dump in progress (root reboot -d / panic-path dumps are stop_cpus-protected, reboot-path dumps are not).

Proof of contest

VERIFIED on the guest (findings/poc/DF-2815/): 4-thread DIOCGKERNELDUMP churn (clear every 64th) + reboot(RB_DUMP|RB_NOSYNC) from a separate process β†’ serial console shows Dumping 666 MB: immediately followed by Fatal trap 12 ... fault virtual address = 0xa8 ... dev_ddump+0xc <- blk_write β€” the NULLed di->priv dereference; baseline without churn: Dump complete. Fix (dumper_lock spinlock + stack snapshot in dumpsys) validated on a rebuilt kernel: identical churn yields Dump complete with the churner provably live.

Validated fix.diff in findings/poc/DF-2815/.

References

  • DF-2743 (the missing ioctl privilege check feeding the same sink), DF-2744 (dumpcheck underflow)

Timeline

  • 2026-08-31 Discovered during pass-2 audit of kern_shutdown.c (GLM 5.3); mid-dump panic reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2815 Β· 14 files
FileTypeDescriptionSize
README.md β€” 4.5 KB ↓ raw
VERDICT.md β€” 5.4 KB ↓ raw
churn.c β€” 3.2 KB view raw
rebootdirect.c β€” 610 B view raw
build.sh β€” 327 B view raw
run.sh β€” 1.6 KB view raw
build.log β€” 146 B view raw
run.baseline.log β€” 573 B view raw
run.patched.log β€” 798 B view raw
serial.race9.log β€” 2.4 KB view raw
panic.txt β€” 1.6 KB view raw
env.txt β€” 785 B view raw
fix.diff β€” 2.7 KB view raw
verdict.json β€” 6.8 KB view raw

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.
VERDICT.md
↓ download raw

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

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to the guest /usr/src copy, kernel rebuilt in-guest (make nativekernel && make installkernel) and booted (#1 Sep 1 17:21:28). Re-running the exact reproducing recipe (4-thread mode-3 churn + rebootdirect RB_DUMP|RB_NOSYNC): the dump now runs to completion ('Dumping 624 MB: ... Dump complete', run.patched.log) with the churner provably alive (console markers at boot() entry and through the kproc waits); no page fault, no panic. In an additional run the churn's clear happened to be in effect at dumpsys() entry and the dump was cleanly skipped - also correct, crash-free behavior. Baseline (stock kernel #0) signature 'Fatal trap 12 ... dev_ddump()+0xc while dumping' is gone.

['run.patched.log - patched-kernel run: churn markers live, full dump progress, Dump complete, no Fatal/panic', 'fix.diff - the validated diff (also carries the DF-2816 hunk validated in that same build)']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Tue Sep 1 17:21:28 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

root/operator fd on a disk device -> 4 threads spam ioctl(DIOCGKERNELDUMP) set/clear (set_dumper NULL/bcopy race on the global) -> separate single-threaded process calls reboot(2) with RB_DUMP|RB_NOSYNC -> kernel enters dumpsys() during a 'set' phase -> churn clear bzeros dumper mid-dump -> dev_ddump(di->priv==NULL) -> dev_needmplock NULL->si_ops deref -> page fault while dumping -> panic in the reboot path (double panic if triggered from the panic path). uid0 NOT reachable: no attacker-controlled function pointer or forged object; impact ceiling = panic DoS + destructive wrong-device dump writes (torn registration variant).

Evidence (decisive lines)

["panic.txt - decisive serial excerpt: 'Dumping 666 MB:' then Fatal trap 12, fault VA 0xa8, dev_ddump()+0xc <- blk_write(), cpu0, pri 31 (CRIT)", 'serial.race9.log - full serial console of the reproducing run (RACE-MARK-9, stock kernel #0)', 'run.baseline.log - clean baseline dump without churn (Dump complete)', "run.patched.log - fix validation: churn markers (T/c) live through shutdown, 'Dumping 624 MB ... Dump complete', no fault (patched kernel #1)", 'churn.c / rebootdirect.c - final PoC sources; run.sh - exact recipes', 'fix.diff - git-apply-checked diff (spinlock dumper_lock + dumpsys snapshot)']

PoC changes

Seed concept (dumpctl churn vs reboot -d) did not reproduce as-is; iterated to a working recipe: (1) churner must be a separate process - threads of the reboot(2) caller are torn down by shutdown_cleanup_proc first (that is DF-2816, which panicked via a different path); (2) removed all in-loop file/console I/O from the churner - the root fs is force-unmounted before the dump and blocked writes freeze the churn threads; (3) replaced reboot(8) with a direct single-threaded reboot(2) helper using RB_NOSYNC|RB_DUMP so dumpsys() is reached while the churner is provably alive; (4) mode-3 churn cadence (clear every 64th ioctl) maximizes NULL duty. Added diagnostic marker variant to prove churner liveness through the shutdown phases.

Verified recommended fix

Serialize set_dumper()/clear with a spinlock and make dumpsys() take a stack snapshot of the dumper under that lock before calling md_dumpsys() (see fix.diff).

Verdict

The global struct dumperinfo dumper in sys/kern/kern_shutdown.c is completely unsynchronized: set_dumper() (:948-961) does an unlocked EBUSY check-then-write / bzero, and dumpsys() (:980-983) hands &dumper itself (not a snapshot) to md_dumpsys(), whose write loops re-read di->priv on every block (minidump_machdep.c:95/143/348/427). A DIOCGKERNELDUMP clear (subr_disk.c:1186-1188, unlocked and unprivileged per DF-2743) landing after dumpsys()'s check NULLs di->priv mid-dump; the next dev_ddump(NULL,...) faults reading NULL->si_ops in dev_needmplock (kern_device.c:119). Demonstrated on the stock INVARIANTS guest: with a 4-thread ioctl churner on the dump device and a single-threaded reboot(2) (RB_DUMP|RB_NOSYNC) caller, the kernel page-faults mid-dump at dev_ddump+0xc (fault VA 0xa8, cpu0, CRIT priority, trace dev_ddump <- blk_write; panic.txt). Ceiling rigorously assessed: the dumper function-pointer field can only be diskdump or NULL and priv only a referenced cdev, so no function-pointer forgery exists - the primitive tops out at kernel-panic DoS (proven) plus code-proven destructive dump misdirection (torn set-vs-set copy mixing priv/geometry of two devices -> wrong-device/wrong-offset dump writes). Fix (spinlock + stack snapshot in dumpsys) validated on a rebuilt guest kernel: identical churn recipe yields 'Dump complete' with the churner provably alive (run.patched.log).