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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1171 Β· 13 files
FileTypeDescriptionSize
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
README.md readme how to reproduce + threat model
↓ download 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 nata probe path never runs on the crafted disk. See VERDICT.md for 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.

VERDICT.md verdict full narrative: mechanism, primitive characterization, reachability, live-trigger attempt, uid0 assessment, fix validation
↓ download raw

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 nataraid is in sys/config/X86_64_GENERIC:83; ata-raid.c is built when nataraid is set (sys/conf/files:143). Confirmed live: nm /boot/kernel/kernel.debug shows ata_raid_subdisk_attach and ata_raid_rw in the running #0 kernel.
  • Intel-vendor gating passes on this guest: the guest exposes an Intel PIIX4 IDE controller (atapci0@pci0:0:1:1, chip 0x70108086, vendor 0x8086 == ATA_INTEL_ID, sys/dev/disk/nata/ata-pci.h:146). Both gating checks in ata_raid_read_metadata pass for an ad device here: device_get_devclass(GRANDPARENT(GRANDPARENT(subdisk))) == atapci_devclass (ata-raid.c:1411,1414) and pci_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 into ata_raid_intel_read_meta.
  • Trigger context: ata_raid_subdisk_attach β†’ ata_raid_read_metadata is called from ad_attach when 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:

  1. -drive file=crafted.img,if=ide on the existing PIIX4 controller
  2. same, with a valid empty MBR signature to avoid wrong-device boot
  3. -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_attach during 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 freshly kmalloc(sizeof(struct ar_softc)) allocation at ata-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 the ar_softc.
  • The corruption first lands inside ar_softc itself (toggle, rebuild_lba, lock, disk, devstat, cdev, pid). On default GENERIC (INVARIANTS ON) the subsequent ata_raid_attach use of the corrupted lock/disk/devstat reliably 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 real struct ar_softc / struct ar_disk / struct intel_raid_mapping layouts; 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) whose map->total_disks is attacker-set, for the live-trigger attempts.

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.diff applies cleanly (git apply --check OK; patch -p1 in-guest OK).
  • Single-fix kernel built from with-src warm 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_testable for 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_disks is capped at 16, so the loop at ata-raid.c:2243 iterates disk < 16 only and never indexes past disks[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)

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Thu Jul 16 03:20:54 UTC 2026

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 11,472B OOB (448 into ar_softc trailing + 11,024 into heap). nataraid compiled in GENERIC:83, Intel PIIX4 passes gate check.