Card-derived erase_sector==0 from CSD WRITE_BL_LEN=0 causes integer divide-by-zero kernel panic in mmcsd_delete() on first TRIM
Summary
erase_sector = erase_sector * write_bl_len / 512 with both factors card-chosen: a crafted CSD with WRITE_BL_LEN bits=0 yields (1*1)/512=0, the EXT_CSD/SD-STATUS overrides never repair it, and the zero flows to mmcsd's `start % erase_sector` on the first BIO_FREEBLKS - i.e. filesystem deletions on the mounted malicious card (TRIM, UFS softdep freeblks). Rig on the production mmc.c + synthetic bridge presenting the crafted MMC card: 'ivar->erase_sector=0' then Fatal trap 18 idivl in mmcsd_task 2/2; MAX(...,1) fix validated on a rebuilt kernel (erase_sector=1, TRIM completes). DF-5339/5430/5603 family threat model.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5768 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.3 KB | β raw | |
| VERDICT.md | β | 4.5 KB | β raw | |
| dfh_harness.c | β | 19.8 KB | view raw | |
| Makefile | β | 398 B | β download | |
| build.sh | β | 97 B | view raw | |
| run.sh | β | 110 B | view raw | |
| build.log | β | 5.8 KB | view raw | |
| run.log | β | 925 B | view raw | |
| fix.log | β | 1.1 KB | view raw | |
| panic.txt | β | 1.2 KB | view raw | |
| env.txt | β | 322 B | view raw | |
| fix.diff | β | 1.9 KB | view raw | |
| verdict.json | β | 5.1 KB | view raw |
DF-5768 β card-derived erase_sector == 0 β integer divide-by-zero kernel panic in mmcsd_delete() (TRIM path)
File: sys/bus/mmc/mmc.c (pass 2, GLM 5.3) Β· sink in sys/dev/disk/mmcsd/mmcsd.c
Class: CWE-369 (divide by zero on externally-influenced data) + CWE-20
What
mmc_discover_cards() derives the card erase-sector size with truncating
integer division from two card-supplied CSD fields:
- SD:
sys/bus/mmc/mmc.c:1476-1477ivar->erase_sector = csd.erase_sector * csd.write_bl_len / MMC_SECTOR_SIZE; - MMC:
sys/bus/mmc/mmc.c:1595-1596(identical expression)
csd.erase_sector >= 1 (WP_GRP/ERASE_GRP fields, +1 in the decoder) and
csd.write_bl_len = 1 << WRITE_BL_LEN_bits with a 4-bit, unvalidated
field, so a crafted CSD with WRITE_BL_LEN bits == 0 gives
write_bl_len == 1 and ivar->erase_sector == (1 * 1) / 512 == 0.
The EXT_CSD override (mmc.c:1669-1671, 1024 * byte, only when byte
β 0) and the SD-STATUS override (mmc.c:1538-1540, 16 << au_size,
only when β 0) never repair a zero β a hostile card simply reports
EXT_CSD[224] == 0 / au_size == 0.
The zero flows to mmcsd(4) via MMC_IVAR_ERASE_SECTOR
(mmc.c:1949-1951) and the first BIO_FREEBLKS (TRIM) against the disk
executes, in production mmcsd_delete()
(sys/dev/disk/mmcsd/mmcsd.c:407-411):
erase_sector = mmc_get_erase_sector(dev); /* == 0 */
start = block + erase_sector - 1;
start -= start % erase_sector; /* %0 β #DE β panic */
The kernel issues BIO_FREEBLKS through VOP_FREEBLKS /
devfs_spec_freeblks() (sys/vfs/devfs/devfs_vnops.c:2028-2049) β i.e.
deleting blocks on a UFS filesystem mounted with TRIM, softdep
freeblks processing, etc. Raw disk nodes are root:operator 0640
(sys/kern/subr_disk.c:701). Threat model: malicious/mis-programmed
SD/MMC card inserted into a machine (the DF-5339/5430/5603 family
threat model); the panic also fires at mount/first-TRIM time with no
user interaction beyond normal filesystem use.
How to reproduce (no SD hardware needed)
The harness KLD compiles the production sys/bus/mmc/mmc.c into the
module (verbatim #include) and registers it on a fake mmcbr bridge
(dfhbr) that answers enumeration with a synthetic MMC card:
- CSD:
spec_vers = 5,read_bl_len = 512, erase group fields 0,WRITE_BL_LEN bits = 0(βwrite_bl_len = 1byte); - EXT_CSD:
SEC_CNT = 4096,ERASE_GRP_SIZE = 0(keeps the CSD-derived value),CARD_TYPE = 0; - a protocol-conformant state machine (CMD2 stops answering once the card owns an RCA, so production discovery terminates after one card).
The builtin production mmcsd(4) then attaches /dev/mmcsd0, and the
module issues a BUF_CMD_FREEBLKS bio that is a line-for-line mirror of
devfs_spec_freeblks() (devfs_vnops.c:2031-2045).
# guest (root), from a clean boot:
cd /tmp/df5768 && SYSDIR=/usr/src/sys make
kldload ./df5768.ko
Expected (stock kernel): serial console shows
DF-5768: csd.erase_sector=1 csd.write_bl_len=1 -> ivar->erase_sector=0 (mmc.c:1595-1596) DF-5768: issuing BUF_CMD_FREEBLKS to mmcsd0 ... Fatal trap 18: integer divide fault while in kernel mode ... Stopped at mmcsd_task+0x6cb: idivl %ecx,%eax
and the guest goes down. With the fix (fix.diff), the same run prints
ivar->erase_sector=1, the TRIM completes, kldunload is clean and the
guest stays up.
DF-5768 β VERDICT
Finding: card-derived ivar->erase_sector == 0 β integer divide-by-zero
kernel panic in mmcsd_delete() on the first TRIM (BIO_FREEBLKS).
File: root cause sys/bus/mmc/mmc.c:1476-1477 (SD) and mmc.c:1595-1596
(MMC); sink sys/dev/disk/mmcsd/mmcsd.c:407-411.
Verdict: REPRODUCED β kernel panic (deterministic, 2/2 baseline runs);
fix validated on a patched kernel.
1. Root cause chain (all production code)
- A malicious MMC card supplies a CSD with
WRITE_BL_LENfield bits = 0 (fully parseable; the decoder atmmc.c:1147computeswrite_bl_len = 1 << 0 = 1) and minimal erase-group fields (csd.erase_sector = 1,mmc.c:1142-1143). mmc_discover_cards()computesivar->erase_sector = 1 * 1 / 512 = 0(mmc.c:1595-1596; same expression for SD atmmc.c:1476-1477). Neither the EXT_CSD override (mmc.c:1669-1671, skipped for byte 0) nor the SD-STATUS override (mmc.c:1538-1540, skipped forau_size == 0) repairs the zero. The card also reportsEXT_CSD[ERASE_GRP_SIZE]=0/au_size=0, which the harness reproduces.- The value reaches mmcsd(4) via
MMC_IVAR_ERASE_SECTOR(mmc.c:1949-1951). - The first
BIO_FREEBLKSagainst the disk (kernel TRIM path:VOP_FREEBLKSβdevfs_spec_freeblks(),sys/vfs/devfs/devfs_vnops.c:2028-2049; issued by UFS softdep freeblks / mounted-with-TRIM deletions; raw node isroot:operator 0640,sys/kern/subr_disk.c:701) executesmmcsd_delete()(mmcsd.c:407-411):start -= start % erase_sector;witherase_sector == 0βidivfault βFatal trap 18: integer divide faultβ panic.
2. Reproduction (no SD hardware)
KLD harness (dfh_harness.c) containing the production
sys/bus/mmc/mmc.c compiled in verbatim, registered on a fake mmcbr
bridge (dfhbr) that presents the crafted card (see README.md). The
builtin production mmcsd(4) attaches /dev/mmcsd0; the module then
issues a BUF_CMD_FREEBLKS bio that mirrors devfs_spec_freeblks()
line for line.
Baseline (stock kernel, clean snapshot, run twice):
mmc0: <MMC/SD bus> on dfhbr0 mmcsd0: 2MB <MMC Memory Card> at mmc0 26MHz/1bit DF-5768: csd.erase_sector=1 csd.write_bl_len=1 -> ivar->erase_sector=0 (mmc.c:1595-1596) DF-5768: issuing BUF_CMD_FREEBLKS to mmcsd0 (exactly devfs_spec_freeblks, devfs_vnops.c:2031-2045)... Fatal trap 18: integer divide fault while in kernel mode kernel: type 18 trap, code=0 Stopped at mmcsd_task+0x6cb: idivl %ecx,%eax
The idivl in the (inlined mmcsd_delete) mmcsd_task frame is the
modulo by the card-derived zero. Both baseline runs panicked identically
(panic.txt = run 2; run 1 identical modulo addresses). ssh died with the
guest (vm.sh status β down / DDB).
3. Fix validation
fix.diff floors the derived value: MAX(..., 1) at both sites
(mmc.c:1478, mmc.c:1597 after patching). Applied inside the guest's
/usr/src only; make -j6 nativekernel KERNCONF=X86_64_GENERIC β
kernel #1: Sun Oct 11 18:02:40 UTC 2026; harness rebuilt against the
patched source; identical PoC re-run:
DF-5768: csd.erase_sector=1 csd.write_bl_len=1 -> ivar->erase_sector=1 (mmc.c:1595-1596) DF-5768: issuing BUF_CMD_FREEBLKS to mmcsd0 ... DF-5768: biowait returned - divide-by-zero did NOT occur kldunload rc=0
Guest stayed up, module unloaded cleanly. Baseline panic GONE; the card-derived zero now behaves as a 1-sector floor and TRIM flows through the normal production path.
4. Impact / threat
Malicious (or grossly mis-programmed) SD/MMC card β deterministic kernel
panic the first time the block layer frees blocks on the card (TRIM
mount option, softdep freeblks, or any root-issued FREEBLKS). Same
threat model as the DF-5339/5430/5603 family: attacker-controlled
peripheral data parsed at mount/insertion time. DoS, not memory
corruption: idiv traps before any memory misuse, so there is no
exploitation path beyond denial of service.
5. Environment
See env.txt: DragonFly 6.5-DEVELOPMENT x86_64, X86_64_GENERIC with
INVARIANTS (config:56), gcc 8.3, QEMU 6 vCPU / 4 GB 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
fixedPatched kernel (kernel #1 Sun Oct 11 18:02:40 UTC 2026, guest /usr/src only) with MAX(...,1) at mmc.c:1478/1597: identical PoC prints ivar->erase_sector=1, the FREEBLKS bio completes through the normal production erase path, 'divide-by-zero did NOT occur', kldunload rc=0, guest stays up. Baseline panic eliminated.
fix.log (full patched-run console output incl. erase_sector=1, biowait return, clean kldunload); fix.diff (git apply --check clean against sys/)
Confirmed kernel references
- sys/bus/mmc/mmc.c:1476
- sys/bus/mmc/mmc.c:1595
- sys/bus/mmc/mmc.c:1142
- sys/bus/mmc/mmc.c:1147
- sys/bus/mmc/mmc.c:1538
- sys/bus/mmc/mmc.c:1669
- sys/bus/mmc/mmc.c:1949
- sys/dev/disk/mmcsd/mmcsd.c:407
- sys/dev/disk/mmcsd/mmcsd.c:408
- sys/dev/disk/mmcsd/mmcsd.c:409
- sys/dev/disk/mmcsd/mmcsd.c:411
- sys/vfs/devfs/devfs_vnops.c:2028
- sys/kern/subr_disk.c:701
Detail
Exploit chain
malicious SD/MMC card -> CSD WRITE_BL_LEN=0 bits -> ivar->erase_sector=0 at discovery -> mmcsd0 attaches -> first BIO_FREEBLKS (TRIM via VOP_FREEBLKS/devfs_spec_freeblks or root-issued freeblks) -> mmcsd_delete modulo by zero -> Fatal trap 18 integer divide fault -> kernel panic / system down. (No memory-corruption primitive: trap fires before any misuse; impact ceiling is denial of service.)
Evidence (decisive lines)
panic.txt (full trap record incl. 'DF-5768: csd.erase_sector=1 csd.write_bl_len=1 -> ivar->erase_sector=0' then 'Fatal trap 18' and 'idivl %ecx,%eax' in mmcsd_task+0x6cb); run.log (2 baseline runs); fix.log (patched kernel: erase_sector=1, biowait returns, kldunload rc=0); VERDICT.md; dfh_harness.c; build.log
PoC changes
The finding's PoC sketch (direct syscall/ioctl trigger) was not viable: DragonFly has no DIOCGDELETE userland ioctl for this path. Replaced with the family-standard KLD harness: production sys/bus/mmc/mmc.c compiled verbatim into the module (SYSCTL/DRIVER_MODULE neutered with macro save/restore), registered on a synthetic mmcbr bridge implementing a JEDEC-conformant fake card (state machine: CMD2 stops answering after RCA assignment so production discovery terminates; SD probe fails -> mode_mmc; CSD with WRITE_BL_LEN bits=0, spec_vers=5, EXT_CSD SEC_CNT=4096/ERASE_GRP_SIZE=0). The trigger mirrors devfs_spec_freeblks() line-for-line (getpbuf + BUF_CMD_FREEBLKS + dev_dstrategy on the mmcsd0 cdev). Also fixed: module-init ordering (dfhbr devclass must exist before device_add_child -> deferred to a kthread), -Werror macro redefinitions, duplicate case labels (CMD3/SD_CMD3), and R1 status construction.
Verified recommended fix
Clamp the card-derived erase sector at both CSD-derivation sites: ivar->erase_sector = MAX(ivar->csd.erase_sector * ivar->csd.write_bl_len / MMC_SECTOR_SIZE, 1); (SD mmc.c:1476-1477 and MMC mmc.c:1595-1596) - see fix.diff, validated end-to-end.
Verdict
Card-derived ivar->erase_sector == 0 (mmc.c:1476-1477 SD / 1595-1596 MMC: erase_sector * write_bl_len / 512 truncates to 0 for a CSD with WRITE_BL_LEN bits=0) reaches production mmcsd_delete() (mmcsd.c:407-411) via MMC_IVAR_ERASE_SECTOR; the first BIO_FREEBLKS (kernel TRIM path, devfs_spec_freeblks devfs_vnops.c:2028-2049) executes start % erase_sector == %0 and panics the kernel. Reproduced deterministically (2/2 runs) end-to-end on the stock INVARIANTS kernel with a KLD harness that compiles the production mmc.c in and drives it with a synthetic malicious MMC card (fake mmcbr bridge); the builtin production mmcsd attached /dev/mmcsd0 and the panic fired inside it: 'Fatal trap 18: integer divide fault', 'Stopped at mmcsd_task+0x6cb: idivl %ecx,%eax'. Fix validated on a rebuilt kernel: MAX(...,1) clamp at both derivation sites -> erase_sector=1, TRIM completes, clean kldunload, guest stays up. DoS only: the idiv traps before any memory misuse; no escalation path.
No comments yet.