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

Intel MatrixRAID heap OOB read via untrusted disk_idx and unvalidated map pointer

Summary

ata_raid_intel_read_meta computes map pointer by indexing meta->disk[] with untrusted u_int8_t total_disks then dereferences map->disk_idx[] with untrusted index masked to 16 bits to index meta->disk[] again. meta is fixed 1536-byte heap buffer. For total_disks>~21 map pointer past allocation. disk_idx masked 16 bits meta->disk[disk_idx] 48 bytes/element reads up to ~3MB past meta. bcopy copies 16 bytes arbitrary heap into raid->disks[].serial heap-garbage into .sectors. raid->total_disks=map->total_disks outer loop writes raid->disks[disk] for disk up to 255 OOB write.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2498 Β· 15 files
FileTypeDescriptionSize
README.md β€” 3.5 KB ↓ raw
VERDICT.md β€” 4.5 KB ↓ raw
dfrig3_ata.c β€” 36.4 KB view raw
Makefile β€” 445 B ↓ download
atastat.c β€” 927 B view raw
build.sh β€” 138 B view raw
run.sh β€” 236 B view raw
run_nonfatal.log β€” 4.4 KB view raw
run_test23_24.log β€” 2.1 KB view raw
env.txt β€” 652 B view raw
fix.diff β€” 6.4 KB view raw
fix_build.log β€” 206 B view raw
fix_validation.log β€” 46.5 KB view raw
manifest.json β€” 1.9 KB view raw
verdict.json β€” 6.3 KB view raw

DF-2498 β€” Intel MatrixRAID heap OOB write/read via untrusted map->total_disks / disk_idx

What was verified

sys/dev/disk/nata/ata-raid.c, ata_raid_intel_read_meta():

  • :2165 map = &meta->disk[meta->total_disks]; β€” total_disks is a raw u_int8_t; the metadata object is a fixed 1536-byte kmalloc, so any total_disks > ~22 computes the mapping pointer past the allocation.
  • :2230 raid->total_disks = map->total_disks; β€” taken verbatim, up to 255 (MAX_DISKS is 16).
  • :2243-2263 the disks fill loop then writes raid->disks[disk] for disk < raid->total_disks β€” i.e. up to 255 entries of a 16-entry array β€” and indexes meta->disk[map->disk_idx[disk] & 0xffff] (48 bytes/entry, so 0xffff reads at meta+216+48*65535 β‰ˆ 3.0 MB past the object).

Both halves reproduced through 100% production code (rig: the production ata-raid.c compiled into a KLD; crafted Intel metadata with a valid checksum served by the only interposed seam, ata_queue_request):

  • test 10 β€” the OOB WRITE (primary): crafted map->total_disks=60 β†’ the production loop wrote raid->disks[16..59]:
  • 44 entries past disks[MAX_DISKS-1] in-object, smashing toggle/rebuild_lba/lock/disk/devstat/cdev/pid β€” readback shows dev=<parent ptr> serial=d0d1… sectors=0xc0ffee flags=0xb at disks[16], [24], [32], [40], [48];
  • 11/11 entries disks[49..59] past the 2424-byte allocation (raw readback of the crafted 0x00C0FFEE marker at +2488..+2992);
  • an adjacent guard chunk (0x11-filled, same size class) SMASHED 324/2424 bytes with the crafted pattern β€” independent proof the writes crossed into a live neighbor allocation;
  • the serial-match loop (:2266-2283) then wrote .dev=parent and .flags |= PRESENT|ONLINE OOB for the same entries.
  • test 24 β€” the OOB READ (~3.0 MB): disk_idx[0]=1 (in-bounds marker 0xE0..) proves the loop ran and honored the index; the sibling iterations with disk_idx=0xffff read meta->disk[0xffff].serial at meta+3145996 and bcopy'd the (here zeroed) heap bytes into raid->disks[]. The wild read executed without a fault because the kernel direct-maps the whole kmem region β€” the "panic" variant of the filed claim is heap-layout-dependent; the read primitive itself is deterministic.

No deterministic panic on this guest (silent corruption), matching the DF-2494 finding's profile in the same parser family.

Build / run (guest, root; requires vm.sh reset with-src)

cd /root/dfrig3 && ./build.sh
kldload ./dfrig3_ata.ko
sysctl kern.dfrig3_ata_test=10    # OOB write: disks[16..59] smash
sysctl -n kern.dfrig3_ata_status
sysctl kern.dfrig3_ata_test=24    # OOB read at meta+3.0MB (marker proof)
sysctl -n kern.dfrig3_ata_status

Expected (baseline): test 10 in-object proof … CONFIRMED + past-object proof: 11/11; test 24 loop-ran marker … CONFIRMED.

Fix validation

