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

acd_fixate() integer divide-by-zero panic when all acd_get_cap() capability reads failed at attach (cur_write_speed stays 0)

Summary

acd_get_cap assigns cur_write_speed = max(..,177) ONLY inside the success branch of its 5-attempt MODE SENSE(0x2a) loop; if all five attempts fail at attach the M_ZERO softc keeps cur_write_speed == 0 forever. The first CDRIOCFIXATE that passes CLOSE TRACK executes timeout / (cur_write_speed / 177) with divisor 0 -> x86 #DE -> kernel panic. REPRODUCED on the audit guest via device-state emulation (capfail mode: boot trace shows the 5 failing attempts; ioctl reaches the division): 'Fatal trap 18: integer divide fault', 'Stopped at acd_ioctl+0x1842: idivl'; fix validated (division executes with clamped divisor, guest survives). Divisor-clamp fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4185 Β· 7 files
FileTypeDescriptionSize
README.md β€” 2.5 KB ↓ raw
VERDICT.md β€” 3.3 KB ↓ raw
panic.txt β€” 1.8 KB view raw
panic_serial_full.log β€” 105.9 KB view raw
fix.diff β€” 1.1 KB view raw
verdict.json β€” 3.1 KB view raw
manifest.json β€” 873 B view raw

DF-4185 β€” acd_fixate() integer divide-by-zero panic via cur_write_speed == 0

  • File: sys/dev/disk/nata/atapi-cd.c (pass 2)
  • Class: kernel panic / local DoS from device-state-dependent division (CWE-369)
  • Reachability: CDRIOCFIXATE on /dev/acd0 (root / RESTRICTEDROOT per the disk-layer gate), against a drive whose capability page reads all failed at attach (sleepy/ill USB ODD, erroring firmware, or a crafted device).

Root cause

acd_get_cap() (sys/dev/disk/nata/atapi-cd.c:1696-1717) retries the capabilities MODE SENSE 5 times; only on success does it run cdp->cap.cur_write_speed = max(ntohs(...), 177);. If all five attempts fail, the M_ZERO-allocated acd_softc leaves cap.cur_write_speed == 0 forever (no other write site). Later,

