Intel MatrixRAID parser: OOB heap write on ar_softc via unclamped map->total_disks
Summary
ata_raid_intel_read_meta at ata-raid.c:2230: raid->total_disks=map->total_disks (u8 from disk metadata, up to 255) stored without bounds check. Loop :2243-2252 writes raid->disks[disk].* for disk<raid->total_disks into disks[MAX_DISKS=16]. total_disks>16 overflows disks[] into ar_softc lock/disk/devstat/cdev/pid fields and adjacent kernel heap. Attacker controls 32 of every 48 overflowed bytes (serial+sectors from metadata). Checksum bypassable (attacker controls all u32 words). Attacker: crafted SATA disk on Intel AHCI, no privilege required, automatic at attach. Reliable kernel arbitrary write -> code execution or panic.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1171 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| overflow_harness.c | trigger-source | userspace replica of ata_raid_intel_read_meta overflow math using real ar_softc/ar_disk/intel_raid_mapping struct layouts | 6.8 KB | view raw |
| build_meta.py | trigger-source | builds a 2MB raw disk image with a valid Intel MatrixRAID metadata block (magic+checksum) whose map->total_disks is attacker-set (default 32) | 4.7 KB | view raw |
| crafted_raid.img | trigger-source | built crafted disk image (total_disks=32) used in the live-trigger attempts | 2.0 MB | β download |
| build.sh | build-script | builds the harness and/or the crafted disk image | 691 B | view raw |
| run.sh | run-script | runs the overflow-math harness | 319 B | view raw |
| harness_output.txt | run-log | captured harness stdout: the OOB byte count (11472) and victim-field offsets | 771 B | view raw |
| env.txt | environment | guest uname, kern.version, PCI ATA controller (Intel PIIX4 0x70108086), nataraid in GENERIC, no ad devices | 471 B | view raw |
| fix.diff | suggested-fix | git-apply-able: clamp raid->total_disks to MAX_DISKS at ata-raid.c:2230 with device_printf warning | 633 B | view raw |
| fix_build.log | build-log | full single-fix kernel build log (make -j6 nativekernel, rc=0) | 5.6 MB | β download |
| VERDICT.md | verdict | full narrative: mechanism, primitive characterization, reachability, live-trigger attempt, uid0 assessment, fix validation | 11.6 KB | β raw |
| README.md | readme | how to reproduce + threat model | 2.8 KB | β 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 |
DF-1171 β PoC evidence pack
Intel MatrixRAID parser OOB heap write in ata_raid_intel_read_meta()
(sys/dev/disk/nata/ata-raid.c): raid->total_disks = map->total_disks (u8,
β€255) is stored at ata-raid.c:2230 without bounds check, then the loop at
:2243-2263 writes raid->disks[disk].* for disk < raid->total_disks,
overflowing disks[MAX_DISKS=16] into the trailing ar_softc fields
(lock/disk/devstat/cdev/pid) and adjacent kernel heap.
How to reproduce
1. Overflow-math harness (primary proof)
A userspace replica built from the real kernel struct definitions. It proves the exact overflow size and victim offsets without needing SATA hardware.
./build.sh harness # cc -Wall -O2 -o overflow_harness overflow_harness.c
./run.sh harness # ./overflow_harness (prints OOB byte count + victims)
Expected (bug present): for map->total_disks = 255, prints
RESULT: OOB heap write CONFIRMED β 11472 bytes past disks[15].
2. Crafted-disk image (live-trigger attempt)
./build.sh meta # python3 build_meta.py crafted_raid.img <total_disks>
Produces a 2 MB raw image with a valid Intel MatrixRAID metadata block (correct
magic + verified checksum) whose map->total_disks is attacker-set (default
32). On a DragonFlyBSD host with an Intel ATA/AHCI controller and an attached ATA
disk, attaching this image as that disk fires ata_raid_intel_read_meta at probe
time and triggers the overflow β kernel panic on default GENERIC (INVARIANTS).
Note on this guest: the live in-kernel trigger could NOT be exercised here. The DragonFly loader/SeaBIOS hangs on any extra hard disk (a blank 64 MB IDE disk reproduced the identical hang) before the kernel boots, so the
nataprobe path never runs on the crafted disk. SeeVERDICT.mdfor details. The bug itself is unambiguously real (source trace + harness).
Files
| file | purpose |
|---|---|
overflow_harness.c |
harness: real struct layouts, exact overflow math |
build_meta.py |
builds the crafted Intel-RAID disk image |
fix.diff |
git-apply-able clamp of total_disks to MAX_DISKS |
VERDICT.md |
full analysis: mechanism, primitive, reachability, fix |
harness_output.txt |
captured harness stdout (the OOB proof) |
fix_build.log |
full single-fix kernel build log (rc=0) |
env.txt |
guest environment |
manifest.json |
artifact catalog |
Threat model / impact
Trigger: a malicious SATA/IDE disk (USB-SATA, hot-plug, crafted VM image)
presented to a DragonFlyBSD host on an Intel ATA/AHCI controller. Fires at attach
with no user privilege. Realistic ceiling: reliable kernel panic / local DoS
on default GENERIC. uid0 is not realistic β the overflow is blind (attacker
controls content, not slab layout) and fires synchronously at device-attach with
no userspace heap-grooming window. See VERDICT.md.
DF-1171 β Intel MatrixRAID parser OOB heap write on ar_softc via unclamped map->total_disks
Verdict
REPRODUCED (primitive confirmed at source + harness level). The bug is a
genuine unbounded heap overflow in ata_raid_intel_read_meta() β confirmed by
line-by-line source trace and a faithful userspace harness built from the real
kernel struct definitions (struct ar_softc, struct ar_disk,
struct intel_raid_mapping). The driver is compiled into the default
X86_64_GENERIC kernel and the Intel-vendor gating path is present on this
guest's PIIX4 IDE controller, but the live in-kernel trigger is blocked by a
QEMU/loader artifact (the DragonFly loader hangs on any extra hard disk before
the kernel boots β see "Live-trigger attempt" below). The demonstrated primitive
ceiling is memory corruption / kernel panic (DoS) at device-attach time;
uid0 escalation is not realistic from this primitive (see "Exploit chain").
Mechanism (trigger β primitive β effect)
ata_raid_intel_read_meta() parses on-disk Intel MatrixRAID metadata read from
the last sectors of an ATA disk (INTEL_LBA = total_secs - 3,
sys/dev/disk/nata/ata-raid.h:293). After validating the magic string and
checksum, it stores the attacker-supplied disk count without bounds checking:
ata-raid.c:2230 raid->total_disks = map->total_disks; // map->total_disks is u_int8_t -> up to 255
map->total_disks is a u_int8_t (sys/dev/disk/nata/ata-raid.h:351), so it
can be up to 255. raid->disks[] is declared as disks[MAX_DISKS] with
MAX_DISKS = 16 (sys/dev/disk/nata/ata-raid.h:39,108). The subsequent loop
writes raid->disks[disk].* for disk < raid->total_disks:
ata-raid.c:2243-2263
for (disk = 0; disk < raid->total_disks; disk++) {
...
raid->disks[disk].dev = NULL; // :2246
bcopy(meta->disk[disk_idx].serial,
raid->disks[disk].serial, ...); // :2247-2249 (16 attacker bytes)
raid->disks[disk].sectors = meta->disk[disk_idx].sectors; // :2250-2251 (attacker u32)
raid->disks[disk].flags = ...; // :2252-2262
}
For any map->total_disks > 16, index disk runs past disks[15] and writes
into the trailing fields of struct ar_softc β toggle, rebuild_lba, lock,
disk, devstat, cdev, pid (ata-raid.h:109-115) β and then past the end
of the kmalloc(sizeof(struct ar_softc)) allocation (ata-raid.c:2171) into
adjacent kernel heap.
Primitive characterization (harness, real struct layouts)
overflow_harness.c reproduces the exact in-kernel data flow using faithful
copies of the kernel structs. On LP64:
| quantity | value |
|---|---|
sizeof(struct ar_disk) |
48 (8 dev + 16 serial + 8 sectors + 8 last_lba + 4 flags + 4 pad) |
sizeof(struct ar_softc) |
1328 |
disks[0] offset |
112 |
disks[16] (first OOB) offset |
880 == ar_softc.toggle |
| attacker-controlled bytes per OOB entry | 24 of 48 (serial[16] + sectors[8]) |
For the maximal attacker value map->total_disks = 255:
OOB entries written past disks[15]: 239 OOB bytes written past disks[15]: 11472 ( = 239 * 48 ) of which INSIDE ar_softc struct : 448 bytes (toggle/rebuild_lba/lock/disk/devstat/cdev/pid) of which PAST the struct boundary: 11024 bytes (into adjacent kernel heap)
The finding's "~11,424 bytes" estimate is correct to within the per-entry
rounding (exact is 11,472). The "32 of 48 attacker-controlled bytes" claim is
essentially correct (the loop also NULLs dev and zeroes last_lba/flags,
which the attacker does not shape β so attacker-shaped content is 24 bytes, with
the remaining writes being fixed zero/NULL).
Reachability & threat model
- Driver present in default GENERIC:
device nataraidis insys/config/X86_64_GENERIC:83;ata-raid.cis built whennataraidis set (sys/conf/files:143). Confirmed live:nm /boot/kernel/kernel.debugshowsata_raid_subdisk_attachandata_raid_rwin the running#0kernel. - Intel-vendor gating passes on this guest: the guest exposes an Intel PIIX4
IDE controller (
atapci0@pci0:0:1:1, chip0x70108086, vendor0x8086==ATA_INTEL_ID,sys/dev/disk/nata/ata-pci.h:146). Both gating checks inata_raid_read_metadatapass for anaddevice here:device_get_devclass(GRANDPARENT(GRANDPARENT(subdisk))) == atapci_devclass(ata-raid.c:1411,1414) andpci_get_vendor(GRANDPARENT(device_get_parent(subdisk))) == ATA_INTEL_ID(ata-raid.c:1415,1423). So a crafted ATA disk presented on this controller would dispatch intoata_raid_intel_read_meta. - Trigger context:
ata_raid_subdisk_attachβata_raid_read_metadatais called fromad_attachwhen an ATA disk is attached (ata-disk.c:151-152βata-raid.c:4161). This is a device-probe/attach-time path: it fires at boot or on hot-plug of an ATA/SATA disk, NOT in response to any userspace syscall. - Realistic threat model: a malicious SATA/IDE disk (USB-attached SATA, hot-plug, or a crafted VM disk image) presented to a DragonFlyBSD host on an Intel ATA/AHCI controller triggers the overflow automatically at attach with no user privilege required. This is a valid, default-config attack surface.
Live-trigger attempt (why no in-kernel panic is shown)
A crafted raw disk image was built (build_meta.py) carrying a valid Intel
MatrixRAID metadata block (correct magic "Intel Raid ISM Cfg Sig. ", verified
checksum, map->total_disks = 32 β 16 OOB entries) placed at INTEL_LBA. It
was attached to the running guest three ways:
-drive file=crafted.img,if=ideon the existing PIIX4 controller- same, with a valid empty MBR signature to avoid wrong-device boot
-device ich9-ahci,id=ahci0 -device ide-hd,bus=ahci0.0(Intel ICH9 AHCI, the finding's named vector)
In all three cases the guest produced zero bytes of serial output for
80β90 s β a hang in SeaBIOS/the DragonFly loader before the kernel's console
is initialized. A control experiment with a blank all-zero 64 MB IDE disk
(no metadata at all) reproduced the identical hang, proving the hang is a
QEMU/loader artifact triggered by adding any hard disk to this guest, not
by the crafted metadata. Because the loader never hands control to the kernel,
the nata probe path that contains the bug never executes on the crafted disk.
This is a genuine environment blocker for the live in-kernel demonstration:
the bug is unambiguously real (source + harness), the driver and Intel-vendor
path are present in the running default kernel, but the live trigger cannot be
exercised on this guest because the loader stalls on extra hard disks. On real
Intel AHCI hardware (or a QEMU config whose loader tolerates the extra disk) the
overflow would fire and, on default GENERIC with INVARIANTS, panic when the
corrupted lock/disk/devstat/cdev/pid are subsequently used by
ata_raid_attach.
Exploit chain (uid0 assessment)
uid0 escalation is NOT realistic from this primitive. This is a valid hard stop, for a structural reason, not a bail-out:
- The overflow fires synchronously inside
ata_raid_subdisk_attachduring device probe β i.e. at boot or hot-plug time. No userspace process is concurrently running in a position to groom the slab so that a chosen victim object (struct file,struct ucred, an ops vector, β¦) lands adjacent to the freshlykmalloc(sizeof(struct ar_softc))allocation atata-raid.c:2171. - The attacker's only input is the on-disk metadata content (the
serial[16] + sectors[8]bytes per overflow entry). The attacker has no influence over the slab layout at attach time. The overflow is therefore blind: attacker bytes are written into whatever the kernel happened to place adjacent to thear_softc. - The corruption first lands inside
ar_softcitself (toggle,rebuild_lba,lock,disk,devstat,cdev,pid). On default GENERIC (INVARIANTS ON) the subsequentata_raid_attachuse of the corruptedlock/disk/devstatreliably panics before any privilege-meaningful operation β and there is no userspace grooming window to convert the blind spill into a controlled overwrite of a credential/ops-vector target.
This matches the "primitive fires at device-attach, attacker controls content but not layout, no grooming window" case: the demonstrated and realistic ceiling is reliable kernel panic / local DoS from a malicious disk, not uid0. Escalating this to uid0 would require a separate heap-observation/leak primitive giving the attacker slab-layout control at attach time, which this bug does not provide.
PoC changes
Authored from scratch (the evidence-pack dir was empty). Deliverables:
overflow_harness.cβ userspace replica of the in-kernel overflow math using the realstruct ar_softc/struct ar_disk/struct intel_raid_mappinglayouts; prints the exact byte offsets and overflow sizes.build_meta.pyβ builds a crafted raw disk image with a valid Intel MatrixRAID metadata block (magic + verified checksum) whosemap->total_disksis attacker-set, for the live-trigger attempts.
Recommended fix
fix.diff clamps raid->total_disks to MAX_DISKS immediately after it is
read from map->total_disks at ata-raid.c:2230, with a device_printf for
visibility. This matches the finding markdown's proposed fix ("clamp total_disks
to MAX_DISKS"). The clamp is chosen over rejection-with-free because
raidp[array] at that point may be a pre-existing shared INTEL array reused for
the same config_id (see ata-raid.c:2178-2180); freeing it would risk a
use-after-free against the existing kfree(raidp[array]) pattern. Clamping is
provably safe and directly prevents the disks[] overflow (the loop bound and
all later raid->total_disks uses see the clamped value).
Fix validation (Phase 8)
fix.diffapplies cleanly (git apply --checkOK;patch -p1in-guest OK).- Single-fix kernel built from
with-srcwarm obj:make -j6 nativekernel KERNCONF=X86_64_GENERICβ rc=0 (fix_build.log, 35304 lines). - Patched kernel installed over
/boot/kernel/kernel(bare loader name) +kernel.debug; booted clean:kern.version=DragonFly 6.5-DEVELOPMENT #1: Thu Jul 16 03:20:54 UTC 2026(was#0); guest up and answering ssh. - The fix string
"Intel RAID total_disks %u exceeds MAX_DISKS %d, clamping"is present in the booted/boot/kernel/kernelβ the clamp is compiled in. fix_status: not_testablefor runtime behavior: the live trigger cannot run on this guest (loader hangs on any extra hard disk β see above), so a before/after panic comparison is not possible here. The fix is validated at the apply + compile + boot + source-trace level: with the clamp,raid->total_disksis capped at 16, so the loop atata-raid.c:2243iteratesdisk < 16only and never indexes pastdisks[15]β the OOB write is eliminated. On real Intel AHCI hardware the patched kernel would no longer panic on the crafted disk.
Kernel references (confirmed during verification)
sys/dev/disk/nata/ata-raid.c:2230β unclamped store ofmap->total_diskssys/dev/disk/nata/ata-raid.c:2243-2263β OOB write loop intodisks[disk]sys/dev/disk/nata/ata-raid.h:39βMAX_DISKS 16sys/dev/disk/nata/ata-raid.h:108βdisks[MAX_DISKS]sys/dev/disk/nata/ata-raid.h:351βmap->total_disksisu_int8_t(β€255)sys/dev/disk/nata/ata-raid.c:2171βkmalloc(sizeof(struct ar_softc))victimsys/config/X86_64_GENERIC:83βdevice nataraid(default GENERIC)sys/dev/disk/nata/ata-pci.h:146βATA_INTEL_ID 0x8086sys/dev/disk/nata/ata-raid.c:1411-1426β gating checks (both pass on PIIX4)
Fix verification
not_testablenot_testable (loader hangs on extra disk). Compile+boot validated: rc=0, #1 boots, clamp string in binary. Loop capped at disk<16.
BEFORE: 11,472B OOB. AFTER: clamp to MAX_DISKS=16, 0 OOB. Build rc=0.
Confirmed kernel references
Detail
Exploit chain
none -- fires synchronously at device-attach (probe), no userspace heap-grooming window. Attacker controls content (serial+sectors) but NOT slab layout. Blind spill -> reliable panic/DoS. uid0 not realistic.
Evidence (decisive lines)
Harness: total_disks=255 -> 239 OOB entries, 11,472B past disks[15], 448B clobber ar_softc (toggle/lock/disk/devstat/cdev/pid). nm: ata_raid_subdisk_attach in kernel.debug.
PoC changes
Authored: overflow_harness.c (real struct layouts), build_meta.py + crafted_raid.img (crafted disk), fix.diff (clamp total_disks to MAX_DISKS), VERDICT.md, manifest.json.
Verified recommended fix
Clamp raid->total_disks to MAX_DISKS at ata-raid.c:2230 after reading map->total_disks, with device_printf warning. Matches finding proposal. Full diff in findings/poc/DF-1171/fix.diff.
Verdict
REPRODUCED (harness). ata_raid_intel_read_meta ata-raid.c:2230 stores map->total_disks (u8, max 255) unclamped. Loop :2243 writes disks[disk] for disk
No comments yet.