fix.diff validates, in ata_raid_intel_read_meta(): reject meta->total_disks outside [1, MAX_DISKS] (keeps map inside the 1536-byte buffer); reject map->total_disks outside [1, MAX_DISKS] (bounds raid->total_disks/width and the disks loop); skip any disk_idx >= meta->total_disks. On the single-fix kernel (fix_validation.log): test 10 β€” parse rejected (ataraid: Intel invalid map total_disks 60), rc=0, witness NULL, guards untouched, no smash; test 24 β€” the 0xffff entries are skipped (disks[1..15] stay zero), only the in-bounds marker remains; test 23 β€” no wild read, no fault. Guest stayed up throughout.

VERDICT.md
↓ download raw

DF-2498 VERDICT β€” reproduced (heap OOB write past disks[15] and past the allocation; ~3MB OOB read)

Bottom line

CONFIRMED, both halves of the finding. In ata_raid_intel_read_meta() (sys/dev/disk/nata/ata-raid.c) the mapping pointer, the array disk count and every disk-table index come straight from crafted on-disk metadata with no bounds checks:

  1. OOB WRITE (primary) β€” raid->total_disks = map->total_disks (:2230) taken verbatim (crafted 60; MAX_DISKS is 16) makes the fill loop (:2243-2263) write raid->disks[16..59]: 44 entries past disks[] in-object (smashing toggle/rebuild_lba/lock/disk/devstat/cdev/pid of the live struct ar_softc, read back showing the crafted serial/sectors/flags pattern) and 11/11 entries (disks[49..59]) past the end of the 2424-byte kmalloc object (crafted 0x00C0FFEE sector markers read back at victim +2488..+2992; an adjacent 0x11-filled guard chunk of the same size class was smashed across 324/2424 bytes with the same pattern β€” the writes demonstrably crossed into a neighbor allocation). The serial-match loop (:2266-2283) adds further OOB .dev/.flags writes for the same entries. With map->total_disks=255 (also unvalidated) the write range extends to ~+13.3KB past the object (arithmetic shown in the rig output); 60 already crosses the allocation.
  2. OOB READ (~3.0 MB) β€” disk_idx = map->disk_idx[disk] & 0xffff (:2244) with disk_idx=0xffff makes meta->disk[disk_idx] read at meta + 216 + 48*65535 = meta+3145996, ~3.0MB past the 1536-byte metadata allocation (:2247-2262 bcopy 16 serial bytes + sectors + flags from there). Proven executed via a mixed-index craft (in-bounds marker honored, wild sibling reads performed); the bytes at that distance happened to be zeroed heap, so nothing faulted β€” the kernel direct-maps the kmem region, making the "panic" variant heap-layout-dependent rather than deterministic. The meta->total_disks > ~21 β†’ map pointer past the allocation clause of the finding is the same missing check (covered by the fix); on the guest a map-past-buffer read lands in mapped KVA.

No userland leak path was demonstrated (the copied serials are not exposed by IOCATARAIDSTATUS), so the demonstrated impact is silent kernel-heap corruption β€” same profile as DF-2494 in this parser family. The primitive is strong: attacker-chosen 16-byte patterns written at 48-byte stride up to ~13KB past a heap object, plus arbitrary-offset (16-bit index Γ— 48) heap reads into the array state.

Threat model

Crafted Intel MatrixRAID metadata ("Intel Raid ISM Cfg Sig. ", valid checksum β€” computed by the rig exactly as the parser requires) on a disk attached to an nata channel; parsed at boot/attach with no privileges. The rig drives the identical production parser with the identical crafted image (only ata_queue_request interposed to serve the sector).

Fix validation (single-fix kernel)

fix.diff adds three validations to the parser: meta->total_disks in [1, MAX_DISKS] before computing map; map->total_disks in [1, MAX_DISKS] before use (frees the slot and exits like the unknown-type path); disk_idx >= meta->total_disks skipped. On the fixed kernel (fix_validation.log): the crafted map->total_disks=60 parse is rejected (ataraid: Intel invalid map total_disks 60, rc=0, array NULL, guards untouched); the 0xffff disk_idx entries are skipped (disks[1..15] remain zero, no wild read, no fault); the in-bounds marker still works. Guest stayed up through the whole battery.

Kernel references

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

Single-fix kernel (three bounds checks in the Intel parser) validates clean: the crafted map->total_disks=60 parse is rejected ('ataraid: Intel invalid map total_disks 60', rc=0, raidp[0] freed+NULL, guards untouched, no in-object or past-allocation writes); disk_idx=0xffff entries are skipped (disks[1..15] remain zero -- the ~3.0MB wild read is gone) while the in-bounds marker still parses; guest stable through the whole battery.