/* atapi-cd.c:1158-1160 */
if ((error = acd_test_ready(dev)) != EBUSY) {
    timeout = timeout / (cdp->cap.cur_write_speed / 177);   /* 0/177 == 0 */

divides by cur_write_speed / 177 == 0 β†’ x86 idiv trap 18 β†’ kernel panic.

Reproduction

Crafted/failed-device emulation on the audit guest (DF-3690-family method; hook in ata_atapicmd, driver source unmodified): df_audit.capfail=1 makes every MODE SENSE page 0x2a (capabilities) fail during acd_attach, so cur_write_speed stays 0 exactly as with a real failing drive. df_audit.mode=1 makes the drive accept the burner-only commands (MODE SENSE/SELECT page 0x05, CLOSE TRACK) so fixate proceeds to the division.

./build.sh && ./run.sh          # same trigger binary as DF-4184

Observed (panic.txt, full serial in panic_serial_full.log):

Fatal trap 18: integer divide fault while in kernel mode
cpuid = 5; lapic id = 5
instruction pointer     = 0x8:0xffffffff80340232
current process         = 831
Stopped at      acd_ioctl+0x1842:       idivl   %ecx,%eax     <- inlined acd_fixate

Guest died to DDB; ssh sessions killed mid-command.

Fix validation

fix.diff clamps the divisor (write_speed < 177 -> 177). Validated on the guest with the identical emulated attack (df_audit.capfail=1): pre-fix β†’ trap 18 panic; post-fix (DF-4185/fix_run.log sample at ../DF-4184/fix.2.log) β†’ the division path executes (TEST_UNIT_READY err=5 != EBUSY traced) and the ioctl returns EIO after the normal wait, no panic, guest stays up.

Contents

  • panic.txt β€” decisive serial-console excerpt (trap + backtrace)
  • panic_serial_full.log β€” full serial log of the panic boot
  • fix.diff β€” divisor clamp (subset of DF-4184's diff)
  • trigger binary/source shared with ../DF-4184/
VERDICT.md
↓ download raw

DF-4185 VERDICT

Status: reproduced (impact: panic β€” kernel panic / local DoS; no memory corruption: trap 18 is a faulting divide, not a write primitive) Confidence: certain.

Narrative

acd_get_cap() populates cap.cur_write_speed only inside the success branch of its 5-attempt MODE SENSE(0x2a) loop (atapi-cd.c:1706-1716); on total failure the zero-initialized softc keeps cur_write_speed == 0 for the life of the device. The guard max(...,177) everyone remembers only protects the success path. acd_fixate() later computes timeout / (cur_write_speed / 177) (atapi-cd.c:1159) β€” with speed 0 the divisor is 0 and the kernel takes an integer-divide fault.

Reproduced on the audit guest with the DF-3690-family device-state emulation: df_audit.capfail=1 fails the capability senses at attach (hook converts every page-0x2a MODE SENSE to EIO β€” the trace shows the 5 failing attempts at boot), df_audit.mode=1 lets the burner-only commands succeed so acd_fixate walks to the division. One ioctl(CDRIOCFIXATE) from the trigger binary produced:

Fatal trap 18: integer divide fault while in kernel mode
Stopped at      acd_ioctl+0x1842:       idivl   %ecx,%eax

(acd_ioctl+0x1842 = inlined acd_fixate; RIP 0xffffffff80340232.) The guest went to DDB; in-flight ssh died β€” the honest panic signature. Same run also re-captured the DF-4184 over-read sample (see panic_serial_full.log), which is the full acd_fixate trace up to the trap.

Why "panic" and nothing more

idiv with zero divisor raises #DE before any result is written; there is no attacker-controlled quotient/remainder, no OOB write. Impact ceiling: local DoS (repeatable per fixate call whenever the speed field is 0).

Preconditions (honest)

  1. All five capability-page senses fail at acd_attach (real-world: drive erroring/timing out during probe; crafted device; some USB enclosures) β€” OR any future path that leaves the field 0.
  2. CDRIOCFIXATE callable: /dev/acd0 is root:operator 0640 and the DF disk layer gates opens with SYSCAP_RESTRICTEDROOT (subr_disk.c:1072, verified live: plain operator-group user gets EPERM), so the caller is root/RESTRICTEDROOT software; a drive that failed cap-read at attach but later accepts write-param mode pages + CLOSE TRACK (burner-family behavior or crafted device) walks the whole path.

Fix validation

Same emulation kernel + fix.diff divisor clamp, capfail=1, pl=50: the traced sequence reaches TEST_UNIT_READY err=5 (!= EBUSY) β€” i.e. the division executed β€” and the ioctl returns EIO after the normal fixate wait; no trap, guest uptime clean. Baseline identical configuration = trap 18. Logs: panic.txt (baseline) vs ../DF-4184/fix.2.log (patched).

Path references

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 divisor clamp applied on the same emulation kernel with the same attack (capfail=1, valid page_length): the traced run reaches TEST_UNIT_READY != EBUSY (the division executes) and completes the ioctl without any trap, guest stays up; identical configuration on the unpatched kernel panics with trap 18

baseline: panic.txt ('Fatal trap 18', 'idivl %ecx,%eax'); patched: ../DF-4184/fix.2.log ('survived (no panic)', 'hook ccb0=00 ... err=5' showing the != EBUSY division path taken)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #6: Fri Sep 18 12:49:47 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

panic.txt (Fatal trap 18 + idivl backtrace from the serial console); panic_serial_full.log (full acd_fixate command trace up to the trap, incl. the DF-4184 over-read re-capture); ../DF-4184/fix.2.log (patched kernel, same attack, division path executed, no trap)

PoC changes

trigger binary shared with DF-4184; emulation tunables df_audit.capfail=1 (fail all page-0x2a senses at attach, emulating a failing/crafted drive) + df_audit.mode=1 (accept burner-only commands so acd_fixate reaches the division)

Verified recommended fix

clamp the divisor in acd_fixate (write_speed < 177 -> 177); alternatively default cur_write_speed to 177 in acd_get_cap's failure path

Verdict

acd_fixate() divides by (cdp->cap.cur_write_speed / 177); acd_get_cap() only assigns cur_write_speed (>=177) on a successful capabilities MODE SENSE, so a device whose five cap reads fail at attach leaves the field 0 and the very first CDRIOCFIXATE that gets past CLOSE TRACK takes an integer-divide fault: reproduced as 'Fatal trap 18: integer divide fault', 'Stopped at acd_ioctl+0x1842: idivl %ecx,%eax' on the audit guest using device-state emulation (cap senses forced to fail at attach, burner commands accepted), driver source unmodified. Impact is a kernel panic / local DoS (faulting divide, no write primitive). Caller must pass the disk-layer RESTRICTEDROOT gate, so the practical trigger is root/RESTRICTEDROOT burning software against a failing or crafted drive.