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

Kernel divide-by-zero panics from crafted RAID geometry (width/interleave/total_sectors == 0)

Summary

Multiple arithmetic sites in I/O path and status ioctl divide or modulo by raid geometry fields copied verbatim from attacker-controlled metadata never checked for zero. ata_raid_status :1028 total_sectors==0 divide-by-zero. ata_raid_strategy RAID0/RAID01 :359-361 divides by interleave and width both from metadata. RAID5 :366-373 same. ata_raid_attach :181 heads*sectors divisor. No parser validates width/interleave/total_sectors non-zero before softc attached and exposed for I/O.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2496 Β· 17 files
FileTypeDescriptionSize
README.md β€” 2.8 KB ↓ raw
VERDICT.md β€” 4.3 KB ↓ raw
dfrig3_ata.c β€” 36.4 KB view raw
Makefile β€” 445 B ↓ download
atastat.c β€” 927 B view raw
build.sh β€” 185 B view raw
run.sh β€” 246 B view raw
run_nonfatal.log β€” 4.4 KB view raw
env.txt β€” 652 B view raw
panic_strategy.txt β€” 584 B view raw
panic_status_ioctl.txt β€” 695 B view raw
panic_attach.txt β€” 696 B view raw
fix.diff β€” 6.4 KB view raw
fix_build.log β€” 206 B view raw
fix_validation.log β€” 46.5 KB view raw
manifest.json β€” 1.7 KB view raw
verdict.json β€” 5.7 KB view raw

DF-2496 β€” divide-by-zero panics from crafted RAID geometry (nata ata-raid)

What was verified

sys/dev/disk/nata/ata-raid.c copies RAID geometry (interleave, width, total_sectors, heads/sectors) verbatim from on-disk metadata and uses the fields as divisors with no zero check. All three filed divide sites were driven to a deterministic kernel panic (fatal trap 18, integer divide fault) on the QEMU guest, each in its own boot:

site code proof
ata_raid_strategy RAID0/01 chunk = blkno % rdp->interleave (ata-raid.c:359, interleave=0 from crafted Intel stripe_sectors) panic_strategy.txt: Fatal trap 18 ... Stopped at ata_raid_strategy+0x2f3: divq %r12,%eax
ata_raid_status via REAL /dev/ata ioctl progress = 100 * rebuild_lba / total_sectors (ata-raid.c:1028, total_sectors=0 from crafted Intel total_sectors) panic_status_ioctl.txt: Fatal trap 18 ... Stopped at ata_raid_ioctl+0x11b: divq 0x48(%r14),%eax β€” IP in the real kernel built-in ataraid (0xffffffff803388eb), reached by the real userland atastat ioctl
ata_raid_attach total_sectors/(heads*sectors) (ata-raid.c:181, heads=0; the rounddown(..., interleave*width) at :157 is the same class) panic_attach.txt: Fatal trap 18 ... divq (attach inlined into the sysctl caller at -O2)

Setup half (run_nonfatal.log, test 12): the production ata_raid_intel_read_meta() accepts crafted metadata with stripe_sectors=0 and total_sectors=0 and installs an AR_S_READY RAID0 array with interleave=0, total_sectors=0 β€” no parser validation.

Build / run (guest, root; requires vm.sh reset with-src)

cd /root/dfrig3 && ./build.sh     # KLD: production ata-raid.c #included verbatim
kldload ./dfrig3_ata.ko
sysctl kern.dfrig3_ata_test=12    # non-fatal: install degenerate geometry, print it
sysctl -n kern.dfrig3_ata_status
sysctl kern.dfrig3_ata_test=20    # FATAL: strategy divide (panic on serial)
# separate boots:
sysctl kern.dfrig3_ata_test=21    # arm REAL ata_raid_arrays[0], then:
/root/dfrig3/atastat 0            # FATAL: real ioctl hits status divide
sysctl kern.dfrig3_ata_test=22    # FATAL: attach divide

Expected (baseline): tests 20/21/22 each end in Fatal trap 18: integer divide fault while in kernel mode on the serial console (guest dead at db>).

Fix validation

