Card-derived zero CMD6 switch timeout reaches live INVARIANTS KASSERT in mmc_switch()/mmc_switch_status() β guaranteed kernel panic at MMC card enumeration
Summary
Both helpers open with KASSERT(timeout != 0) and DFly ships INVARIANTS in the stock config, so the assertions are live panics: the timeout is card-controlled at every call site (cmd6_time = 10 * EXT_CSD[248] read verbatim, no floor) and a crafted/mis-programmed card reporting 0 panics during every enumeration - boot loop if the card is the boot device. Reproduced on the stock guest kernel via production-KLD (compiled-in mmc_switch called with timeout=0, the exact derived value): 'panic: mmc_switch: no timeout', guest down; floor-instead-of-assert fix validated on a full kernel rebuild (clean MMC_ERR_FAILED path, guest up). Physical-adjacency precondition, panic is the ceiling. mmc.c leads for its re-queued pass recorded: 10*GEN_CMD6_TIME ms-vs-us unit bug (~1000x short switch budget), card-controlled SEC_CNT geometry, EXT_CSD REV not cross-checked, CID decode.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5603 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.4 KB | β raw | |
| VERDICT.md | β | 6.5 KB | β raw | |
| df5603_harness.c | β | 3.1 KB | view raw | |
| Makefile | β | 81 B | β download | |
| build.sh | β | 97 B | view raw | |
| run.sh | β | 110 B | view raw | |
| build.log | β | 5.7 KB | view raw | |
| run.log | β | 1.4 KB | view raw | |
| run.2.log | β | 1.1 KB | view raw | |
| panic.txt | β | 819 B | view raw | |
| env.txt | β | 422 B | view raw | |
| fix.diff | β | 1.8 KB | view raw | |
| verdict.json | β | 5.0 KB | view raw |
DF-5603 β card-derived zero switch-timeout panics mmc_switch()/mmc_switch_status() (live KASSERT on stock INVARIANTS kernel)
File: sys/bus/mmc/mmc_subr.c (pass 2, GLM 5.3)
Verdict: reproduced (impact: kernel panic / system DoS), fix validated.
Class: CWE-617 (reachable assertion on card-controlled data) + CWE-20.
What
mmc_switch() (sys/bus/mmc/mmc_subr.c:158) and mmc_switch_status()
(sys/bus/mmc/mmc_subr.c:187) open with:
KASSERT(timeout != 0, ("%s: no timeout", __func__));
On DragonFly's stock kernel this assertion is live: options INVARIANTS
ships in sys/config/X86_64_GENERIC:56 (unlike FreeBSD GENERIC, where
KASSERT is compiled out). The timeout argument is not an arbitrary
programmer-supplied constant β at every call site it is ivar->cmd6_time,
which mmc.c:1661-1665 derives directly from a card-controlled EXT_CSD
byte:
ivar->cmd6_time = 500 * 1000;
if (ivar->csd.spec_vers >= 6) {
ivar->cmd6_time = 10 * ivar->raw_ext_csd[EXT_CSD_GEN_CMD6_TIME];
}
A card reporting spec_vers >= 6 (CSD is also card data) and
EXT_CSD[EXT_CSD_GEN_CMD6_TIME] == 0 (byte 248) yields cmd6_time == 0,
which is then passed as the timeout to mmc_switch() at mmc.c:1672
(erase-group-def switch), mmc.c:709 (bus-width switch), mmc.c:785 (power
class), mmc.c:828 (timing switch) and to mmc_switch_status() at
mmc.c:835 β i.e. every MMC card enumeration reaches one of these calls,
so a crafted (or cheaply mis-programmed) eMMC/MMC card panics the kernel at
boot or on insertion.
How to reproduce (no SD hardware needed)
mmc is compiled into the stock kernel (device mmc, guest config:324), so
a KLD harness can drive the production helpers directly with the exact
card-derived value (0). No SD controller is required; the two device
arguments are two distinct real devices so the helpers' error-log branch
(softc deref) is skipped and the bridge dispatch degrades to
kobj_error_method() β ENXIO (kern/subr_kobj.c:69-72).
# build (guest)
cd /tmp/df5603 && SYSDIR=/usr/src/sys make
# run (guest, as root)
kldload /tmp/df5603/df5603.ko
Expected (stock kernel): serial console shows
DF-5603: calling mmc_switch(timeout=0) as mmc.c does for a card with EXT_CSD[248]=0 ...
panic: mmc_switch: no timeout
mmc_switch() at mmc_switch+0x178
df5603_modevent() at df5603_modevent+0x93
...
Debugger("panic")
and the guest goes down (ssh hangs; vm.sh status β down). See panic.txt,
run.log.
Expected (with fix.diff): both helper calls return 4
(MMC_ERR_FAILED) without panicking; module unloads; guest stays up. See
run.2.log.
Fix
fix.diff β replace both KASSERTs with a 500 ms floor (the same default
mmc.c uses when the EXT_CSD field is absent) and clamp the producer in
mmc.c:1663-1666. Validated by full kernel rebuild (nativekernel) + re-run:
panic gone, clean error return.
Files
df5603_harness.cβ KLD harness (production helpers, synthetic card value)Makefile,build.sh,run.shβ exact build/runbuild.logβ full untrimmed compiler output (baseline kernel module build)run.logβ baseline run narrative + panic excerptrun.2.logβ patched-kernel runpanic.txtβ serial-console panic proofenv.txtβ guest identity, compiler, INVARIANTS config linefix.diffβ git-apply-able unified diff (never applied to the host sys/ tree)VERDICT.md,manifest.json,verdict.json
DF-5603 β VERDICT
Status: reproduced (impact: panic β kernel crash / whole-system DoS;
confidence: certain; attempts: 2 β baseline + patched re-run).
Fix validated: baseline behavior gone on patched kernel.
1. Root cause
sys/bus/mmc/mmc_subr.c enforces its "timeout != 0" API contract with a
KASSERT at both entry points:
mmc_switch()β sys/bus/mmc/mmc_subr.c:158mmc_switch_status()β sys/bus/mmc/mmc_subr.c:187
DragonFly compiles KASSERT to a live panic() whenever INVARIANTS is
configured (sys/sys/systm.h:94-96), and options INVARIANTS ships in
the stock kernel config (sys/config/X86_64_GENERIC:56). The running
guest kernel is exactly that config (uname: X86_64_GENERIC).
The timeout argument is card-derived at every production call site:
mmc.c:1661-1665:ivar->cmd6_time = 10 * ivar->raw_ext_csd[EXT_CSD_GEN_CMD6_TIME];for cards reportingcsd.spec_vers >= 6(CSD content is card-controlled). EXT_CSD byte 248 is read verbatim from the card atmmc.c:1619(mmc_send_ext_csd) β no validation, no floor.- Consumers passing it as
timeout:mmc.c:1672(EXT_CSD_ERASE_GRP_DEF switch, taken whenever the card reports a non-zero erase-group byte),mmc.c:709-711(bus-width switch),mmc.c:785-786(power class),mmc.c:828-830andmmc.c:835-836(timing switch + status wait).
So a card with EXT_CSD[248] == 0 (and spec_vers >= 6 in CSD) reaches
mmc_switch(..., 0, ...) during enumeration β panic: mmc_switch: no
timeout. FreeBSD upstream shares the KASSERT and the 10 * producer
expression, but FreeBSD's GENERIC does not compile INVARIANTS, so the
assertion is dormant there; DragonFly's default config turns it into a
production panic. (Even a non-malicious mis-programmed card β the reason
the mmc quirks table exists β can carry a zeroed byte 248.)
2. Reproduction (baseline)
Harness: findings/poc/DF-5603/df5603_harness.c β a KLD that calls the
production, compiled-in mmc_switch() with timeout == 0 (the exact
value mmc.c derives for such a card), using two distinct real devices for
brdev/reqdev so that (a) the helpers' error-log branch
(device_get_softc(brdev) as struct mmc_softc *) is never taken
(brdev != reqdev short-circuits it at mmc_subr.c:95/:139) and (b) the
bridge dispatch resolves to kobj_error_method() β ENXIO
(sys/kern/subr_kobj.c:69-72), making the code path after the (removed)
assertion harmless. This is the sanctioned evidence class for this rig:
production code driven with a synthetic card value; no SD hardware needed.
Observed (run.log, panic.txt, serial console):
DF-5603: calling mmc_switch(timeout=0) as mmc.c does for a card with EXT_CSD[248]=0 ...
panic: mmc_switch: no timeout
cpuid = 1
mmc_switch() at mmc_switch+0x178
df5603_modevent() at df5603_modevent+0x93
module_register_init() ...
linker_load_module() ...
Debugger("panic")
ssh session hung (timeout 124); vm.sh status β down. That is the
classification table's panic impact, reproduced.
3. Impact ceiling
- Whole-system crash (all CPUs stopped, DDB) at MMC card enumeration β i.e. at boot if the crafted/mis-programmed card is the boot device (persistent boot loop) or on insertion/hotplug.
- Not memory corruption: the primitive is an assertion, not a write; there
is no route to
uid=0from this bug. Threat actor needs a crafted or defective MMC-compliant card (physical insertion) or malicious sdhci-capable hardware (e.g. crafted PCIe device). No unprivileged-local or remote software path touches these helpers (only mmc.c does). - Severity: Medium (system DoS with physical-adjacency precondition on default-config kernels β and default config is where INVARIANTS lives).
4. Fix validation
fix.diff (applies cleanly to the read-only sys/ tree β git apply
--check verified on the host): replaces both KASSERTs with a 500 ms floor
and clamps the producer at mmc.c:1663-1666.
Validated end-to-end on the guest:
vm.sh reset with-src(clean source snapshot), appliedfix.diffinside the guest's/usr/srccopy only.make -j6 nativekernel KERNCONF=X86_64_GENERICβ rc=0 (bus/mmc rebuilt);make installkernelβ rc=0; reboot into kernel#1: Thu Oct 8 12:55:40 UTC 2026.- Rebuilt the harness against the same source; ran the identical PoC:
kldloadrc=0, module load and unload clean, guest stays up (vm.sh statusβ up). dmesg:mmc_switch returned 4 WITHOUT panicking/mmc_switch_status returned 4 WITHOUT panicking(4 =MMC_ERR_FAILEDfrom the ENXIO dispatch).
Baseline panic GONE; the card-derived zero now behaves as a clamped 500 ms poll budget that fails through the normal error path (fail-closed, card enumeration reports an error instead of crashing the box).
5. mmc.c leads for its own (re-queued) pass β noted, not audited here
mmc.c:1663-1664: besides the zero case,10 * EXT_CSD_GEN_CMD6_TIMEstores a millisecond count into a field documented[us](mmc.c:104) and compared againstmmcbr_get_max_busy_timeout()which sdhci returns in Β΅s (sdhci.c:1877-1883, "1 s" = 1000000): the switch poll budget is ~1000Γ shorter than the JEDEC-advertised card timeout β spuriousMMC_ERR_TIMEOUTon legitimately slow cards (HS200/ HS400/erase config). Shared with FreeBSD head (upstream has the same expression); worth flagging when mmc.c is re-audited.mmc.c:1627-1631: card-controlled 32-bitSEC_CNTβivar->sec_countβ disk geometry with no sanity bound.mmc.c:1619: EXT_CSD content used without validatingEXT_CSD[EXT_CSD_REV]against the CSDspec_versused to gate parsing.- Upstream hardening DF lacks: the CRC-retry loop in FreeBSD's
mmc_switch_status()and theretune_pausedbracketing inmmc_switch()(robustness, not security). - DF-1115 (dead SWITCH_ERROR check at mmc_subr.c:211) remains the known
pass-1 finding for this file; FreeBSD head has since fixed it as
(status & R1_SWITCH_ERROR) != 0β DF-1115's fix should match that.
6. Environment
See env.txt: DragonFly 6.5-DEVELOPMENT x86_64, kernel X86_64_GENERIC
(INVARIANTS at config:56), gcc 8.3, QEMU 6 vCPU guest. Guest restored to
the clean with-src snapshot after the runs.
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
fixedfix.diff (both helper KASSERTs -> 500 ms floor + producer clamp in mmc.c:1663) validated by full guest kernel rebuild (make -j6 nativekernel KERNCONF=X86_64_GENERIC, installkernel, reboot into #1 Oct 8 12:55) and re-run of the identical PoC: baseline panic 'mmc_switch: no timeout' GONE; mmc_switch/mmc_switch_status both returned MMC_ERR_FAILED (4) through the normal error path, module unloaded cleanly, guest stayed up. fix_patched_reproduced=0 is the desired outcome (bug no longer reproduces on the patched kernel).
run.2.log (kldload rc=0, unload rc=0, vm.sh status=up) + dmesg markers in VERDICT.md section 4 ('returned 4 WITHOUT panicking' for both helper sites); baseline for comparison in panic.txt/run.log (panic, guest down).
Confirmed kernel references
Detail
Exploit chain
crafted eMMC/MMC card with CSD spec_vers>=6 and EXT_CSD[248]=0 -> card enumeration (mmc_discover_cards) -> mmc.c:1663 cmd6_time=0 -> mmc_switch(...,timeout=0,...) [mmc.c:1672 or :709/:828; mmc_switch_status at :835] -> KASSERT at sys/bus/mmc/mmc_subr.c:158/:187 -> panic, all CPUs stopped, DDB. Boot-device variant = persistent boot loop. No escalation beyond DoS (assertion, not a write primitive).
Evidence (decisive lines)
panic.txt: serial-console proof ('panic: mmc_switch: no timeout', stack mmc_switch<-df5603_modevent<-linker_load_module); run.log: ssh hang (exit 124) + vm.sh status=down right after kldload; run.2.log + dmesg markers: patched kernel runs both helper calls (return 4 = MMC_ERR_FAILED), module unloads, guest stays up; build.log: full KLD build output; env.txt: uname X86_64_GENERIC, config:56 INVARIANTS, gcc 8.3.
PoC changes
No seeded PoC existed (pass-2 new finding). Harness authored from scratch: KLD calling the production compiled-in mmc_switch()/mmc_switch_status() with timeout=0; uses two distinct real devices so the softc-deref error-log branch is skipped (brdev!=reqdev at mmc_subr.c:95/:139) and bridge dispatch degrades to kobj_error_method()->ENXIO; kernel printf is kprintf in DF; Makefile needs SRCS+=bus_if.h device_if.h. Fix validation required nativekernel (snapshot lacks buildworld stamp) and surviving the /tmp wipe on reboot.
Verified recommended fix
Floor the card-derived timeout instead of asserting on it: replace both KASSERT(timeout != 0) in sys/bus/mmc/mmc_subr.c with if (timeout == 0) timeout = 500 * 1000; and clamp the producer at mmc.c:1663 (cmd6_time == 0 -> 500 * 1000).
Verdict
Card-derived zero switch timeout panics the stock DragonFly kernel: mmc.c:1661-1665 copies EXT_CSD[EXT_CSD_GEN_CMD6_TIME] (byte 248, read verbatim from the card at mmc.c:1619) into ivar->cmd6_time for cards reporting CSD spec_vers >= 6, with no floor; every enumeration then passes it to mmc_switch()/mmc_switch_status() (mmc.c:1672/:709/:785/:828/:835), whose KASSERT(timeout != 0) at mmc_subr.c:158/:187 is a live panic because options INVARIANTS ships in X86_64_GENERIC (config:56). A crafted or mis-programmed MMC card (EXT_CSD[248]=0) therefore crashes the kernel at boot/insertion. Reproduced on the stock guest kernel with a production-code KLD harness driving the compiled-in helpers with the exact card-derived value: 'panic: mmc_switch: no timeout', guest down, DDB on serial. Not memory corruption - no uid=0 route; the ceiling is a whole-system DoS requiring a crafted physical card (or sdhci-capable malicious hardware).
No comments yet.