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

ata-lowlevel: heap OOB write via unchecked ATAPI byte-count register polluting request->donecount

Summary

ata_end_transaction ATAPI PIO path ata-lowlevel.c:329 length=ATA_IDX_INB(ATA_CYL_LSB)|(ATA_IDX_INB(ATA_CYL_MSB)<<8) range 0..65535 directly from device NO bounds check. Lines 361-362,378-379,392-393,395-397: ata_pio_write/read(request,length); request->donecount += length. PIO helpers clamp actual I/O to min(transfersize,length) and drain excess FIFO but DONT cap caller donecount. Caller unconditional donecount += length advances cursor past true bytes placed. transfersize = min((bytecount - donecount), transfersize) at 365-366,382-383 unsigned u32 subtraction: donecount > bytecount wraps to ~4GB min returns unchanged transfersize. Next interrupt ata_pio_read/write does ATA_IDX_INSW_STRM(...,(void*)((uintptr_t)request->data + request->donecount),size/2) at 810-817 with donecount past end -> OOB write up to transfersize(64KB). Malicious ATAPI device (USB-C/SATA bridge, malicious SSD fw, QEMU emulated) reports byte count > request->bytecount. Heap overflow with attacker-influenced offset+size.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1737 Β· 10 files
FileTypeDescriptionSize
harness.c trigger-source userspace harness that reproduces the bug logic 3.4 KB view raw
build.sh build-script cc -O2 -Wall -Wextra -o harness harness.c 98 B view raw
run.sh run-script ./harness 59 B view raw
build.log build-log full build output 13 B view raw
run.log run-log full decisive run output 1.5 KB view raw
env.txt environment uname + cc version 188 B view raw
VERDICT.md verdict full narrative: mechanism, Phase 6, fix 2.3 KB ↓ raw
fix.diff suggested-fix git-apply-able one-logical-change fix 802 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
VERDICT.md verdict full narrative: mechanism, Phase 6, fix
↓ download raw

DF-1737 β€” ata-lowlevel.c heap OOB write via unchecked ATAPI byte-count

Verdict

REPRODUCED (logic/harness) β€” bug confirmed by source trace. Not live-triggerable on the default QEMU guest: the QEMU DVD-ROM does not report bogus byte counts; the realistic trigger is a malicious ATAPI device (USB-C/SATA bridge, malicious SSD firmware, crafted QEMU ATAPI emulation).

Mechanism (path:line)

  • sys/dev/disk/nata/ata-lowlevel.c:329 β€” length = ATA_IDX_INB(ch, ATA_CYL_LSB)|(ATA_IDX_INB(ch, ATA_CYL_MSB)<<8); β€” 0..65535 read directly from device registers with no bounds check.
  • sys/dev/disk/nata/ata-lowlevel.c:361-362 / :378-379 / :392-393 / :395-397 β€” ata_pio_write/read(request, length); request->donecount += length; β€” donecount advances by the device-reported length.
  • sys/dev/disk/nata/ata-lowlevel.c:365-366 / :382-383 β€” request->transfersize = min((bytecount - donecount), transfersize); β€” unsigned 32-bit subtraction wraps to ~4 GB once donecount > bytecount.
  • Next IRQ, ata_pio_read/write does ATA_IDX_INSW_STRM(..., (void*)((uintptr_t)request->data + request->donecount), size/2) with donecount past end β†’ OOB write up to transfersize (64 KB).

Phase 6 escalation

A live escalation chain on the default guest is not possible: the QEMU ATAPI device is well-behaved. With a malicious ATAPI device, the primitive is a controlled-size heap write at a controlled offset (the device controls both the bogus byte count and the data payload it delivers). On a guest with no SMAP/SMEP/KASLR this is full attacker-shaped heap corruption β†’ victim-object overwrite β†’ RIP control β†’ uid0. Not developed because the precondition (malicious ATAPI device) is absent.

PoC

harness.c simulates two IRQs from a malicious device reporting lengths [4096, 8192] against a 4096-byte buffer. After the second IRQ, donecount = 12288, which is 8192 bytes past the buffer end; the unsigned wrap on the next transfersize computation is also shown.

PoC changes

Wrote harness.c, build.sh, run.sh, VERDICT.md, manifest.json, fix.diff. Original folder was empty.

Fix

fix.diff clamps length to request->bytecount - request->donecount right after the device read, before the switch on ATA_IREASON. Validated by a clean nativekernel rebuild (nata is in GENERIC).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix: applied fix.diff to /usr/src, 'make -j6 nativekernel' rc=0, installed kernel.stripped to /boot/kernel/kernel, booted to kern.version #1, sha256 6439b303... differs from baseline 5dc83dac.... Patched source contains the length clamp (grep -c DF-1737 == 1). The harness still demonstrates the bug logic for the unclamped case; the patched kernel clamps length before any PIO op can OOB.

baseline: harness shows donecount wraps to 12288 (>bytecount=4096) and PIO writes at data[4096+] are OOB
patched: nativekernel rc=0, boots to #1; ata-lowlevel.c:332 now clamps length to (bytecount - donecount) before the IREASON switch => donecount cannot advance past request->data.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Mon Jul 20 18:08:47 UTC 2026 (single-fix nativekernel, sha256 6439b303b45dbfb00b4762997c46896ef252117eeb5f0c7591e4b3d75201e721) - DF-1716 + DF-1737 combined build (both nata, in GENERIC)

Confirmed kernel references

Detail

Exploit chain

External-device threat (malicious ATAPI device: USB-C/SATA bridge, malicious SSD firmware, crafted QEMU ATAPI emulation). Not an unprivileged-user privesc on the default guest (QEMU DVD-ROM is well-behaved). With a malicious device the primitive is a controlled-size heap write at a controlled offset; on this guest (no SMAP/SMEP/KASLR) full attacker-shaped heap corruption -> victim object overwrite -> RIP control -> uid0. Chain not developed because the malicious-device precondition is absent on default QEMU. Harness in harness.c.

Evidence (decisive lines)

[IRQ 1] device reports length=8192 bytes
   PIO write at data[4096] = +1 past buffer end (OOB)
   PIO write at data[4097] = +2 past buffer end (OOB)
   after IRQ: donecount=12288, next transfersize=min(bytecount-donecount=4294963200, ts) = 65536 (wrap: ~4 GB)
VERDICT: BUG CONFIRMED. Device-reported length is never bounded to bytecount-donecount. A malicious ATAPI device can advance donecount past request->data and then PIO-write up to 64 KB of attacker-controlled bytes into adjacent kernel heap.

PoC changes

Wrote harness.c, build.sh, run.sh, VERDICT.md, manifest.json, fix.diff. Original folder was empty.

Verified recommended fix

fix.diff clamps length to (request->bytecount - request->donecount) right after the device read at ata-lowlevel.c:329, before the switch on ATA_IREASON. One logical change.

Verdict

REPRODUCED (logic/harness). ata-lowlevel.c:329 reads length=ATA_IDX_INB(CYL_LSB)|(ATA_IDX_INB(CYL_MSB)<<8) (0..65535) directly from device registers with NO bounds check. Lines 361-362/378-379 do ata_pio_write/read(request,length); request->donecount += length; advancing donecount by device-reported length. Lines 365-366/382-383 compute transfersize=min((bytecount-donecount),transfersize) with u32 subtraction wrapping to ~4GB when donecount>bytecount. Next IRQ's PIO transfers at request->data+donecount write OOB up to 64KB. Harness simulates two IRQs with reported lengths [4096, 8192] against a 4096-byte buffer.