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

mmcsd_detach() does not exclude or drain the data path: strategy/open callers race lockuninit() and the newbus softc free (DF-4646 family)

Summary

Teardown kills the task, flushes queue, destroys disk, lockuninit - with no gone-flag, no in-flight drain, no si_drv1 clearing; a strategy/open caller that resolves si_drv1 before the free parks across it and lockmgr's freed memory. Faithful fd-pinned workloads on a real QEMU sdhci-pci+sd-card stack survived 10 removal/re-insert cycles (devfs dead-ops revector blocks them) - honestly classified narrow TOCTOU, not a guaranteed UAF (unlike the reproduced nvme twin DF-4646); over-strong kernel hammer panicked 3/3 in-window as serialization evidence. gone+refcount+si_drv1-clear fix mirroring the validated DF-4646 shape.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4783 Β· 17 files
FileTypeDescriptionSize
README.md β€” 4.3 KB ↓ raw
VERDICT.md β€” 4.4 KB ↓ raw
mmcrace.c β€” 7.7 KB view raw
mmhold.c β€” 1.2 KB view raw
mmio.c β€” 1.1 KB view raw
mmcbang.c β€” 2.6 KB view raw
build.sh β€” 107 B view raw
run.sh β€” 173 B view raw
build.log β€” 353 B view raw
run.log β€” 1.0 KB view raw
userbat.log β€” 331 B view raw
panic-run1.txt β€” 416 B view raw
panic-run2.txt β€” 364 B view raw
panic-run3.txt β€” 680 B view raw
env.txt β€” 190 B view raw
fix.diff β€” 2.5 KB view raw
verdict.json β€” 3.5 KB view raw

PoC DF-4783 β€” mmcsd(4) teardown vs data path (DF-4646 family): NOT reproduced with faithful workloads; 3/3 panics with an over-strong kernel hammer; full analysis inside

Rig

The guest boots with a real emulated SDHCI controller and card:

QEMU_EXTRA="-device sdhci-pci -drive id=sddrive,if=none,format=raw,file=sd.img -device sd-card,drive=sddrive"

sdhci_pci_probe() matches class 0x08/0x05 generically (sdhci_pci.c:300-305), the card enumerates (mmcsd0: 256MB <SD Memory Card> at mmc0 26MHz/4bit), and /dev/mmcsd0 is live. mmcrace.ko then drives:

  • action=10 β€” hammer on: 16 kthreads issue real BUF_CMD_READ bios via dev_dstrategy() (the exact call the buffer cache makes), each bio using the buf's embedded b_bio1 layer (required β€” the whole-disk cdev routes through subr_diskslice dsstrategy()/push_bio()).
  • action=1 β€” the exact card-removal teardown path: device_delete_child(mmc0, mmcsd0) == mmc_delete_cards() (mmc.c:259) == sdhci_cleanup_slot() (sdhci.c:874) == card-detect removal == mmcsd_detach() + newbus softc free.
  • action=2 β€” re-insert: re-add the child with the saved ivars (device_add_child + device_probe_and_attach, i.e. mmc_scan() re-discovery).
  • mmhold.c β€” faithful userland holder: opens /dev/mmcsd0 once and pread()s on the same fd forever (fd pins the cdev via specfs, exactly like a mounted filesystem or busy raw device).

Results (all fresh boots)

  1. Faithful holder (mmhold) across teardown: 10101 successful reads, then every pread returns EBADF (destroy_dev re-vectors ops to dead_dev_ops); process keeps running, guest healthy. Same result nvme's DF-4646 rig called "nvzombie".
  2. Faithful battery: 8 mmhold processes Γ— 10 delete/re-insert cycles: ALL cycles completed, guest up, no panic (userbat.log).
  3. Kernel hammer (dev_dstrategy without a cdev reference) Γ— cycling: panic 3/3 fresh boots, all inside the teardown window: - runs 1-2: Stopped at dev_dstrategy+0x1b: movl 0xc(%rax),%r12d (dev_need_mplock dereferencing the freed cdev) - run 3: Stopped at lockmgr_exclusive+0x5d: orl (%r14),%esi (MMCSD_LOCK on freed memory)

