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)
PoC verification
Evidence pack
findings/poc/DF-3690 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
python3 mkmeta.py(host) buildsf1_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) andconfigs[0].total_disks = 60000.- Attach the image as
ata0-masterat QEMU launch (ARAID_DRIVE=... dfbsd-qemu/vm.sh upβ see run.sh). - 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.pymetadata image builder (host, python3)f1_adaptec.imgnot stored (134 MB); regenerate with mkmeta.pyrun.shexact commands usedpanic.txtserial console capture of the panic (baseline)run.patched.logre-run on the fixed kernelfix.diffcombined pass-2 fix (this finding = hunktotal_disks out of range)verdict.jsonmachine verdict
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
- 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_3raw compares pass,generation= 1 big-endian,configs[0].type = ADP_T_RAID0) withconfigs[0].total_disks = be16(60000). - Attached it as
ata0-master(QEMUide.0unit 0, bootindex deferred to the virtio system disk) and booted the unmodified guest kernel (DragonFly 6.5-DEVELOPMENT, X86_64_GENERIC, ataraid compiled in). - The kernel parsed the metadata during
ata_raid_subdisk_attachand 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:1703setsraid->generationfrom metadata beforesys/dev/disk/nata/ata-raid.c:1716tests it, so the "clear out any old info" loop at:1717-1720executes 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 = 0fordisk < 60000β i.e. a NULL/0 write everysizeof(struct ar_disk)(48) bytes across ~2.88 MB starting inside thekmalloc'dar_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_numberis computed from channel/unit (bounded), but itstotal_diskspath is a distinct, 16-bit-wide sink and the loop runs on the FIRST parse. ata_raid_adaptec_print_metaalso loops tobe16toh(total_configs)(u16) overconfigs[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
fixedPatched 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
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.
No comments yet.