fix_validation.log: '===== FIXED-KERNEL TEST 10 ===== ... parse rc=0 raidp[0]=0 ... parse failed' (rejected before any loop writes; guard list unchanged), '===== FIXED-KERNEL TEST 24 ===== ... disks[1..15] all zero, loop-ran marker e0e1 CONFIRMED', dmesg: 'ataraid: Intel invalid map total_disks 60'
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Wed Oct 14 20:11:51 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC

Confirmed kernel references

Detail

Exploit chain

crafted Intel metadata ('Intel Raid ISM Cfg Sig. ', valid checksum, meta->total_disks=2 keeping the mapping in-bounds, map->total_disks=60..255, disk_idx[] arbitrary 16-bit) on a disk attached to an nata channel -> production parse at boot/attach -> (a) raid->disks[disk] written for disk up to 255: attacker-chosen 16-byte serials, sectors and flags planted at 48-byte stride up to ~13.3KB past a 2424-byte heap object (in-object fields toggle/rebuild_lba/lock/disk/devstat/cdev/pid of a LIVE array corrupted first); (b) 16-byte heap samples readable from meta+216+48*idx for any 16-bit idx (up to ~3.0MB away) copied into array state. Silent corruption of adjacent kernel heap allocations; a deterministic panic was NOT achieved on this guest (writes land in mapped neighbors; the wild read lands in mapped KVA) -- primitive, not a finished escalation.

Evidence (decisive lines)

run_nonfatal.log test10 ('in-object proof: disks[16].serial[0..1]=d0d1 == crafted D0 D1 -- OOB WRITE CONFIRMED', 'past-object proof: 11/11 entries disks[49..59] carry the crafted sectors marker 0xC0FFEE at rdp+2488..+2992 (object ends at +2424)', 'guard[33] ... SMASHED (324/2424 bytes): first words ffffffff8260b440 d7d6d5d4d3d2d1d0 ...'); run_test23_24.log (wild read at meta+3145996 executed; loop-ran marker e0e1 CONFIRMED with 0xffff siblings reading zeroed heap 3.0MB away, no fault); fix_validation.log (fixed kernel: 'ataraid: Intel invalid map total_disks 60', parse rejected, guards untouched; disks[1..15] stay zero -- no wild read)

PoC changes

No pass-1 pack existed (empty dir); rig authored on the DF-2494 pattern. The Intel craft required: correct 1024-byte checksum over the post-half-swap buffer (computed in-kernel), the parser's half-swap inverted in the image builder, meta->disk[0].serial matching the fake parent identify serial so the array is retained for readback, and meta->disk[1].serial = 0xE0.. as an in-bounds loop-ran marker. The +3MB read lands in mapped KVA on this guest (no fault) -- demonstrated via the mixed disk_idx marker technique instead of a crash. Impact recorded as 'none' only because the enum lacks a silent-corruption value; see impact_note in manifest.json.

Verified recommended fix

In ata_raid_intel_read_meta(): reject meta->total_disks outside [1, MAX_DISKS] before computing map; reject map->total_disks outside [1, MAX_DISKS] before use; skip any disk_idx >= meta->total_disks. See fix.diff.

Verdict

CONFIRMED, both halves of the finding, through the unmodified production ata_raid_intel_read_meta() fed crafted Intel MatrixRAID metadata with a valid checksum. OOB WRITE: map->total_disks=60 is taken verbatim as raid->total_disks (ata-raid.c:2230; MAX_DISKS=16), so the fill loop (:2243-2263) wrote raid->disks[16..59] -- 44 entries past disks[MAX_DISKS-1] IN-OBJECT (smashing toggle/rebuild_lba/lock/disk/devstat/cdev/pid of the live 2424-byte ar_softc; readback shows the crafted dev/serial=D0../sectors=0xC0FFEE/flags=0xb pattern) and 11/11 entries disks[49..59] PAST the allocation (crafted 0xC0FFEE markers read back at victim+2488..+2992; an adjacent 0x11-filled guard chunk of the same size class was smashed across 324/2424 bytes with the same pattern). The serial-match loop (:2266-2283) adds OOB .dev=parent/.flags writes for the same entries. With map->total_disks=255 (also unvalidated) the range extends to ~+13.3KB past the object. OOB READ: disk_idx = map->disk_idx[disk] & 0xffff with 0xffff makes meta->disk[disk_idx] read at meta+216+48*65535 = meta+3145996, ~3.0MB past the 1536-byte metadata kmalloc; proven executed via a mixed-index craft (in-bounds 0xE0 marker honored, wild sibling reads performed, bytes landed zeroed-heap so no fault -- the kernel direct-maps kmem, making the panic variant heap-layout-dependent). No userland leak path demonstrated (serials are not exposed by IOCATARAIDSTATUS): demonstrated impact is silent kernel-heap corruption, same profile as DF-2494 in this parser family.