fix.diff guards all sites (strategy RAID0/01/5 geometry check β†’ biofinish(EIO), status->progress ternary, attach rounddown/ncylinders ternaries). Single-fix kernel built with nativekernel (see fix_build.log), rig rebuilt against the fixed source; fix_validation.log shows tests 20/21/22 all SURVIVING on the patched kernel: ar0: degenerate RAID0/01 geometry (interleave=0 width=2) + rig_bio_done b_error=5, attach completes, and the exact ioctl that panicked the baseline returns atastat: ok ... progress=0.

VERDICT.md
↓ download raw

DF-2496 VERDICT β€” reproduced (all three divide sites panic the kernel)

Bottom line

CONFIRMED at every filed site. The nata RAID parsers copy geometry fields verbatim from on-disk metadata into the live struct ar_softc and the I/O / status / attach paths then use them as divisors with no zero check. On the audit guest (DragonFly 6.5-DEVELOPMENT x86_64, kernel #0 Thu Jul 2 2026), crafted Intel metadata (stripe_sectors=0, total_sectors=0) and a crafted JBOD geometry (heads=0) each produced a deterministic fatal trap 18 (integer divide fault), one per boot:

  1. strategy (sys/dev/disk/nata/ata-raid.c:359): production ata_raid_intel_read_meta() (rig test 20) installs type=AR_T_RAID0, status=AR_S_READY, interleave=0; the production ata_raid_strategy() then executes chunk = blkno % rdp->interleave β†’ Fatal trap 18 ... Stopped at ata_raid_strategy+0x2f3: divq %r12,%eax (panic_strategy.txt). The RAID5 variants (:366-372, /interleave, %(width-1), %width) are the same class and are covered by the same guard.
  2. status via REAL kernel ioctl (ata-raid.c:1028): the rig's production parse (test 21) plants total_sectors=0 into the REAL kernel ata_raid_arrays[0] (0xffffffff8114dec0), and a real userland ioctl(IOCATARAIDSTATUS) on /dev/ata (atastat) executes the REAL built-in kernel's ata_raid_status divide β†’ Fatal trap 18 ... Stopped at ata_raid_ioctl+0x11b: divq 0x48(%r14),%eax (panic_status_ioctl.txt; IP 0xffffffff803388eb is kernel-base built-in code, not the rig module). Full chain: userland syscall β†’ /dev/ata β†’ ioctl β†’ attacker-zeroed divisor β†’ fatal trap.
  3. attach (ata-raid.c:181): crafted heads=0, sectors=63 on an AR_T_JBOD array (which also skips the rounddown(:157) variant) β†’ production ata_raid_attach() divides by heads*sectors==0 β†’ Fatal trap 18 ... divq inlined into the caller (panic_attach.txt).

Setup half: rig test 12 (run_nonfatal.log) shows the production Intel parse accepting stripe_sectors=0 AND total_sectors=0 and installing a READY RAID0 array β€” no parser-side validation exists.

Threat model

Attacker data is on-disk RAID metadata read at subdisk attach (boot or disk insertion on an nata ATA channel) β€” no OS privileges needed; the panic then triggers on the first I/O / status query / attach. On the standard guest the authentic boot-attach arm is blocked by the QEMU SeaBIOS/IDE-disk limitation documented in DF-2494, so the production parser+divide code is driven through the DF-2494 KLD rig (production ata-raid.c compiled in verbatim; only ata_queue_request is interposed to serve the crafted sector). The status site was additionally proven through the real kernel's own code and a real userland ioctl.

Fix validation (single-fix kernel)

fix.diff adds: strategy RAID0/01 guard (!interleave || !width) and RAID5 guard (!interleave || width < 2) with biofinish(EIO); status->progress ternary on total_sectors; attach ternaries for the rounddown divisor and heads && sectors. Kernel rebuilt (fix_build.log), rig rebuilt against the fixed source. fix_validation.log: tests 20/21/22 all survive β€” ar0: degenerate RAID0/01 geometry (interleave=0 width=2) + rig_bio_done b_error=5 (EIO) for strategy; attach completes (d_ncylinders=0); the previously-fatal ioctl returns atastat: ok type=4 ... progress=0. Baseline panics: 3/3; patched: 0/3.

Kernel 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

Single-fix kernel (fix.diff applied to guest /usr/src, nativekernel + installkernel, rig rebuilt against the fixed source) eliminates all three panics: strategy returns EIO via the degenerate-geometry guard, attach completes (d_ncylinders=0), and the previously-fatal IOCATARAIDSTATUS returns 'ok ... progress=0'. Baseline 3/3 fatal, patched 0/3, guest stable.

fix_validation.log: '===== FIXED-KERNEL TEST 20 ===== ... SURVIVED', 'ar0: degenerate RAID0/01 geometry (interleave=0 width=2)', 'rig_bio_done #1 b_error=5', '===== FIXED-KERNEL TEST 21 + real ioctl ===== ... atastat: ok type=4 total_disks=2 interleave=64 status=1 progress=0', test22 'SURVIVED -- on a FIXED kernel this is the expected outcome'
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Wed Oct 14 20:11:51 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC

Confirmed kernel references

Detail

Exploit chain

crafted Intel/HPTv2 RAID metadata on a disk attached to an nata channel -> production parser copies geometry verbatim (interleave=0 / total_sectors=0 / heads=0, array AR_S_READY) -> first I/O through the ar device (strategy), any IOCATARAIDSTATUS on /dev/ata (root-only device), or array attach executes the unguarded divide -> Fatal trap 18, kernel dead at db> (local/attach-time DoS; status ioctl requires root, I/O path requires only a mounted array). No memory-corruption primitive: impact is denial of service.

Evidence (decisive lines)

panic_strategy.txt (Fatal trap 18, ata_raid_strategy+0x2f3 divq); panic_status_ioctl.txt (Fatal trap 18, ata_raid_ioctl+0x11b divq, real kernel IP, current process = atastat's syscall); panic_attach.txt (Fatal trap 18 divq, attach inlined); run_nonfatal.log test12 (interleave=0 and total_sectors=0 installed verbatim, rc=1); fix_validation.log (tests 20/21/22 all survive on the single-fix kernel: 'ar0: degenerate RAID0/01 geometry (interleave=0 width=2)', rig_bio_done b_error=5, attach completes, atastat returns 'ok ... progress=0')

PoC changes

No pass-1 pack existed (empty dir); rig authored fresh on the proven DF-2494 pattern: the UNMODIFIED production sys/dev/disk/nata/ata-raid.c is #included into a KLD (dfrig3_ata.ko) with only ata_queue_request interposed (serves the crafted metadata sector and logs strategy dispatches). Crafted Intel metadata requires a valid checksum over the post-half-swap buffer (computed in-kernel by the rig builder) and a serial-matching fake parent identify block so the parsed array is retained. Fixed two rig bugs found during runs: %p format on u_int64 (build error) and a NULL kfree in test 10 cleanup that panicked the FIRST fixed-kernel validation boot (rig bug, not a fix bug; re-validated cleanly).

Verified recommended fix

Validate geometry before use: guard strategy RAID0/01 (!interleave || !width) and RAID5 (!interleave || width < 2) with biofinish(EIO); make status->progress conditional on total_sectors != 0; guard the attach rounddown divisor and heads*sectors divisor (see fix.diff).

Verdict

CONFIRMED at all three filed divide sites, each driven to a deterministic Fatal trap 18 (integer divide fault) on the audit guest, one per boot. (1) strategy: production Intel parse (rig test 20) installs interleave=0 from crafted stripe_sectors; production ata_raid_strategy() executes blkno % rdp->interleave (ata-raid.c:359) -> 'Stopped at ata_raid_strategy+0x2f3: divq %r12,%eax'. (2) status via REAL kernel: rig parse plants total_sectors=0 into the REAL ata_raid_arrays[0] (0xffffffff8114dec0) and a real userland ioctl(IOCATARAIDSTATUS) on /dev/ata executes the REAL built-in kernel's status divide (ata-raid.c:1028) -> 'Stopped at ata_raid_ioctl+0x11b: divq 0x48(%r14),%eax' (IP 0xffffffff803388eb = kernel built-in, full syscall->ioctl->divide chain). (3) attach: crafted heads=0 JBOD geometry -> production ata_raid_attach() divides total_sectors/(heads*sectors)==0 (ata-raid.c:181) -> Fatal trap 18 divq (function inlined at -O2). Setup half (test 12): the production Intel parse accepts stripe_sectors=0 AND total_sectors=0 and installs a READY RAID0 array -- no parser-side validation exists. RAID5 variants (:366-372) are the same class and covered by the same fix guard.