Interpretation (verified against dev_dopen semantics): dev_dopen() does NOT raise the cdev sysref β€” the rig's "open" never pinned the cdev, so the hammer was racing a freed cdev, which no in-tree caller does (specfs/vn_strategy callers hold references). These panics demonstrate the missing serialization is real, but they over-state the primitive: they are NOT reachable by a legitimate workload, so the finding is classified not_reproduced for faithful triggers.

The residual code-level race (why this is still a finding)

mmcsd_detach() (mmcsd.c:209-248) β€” unlike the data path β€” has no exclusion mechanism:

  • mmcsd_strategy() reads si_drv1 (mmcsd.c:310) and takes sc->sc_lock (mmcsd.c:311) with nothing preventing lockuninit(&sc->sc_lock) (mmcsd.c:245) or the newbus softc free from racing a caller parked between those two instructions, or parked on the lock when the free lands. si_drv1 is never cleared (nothing NULLs it), so any strategy entry that resolves the pointer before the free walks freed memory afterwards (run-3 signature).
  • mmcsd_open() (mmcsd.c:286-295) has the same shape.
  • The queue flush (mmcsd.c:233-239) runs outside the lock (provably safe today only because the enqueue-condition check and detach's running/suspend update are both under the lock).
  • The suspend > 0 arm of the enqueue condition (mmcsd.c:312) deliberately queues bios into a consumerless queue with no bound (see DF-4784).

The DF-4646 nvme twin crashed deterministically because its teardown freed queues its own still-running admin thread/IRQs used; mmcsd's equivalent (task thread) is properly joined, and devfs's ops re-vector blocks new post-teardown work β€” so what remains here is a genuine but narrow in-flight-stall TOCTOU (CWE-362/416), not a guaranteed UAF.

Reproduce

# faithful (survives):
cd /root/mmcrig && make && kldload ./mmcrace.ko
nohup /root/mmcrig/mmhold > /root/mmhold.log 2>&1 &
sysctl hw.mmcrace.action=1      # card removal under load
# aggressive (panics 3/3):
sysctl hw.mmcrace.action=10; sleep 4
sysctl hw.mmcrace.action=1; sysctl hw.mmcrace.action=2   # cycle x N
VERDICT.md
↓ download raw

DF-4783 verdict β€” NOT reproduced with faithful workloads; mechanism confirmed by inspection; over-strong harness panics 3/3

What was tested (guest: DragonFly 6.5-DEVELOPMENT, QEMU 7.2 sdhci-pci + sd-card)

  1. Faithful holder across teardown (mmhold.c: open once, pread the same fd forever): 10101 successful reads, then EBADF forever; guest healthy. destroy_dev() re-vectors the pinned cdev's ops to dead_dev_ops, so new post-teardown I/O never reaches mmcsd_strategy() β€” identical to the "nvzombie" observation in the DF-4646 nvme pack.

  2. Faithful battery: 8 holder processes Γ— 10 card-removal/re-insert cycles (mmcrace.ko action=1/2 == the real mmc_delete_cards()/ mmc_scan() paths, driven from a worker kthread because device_delete_child() self-deadlocks if called from a sysctl handler holding sysctllock). All cycles clean; no panic.

  3. Aggressive kernel hammer (16 kthreads calling dev_dstrategy() directly, without the cdev reference that all in-tree callers hold): panic in 3/3 fresh boots, all inside the teardown window β€” dev_dstrategy+0x1b: movl 0xc(%rax),%r12d (runs 1-2, freed cdev) and lockmgr_exclusive+0x5d: orl (%r14),%esi (run 3, MMCSD_LOCK on freed memory). Determined afterwards that dev_dopen() does not raise the cdev sysref, so the rig's "open" never pinned the cdev and these panics race a freed cdev β€” a caller-contract violation no legitimate workload performs. They are retained as evidence of the missing serialization (the teardown genuinely mutates/frees objects the data path is actively walking), but they do not establish a reachable primitive, hence the honest classification: not_reproduced.

The code race that stands (by inspection, certain at the code level)

mmcsd_detach() (mmcsd.c:209-248) vs mmcsd_strategy() (mmcsd.c:304-324) and mmcsd_open() (mmcsd.c:286-295):

  • strategy resolves si_drv1 (mmcsd.c:310) β†’ MMCSD_LOCK (mmcsd.c:311) with zero exclusion against MMCSD_LOCK_DESTROY (mmcsd.c:245) and the newbus softc free that follows device_delete_child() (mmc.c:259, sdhci.c:874). A caller stalled between those instructions across the free lockmgr()s freed memory (run-3 signature). si_drv1 is never cleared, so the dangling pointer is walked by anything that resolved it in time.
  • the suspend > 0 arm (mmcsd.c:312) queues into a consumerless queue with no bound (separately filed as DF-4784).
  • the queue flush (mmcsd.c:233-239) runs outside the lock β€” currently safe only because both the enqueue-condition check and detach's update of running/suspend are under the lock; fragile, and the fix moves it under the lock.

Comparison with the reproduced DF-4646 (nvme): nvme's teardown freed queues its own still-running admin thread and interrupt handlers kept using (active UAF), while mmcsd's task thread is correctly joined and devfs's dead-ops re-vector blocks new work β€” so mmcsd's residual is a narrow in-flight-stall TOCTOU rather than a guaranteed UAF.

Threat model

Trigger = card removal / controller hot-unplug / kldunload while I/O is in flight: physical-access attacker yanking an SD card, or root. Consequence if the window is hit: lockmgr on freed memory β†’ panic or, with adversarial heap reuse, bioq corruption (write primitive into freed-softc reuse). Probability per event: low (window is instructions wide); not hit in 10 faithful cycles on the rig.

Fix

fix.diff mirrors the shape validated for DF-4646: a gone flag set under the lock before the thread kill, an in-flight strategy counter drained before disk_destroy()/lockuninit(), clearing si_drv1 on the raw cdev, and moving the queue flush under the lock. Not build-validated here because the faithful baseline did not reproduce (validation would be vacuous).

Files

mmcrace.c (trigger+hammer), mmhold.c (faithful holder), mmio.c (userspace multi-thread re-opener), mmcbang.c (driver attach/detach invoker β€” superseded by mmcrace), panic-run{1,2,3}.txt, userbat.log, run.log, fix.diff.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not build-validated: faithful baseline did not reproduce, so a patched-kernel comparison would be vacuous; fix.diff mirrors the shape validated for DF-4646 (nvme)

fix.diff
↓ fix.diffper-fix-DF-4783

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (full story); panic-run1..3.txt (3/3 kernel-hammer panics); userbat.log + run.log RUN A/B (faithful holders survive, EBADF revector); mmhold.c/mmcrace.c (rig)

PoC changes

Rig written from scratch. Three generations: (1) sysctl-context device_delete_child self-deadlocks on sysctllock via device_sysctl_fini β€” moved all actions to a worker kthread; (2) standalone struct bio panics push_bio ('too many layers') because the whole-disk cdev routes through subr_diskslice β€” bios must use the buf's embedded b_bio1; (3) kernel dev_dopen does not pin the cdev (no sysref) β€” the hammer raced a freed cdev (3/3 panics), which is how the faithful-holder distinction was established. QEMU sdhci data transfers wedge (inhibit bit) after first MULTI read β€” irrelevant to the race; strategy/queue path is exercised identically on EIO.

Verified recommended fix

Set a gone flag + drain in-flight strategy/open callers under sc_lock before disk_destroy/lockuninit, clear the raw cdev's si_drv1, and move the bio queue flush under the lock (see fix.diff).

Verdict

mmcsd_detach (mmcsd.c:209-248) does not exclude the data path β€” mmcsd_strategy (mmcsd.c:310-311) resolves si_drv1 and takes sc_lock with no synchronization against lockuninit (mmcsd.c:245) and the newbus softc free, and si_drv1 is never cleared β€” a genuine CWE-362/416 in-flight-stall race (card removal / hot-unplug / kldunload during I/O). On a real emulated SDHCI+card (QEMU sdhci-pci), faithful fd-pinned workloads survive: a holder process gets EBADF after teardown (dead_dev_ops revector, 10101 OK reads) and 8 holders x 10 removal/re-insert cycles complete without a panic. An over-strong kernel hammer (dev_dstrategy without the cdev reference in-tree callers hold) panicked 3/3 fresh boots inside the window (dev_dstrategy+0x1b / lockmgr_exclusive+0x5d), but that is a caller-contract violation, not a reachable primitive β€” hence not_reproduced. Mechanism certain by inspection; exploit window narrow; fix mirrors the validated DF-4646 nvme shape.