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

Adaptec parser: unbounded 16-bit total_disks zeroing loop β€” massive deterministic kernel heap OOB write at first metadata parse

Summary

ata_raid_adaptec_read_meta copies be16(configs[0].total_disks) (0..65535) into raid->total_disks and then, because raid->generation is assigned from the same metadata BEFORE the test, executes the 'clear out any old info' loop on the FIRST parse of a fresh array slot: disks[disk].dev=NULL/.flags=0 for disk<total_disks - up to ~2.8MB of strided zeroing writes past the 16-entry disks[] of a kmalloc'd ar_softc. No checksum guards the Adaptec format (two raw magic compares only). REPRODUCED on the guest: crafted disk with total_disks=60000 panicked the guest at boot ('panic: vm_fault: fault on stack guard', frame ata_raid_subdisk_attach+0x42b) with zero interaction. FIX VALIDATED on rebuilt kernel: same disk prints 'Adaptec total_disks out of range', no array, boot reaches login. Anyone who can attach a disk (USB enclosure, VM/cloud image, hot-swap bay) with crafted Adaptec HostRAID metadata corrupts kernel heap at attach: boot-time persistent DoS (system cannot boot while the disk is present - demonstrated) up to adjacent-object pointer NULLing.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3690 Β· 12 files
FileTypeDescriptionSize
README.md β€” 2.8 KB ↓ raw
VERDICT.md β€” 3.9 KB ↓ raw
mkmeta.py β€” 4.0 KB view raw
build.sh β€” 133 B view raw
run.sh β€” 145 B view raw
run.log β€” 665 B view raw
panic.txt β€” 8.7 KB view raw
run.patched.log β€” 13.0 KB view raw
fix.diff β€” 3.8 KB view raw
env.txt β€” 505 B view raw
verdict.json β€” 3.1 KB view raw
manifest.json β€” 3.8 KB view raw

DF-3690 β€” Adaptec HostRAID parser: unbounded total_disks zeroing loop smashes the kernel heap at metadata-parse time

What this is

Evidence pack for the pass-2 finding in sys/dev/disk/nata/ata-raid.c (ata_raid_adaptec_read_meta).

Root cause (source)

ata_raid_adaptec_read_meta() takes configs[0].total_disks straight from on-disk metadata (u_int16_t, big endian β€” up to 65535) and stores it into raid->total_disks before the "clear out any old info" loop:

1704:  raid->total_disks = be16toh(meta->configs[0].total_disks);   /* attacker: 0..65535 */
...
1716:  if (raid->generation) {                     /* TRUE: generation was just set at :1703 */
1717:      for (disk = 0; disk < raid->total_disks; disk++) {
1718:          raid->disks[disk].dev = NULL;       /* disks[] has MAX_DISKS=16 entries */
1719:          raid->disks[disk].flags = 0;
1720:      }
1721:  }

struct ar_disk raid->disks[MAX_DISKS] is 16 entries inside a kmalloc(sizeof(struct ar_softc)) object. With total_disks = 60000 the loop writes NULL/0 every 48 bytes across a ~2.8 MB span of kernel heap starting inside the live ar_softc β€” purely attacker-chosen length, deterministic, at subdisk-attach time (boot or disk attach). No checksum guards the Adaptec format (only two magic constants), and raid->generation is set from the same metadata at :1703 before it is tested at :1716, so even the first parse of a fresh array slot takes the loop.

Reproduce

  1. python3 mkmeta.py (host) builds f1_adaptec.img: 262080-sector raw disk (multiple of 16*63) with a valid Adaptec header (magic_0/magic_3 as raw LE compares, generation=1 as be32) and configs[0].total_disks = 60000.
  2. Attach the image as ata0-master at QEMU launch (ARAID_DRIVE=... dfbsd-qemu/vm.sh up β€” see run.sh).
  3. Boot the DragonFly guest.

Expected output (baseline, unpatched kernel)

ata0: <ATA channel 0> on atapci0
panic: vm_fault: fault on stack guard, addr: 0xfffff8008df60000
...
ata_raid_subdisk_attach() at ata_raid_subdisk_attach+0x42b
Debugger("panic")

The zeroing loop ran ~2.8 MB past the ar_softc allocation and hit a kernel thread stack guard page β†’ fatal kernel-mode page fault while parsing the crafted disk. Captured in panic.txt.

With the fix applied (fix.diff: reject total_disks > MAX_DISKS) the same disk yields Adaptec total_disks out of range, no array, normal boot.

Files

  • mkmeta.py metadata image builder (host, python3)
  • f1_adaptec.img not stored (134 MB); regenerate with mkmeta.py
  • run.sh exact commands used
  • panic.txt serial console capture of the panic (baseline)
  • run.patched.log re-run on the fixed kernel
  • fix.diff combined pass-2 fix (this finding = hunk total_disks out of range)
  • verdict.json machine verdict
VERDICT.md
↓ download raw

DF-3690 VERDICT

Status: reproduced (panic; deterministic kernel heap corruption from one crafted disk; no user interaction beyond presenting the disk).

How it was reproduced

  1. Built a 128 MB raw disk image whose last-17-sector Adaptec metadata block (ADP_LBA = total_secs - 17 = 262063) is an otherwise-valid Adaptec HostRAID header (magic_0/magic_3 raw compares pass, generation = 1 big-endian, configs[0].type = ADP_T_RAID0) with configs[0].total_disks = be16(60000).
  2. Attached it as ata0-master (QEMU ide.0 unit 0, bootindex deferred to the virtio system disk) and booted the unmodified guest kernel (DragonFly 6.5-DEVELOPMENT, X86_64_GENERIC, ataraid compiled in).
  3. The kernel parsed the metadata during ata_raid_subdisk_attach and panicked immediately:
