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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5603 Β· 13 files
FileTypeDescriptionSize
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/run
  • build.log β€” full untrimmed compiler output (baseline kernel module build)
  • run.log β€” baseline run narrative + panic excerpt
  • run.2.log β€” patched-kernel run
  • panic.txt β€” serial-console panic proof
  • env.txt β€” guest identity, compiler, INVARIANTS config line
  • fix.diff β€” git-apply-able unified diff (never applied to the host sys/ tree)
  • VERDICT.md, manifest.json, verdict.json
VERDICT.md
↓ download raw

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:

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 reporting csd.spec_vers >= 6 (CSD content is card-controlled). EXT_CSD byte 248 is read verbatim from the card at mmc.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-830 and mmc.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=0 from 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:

  1. vm.sh reset with-src (clean source snapshot), applied fix.diff inside the guest's /usr/src copy only.
  2. 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.
  3. Rebuilt the harness against the same source; ran the identical PoC: kldload rc=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_FAILED from 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_TIME stores a millisecond count into a field documented [us] (mmc.c:104) and compared against mmcbr_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 β†’ spurious MMC_ERR_TIMEOUT on 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-bit SEC_CNT β†’ ivar->sec_count β†’ disk geometry with no sanity bound.
  • mmc.c:1619: EXT_CSD content used without validating EXT_CSD[EXT_CSD_REV] against the CSD spec_vers used to gate parsing.
  • Upstream hardening DF lacks: the CRC-retry loop in FreeBSD's mmc_switch_status() and the retune_paused bracketing in mmc_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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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).
↓ fix.diffper-fix-DF-5603

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).