# 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:158
* `mmc_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 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.
