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)
PoC verification
Evidence pack
findings/poc/DF-4783 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 realBUF_CMD_READbios viadev_dstrategy()(the exact call the buffer cache makes), each bio using the buf's embeddedb_bio1layer (required β the whole-disk cdev routes throughsubr_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)
- 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". - Faithful battery: 8 mmhold processes Γ 10 delete/re-insert cycles:
ALL cycles completed, guest up, no panic (
userbat.log). - 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()readssi_drv1(mmcsd.c:310) and takessc->sc_lock(mmcsd.c:311) with nothing preventinglockuninit(&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_drv1is 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 > 0arm 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
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)
-
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 reachesmmcsd_strategy()β identical to the "nvzombie" observation in the DF-4646 nvme pack. -
Faithful battery: 8 holder processes Γ 10 card-removal/re-insert cycles (
mmcrace.koaction=1/2 == the realmmc_delete_cards()/mmc_scan()paths, driven from a worker kthread becausedevice_delete_child()self-deadlocks if called from a sysctl handler holding sysctllock). All cycles clean; no panic. -
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) andlockmgr_exclusive+0x5d: orl (%r14),%esi(run 3, MMCSD_LOCK on freed memory). Determined afterwards thatdev_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 againstMMCSD_LOCK_DESTROY(mmcsd.c:245) and the newbus softc free that followsdevice_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_drv1is never cleared, so the dangling pointer is walked by anything that resolved it in time. - the
suspend > 0arm (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_testablenot 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
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.
No comments yet.