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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5768 Β· 13 files
FileTypeDescriptionSize
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:

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 = 1 byte);
  • 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.

VERDICT.md
↓ download raw

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)

  1. A malicious MMC card supplies a CSD with WRITE_BL_LEN field bits = 0 (fully parseable; the decoder at mmc.c:1147 computes write_bl_len = 1 << 0 = 1) and minimal erase-group fields (csd.erase_sector = 1, mmc.c:1142-1143).
  2. mmc_discover_cards() computes ivar->erase_sector = 1 * 1 / 512 = 0 (mmc.c:1595-1596; same expression for SD at mmc.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 for au_size == 0) repairs the zero. The card also reports EXT_CSD[ERASE_GRP_SIZE]=0 / au_size=0, which the harness reproduces.
  3. The value reaches mmcsd(4) via MMC_IVAR_ERASE_SECTOR (mmc.c:1949-1951).
  4. The first BIO_FREEBLKS against 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 is root:operator 0640, sys/kern/subr_disk.c:701) executes mmcsd_delete() (mmcsd.c:407-411): start -= start % erase_sector; with erase_sector == 0 β†’ idiv fault β†’ 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched 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/)
↓ fix.diffper-fix-DF-5768

Confirmed kernel references

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.