panic: vm_fault: fault on stack guard, addr: 0xfffff8008df60000
vm_fault() at vm_fault+0x12eb
trap_pfault() at trap_pfault+0x9a
trap() at trap+0x17c
calltrap() at calltrap+0x9
--- trap 000000000000000c, rip = ffffffff8033330b ---
ata_raid_subdisk_attach() at ata_raid_subdisk_attach+0x42b
device_doattach() at device_doattach+0x3af
Debugger("panic")

Why it happens (path:line)

  • sys/dev/disk/nata/ata-raid.c:1704 β€” raid->total_disks = be16toh(meta->configs[0].total_disks); u_int16_t, no bound vs MAX_DISKS=16.
  • sys/dev/disk/nata/ata-raid.c:1703 sets raid->generation from metadata before sys/dev/disk/nata/ata-raid.c:1716 tests it, so the "clear out any old info" loop at :1717-1720 executes on the very first parse of a fresh array slot (the historical "second disk / higher generation" framing is not even needed).
  • The loop body writes raid->disks[disk].dev = NULL; .flags = 0 for disk < 60000 β€” i.e. a NULL/0 write every sizeof(struct ar_disk) (48) bytes across ~2.88 MB starting inside the kmalloc'd ar_softc, through adjacent kernel heap, until it faults (here: a kernel stack guard page).

Primitive characterization

Attacker-controlled length (2..65535 entries) of a strided zeroing write (8-byte NULL pointer at disks+n*48, 4-byte zero at disks+n*48+40) starting at a known offset inside a fresh kernel heap allocation. Deterministic, at attach. In this run it manifested as a guard-page fault panic; with heap layout control (e.g. arranging for the swept region to contain allocator metadata or adjacent objects' pointers) this is a heap-corruption primitive that zeroes security-relevant pointers β€” the upper bound on impact is arbitrary-object-pointer NULLing, the demonstrated impact is a reliable persistent DoS (system cannot finish booting / disk cannot attach while the image is present). Not pushed to uid0 within this run (see notes).

Fix validation

fix.diff adds (hunk at the Adaptec parser):

if (be16toh(meta->configs[0].total_disks) > MAX_DISKS) {
    device_printf(parent, "Adaptec total_disks out of range\n");
    goto adaptec_out;
}

Baseline (unpatched): panic at boot, guest wedged in DDB. Patched (fix + nativekernel rebuild): Adaptec total_disks out of range, no array created, guest boots to login. See run.patched.log. fix_status = fixed.

Not covered / notes

  • The known pass-1 findings (DF-1173/DF-2493: disk_number/total_disks OOB in the other parsers) did not list Adaptec; its disk_number is computed from channel/unit (bounded), but its total_disks path is a distinct, 16-bit-wide sink and the loop runs on the FIRST parse.
  • ata_raid_adaptec_print_meta also loops to be16toh(total_configs) (u16) over configs[127] (bootverbose only) β€” noted, not fixed here.

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 (nativekernel rebuild with fix.diff): same crafted disk prints 'Adaptec total_disks out of range', creates no array, ad0 attaches as a plain disk, boot reaches login. Baseline panic gone.

findings/poc/DF-3690/run.patched.log lines: 'ad0: Adaptec total_disks out of range', 'ad0: 127MB <QEMU HARDDISK 2.5+>', normal boot; compare panic.txt
↓ fix.diffper-fix-DF-3690

Confirmed kernel references

Detail

Exploit chain

crafted disk image with Adaptec metadata (total_disks=60000, gen=1) -> attach at boot -> parse loop writes disks[n].dev=NULL/.flags=0 for n<60000 -> heap + stack guard page smashed -> fatal kernel page fault (DDB wedge). Escalation beyond DoS not pursued: the write content is fixed (NULL/0), value control is absent; realistic ceiling is adjacent-object pointer NULLing (corruption), demonstrated ceiling is boot-time panic.

Evidence (decisive lines)

panic.txt (fatal trap + ata_raid_subdisk_attach frame), run.log, VERDICT.md 'Why it happens', run.patched.log (fix rejects with 'Adaptec total_disks out of range', boots to login)

PoC changes

Seed-free pass-2 finding. First image attempt used big-endian magics and two wrong struct offsets (adaptec magics are raw native compares, unlike generation); guest-compiled layout2.c against the real header fixed the offsets and the image then parsed and panicked as predicted.

Verified recommended fix

reject be16toh(configs[0].total_disks) > MAX_DISKS in ata_raid_adaptec_read_meta before any use (fix.diff hunk 6)

Verdict

Adaptec HostRAID parser copies an untrusted 16-bit total_disks into raid->total_disks and, because raid->generation is assigned from the same metadata before it is tested, executes the disks[]-clearing loop on the FIRST parse: with total_disks=60000 the kernel wrote NULL/0 across ~2.8MB of heap during ata_raid_subdisk_attach and fatally hit a kernel stack guard page (panic captured on the guest, boot from a single crafted disk, zero interaction). Primitive = attacker-chosen-length strided zeroing write starting inside a live ar_softc; demonstrated as reliable persistent DoS / heap corruption.