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)
PoC verification
Evidence pack
findings/poc/DF-2496 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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:
- strategy (
sys/dev/disk/nata/ata-raid.c:359): productionata_raid_intel_read_meta()(rig test 20) installstype=AR_T_RAID0, status=AR_S_READY, interleave=0; the productionata_raid_strategy()then executeschunk = 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. - status via REAL kernel ioctl (
ata-raid.c:1028): the rig's production parse (test 21) plantstotal_sectors=0into the REAL kernelata_raid_arrays[0](0xffffffff8114dec0), and a real userlandioctl(IOCATARAIDSTATUS)on/dev/ata(atastat) executes the REAL built-in kernel'sata_raid_statusdivide β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. - attach (
ata-raid.c:181): craftedheads=0, sectors=63on anAR_T_JBODarray (which also skips therounddown(:157)variant) β productionata_raid_attach()divides byheads*sectors==0βFatal trap 18 ... divqinlined 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
sys/dev/disk/nata/ata-raid.c:359-361β RAID0/01%interleave,/interleave,/widthsys/dev/disk/nata/ata-raid.c:366-373β RAID5/interleave,%(width-1),%widthsys/dev/disk/nata/ata-raid.c:1028β100 * rebuild_lba / total_sectorssys/dev/disk/nata/ata-raid.c:157-158βrounddown(total_sectors, interleave*width)sys/dev/disk/nata/ata-raid.c:181βtotal_sectors/(heads*sectors)sys/dev/disk/nata/ata-raid.c:2229,2231β Intel parse copies stripe_sectors/total_sectors verbatim
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
fixedSingle-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'
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.
No comments yet.