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)
PoC verification
Evidence pack
findings/poc/DF-2498 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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():
:2165map = &meta->disk[meta->total_disks];βtotal_disksis a rawu_int8_t; the metadata object is a fixed 1536-byte kmalloc, so anytotal_disks > ~22computes the mapping pointer past the allocation.:2230raid->total_disks = map->total_disks;β taken verbatim, up to 255 (MAX_DISKSis 16).:2243-2263the disks fill loop then writesraid->disks[disk]fordisk < raid->total_disksβ i.e. up to 255 entries of a 16-entry array β and indexesmeta->disk[map->disk_idx[disk] & 0xffff](48 bytes/entry, so0xffffreads atmeta+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 wroteraid->disks[16..59]: - 44 entries past
disks[MAX_DISKS-1]in-object, smashingtoggle/rebuild_lba/lock/disk/devstat/cdev/pidβ readback showsdev=<parent ptr> serial=d0d1β¦ sectors=0xc0ffee flags=0xbat disks[16], [24], [32], [40], [48]; - 11/11 entries
disks[49..59]past the 2424-byte allocation (raw readback of the crafted0x00C0FFEEmarker 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=parentand.flags |= PRESENT|ONLINEOOB for the same entries. - test 24 β the OOB READ (~3.0 MB):
disk_idx[0]=1(in-bounds marker0xE0..) proves the loop ran and honored the index; the sibling iterations withdisk_idx=0xffffreadmeta->disk[0xffff].serialatmeta+3145996and bcopy'd the (here zeroed) heap bytes intoraid->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.
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:
- OOB WRITE (primary) β
raid->total_disks = map->total_disks(:2230) taken verbatim (crafted 60;MAX_DISKSis 16) makes the fill loop (:2243-2263) writeraid->disks[16..59]: 44 entries pastdisks[]in-object (smashing toggle/rebuild_lba/lock/disk/devstat/cdev/pid of the livestruct 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 (crafted0x00C0FFEEsector 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/.flagswrites for the same entries. Withmap->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. - OOB READ (~3.0 MB) β
disk_idx = map->disk_idx[disk] & 0xffff(:2244) withdisk_idx=0xffffmakesmeta->disk[disk_idx]read atmeta + 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. Themeta->total_disks > ~21 β map pointer past the allocationclause of the finding is the same missing check (covered by the fix); on the guest amap-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
sys/dev/disk/nata/ata-raid.c:2165βmap = &meta->disk[meta->total_disks](u_int8_t, unvalidated)sys/dev/disk/nata/ata-raid.c:2190,2203βraid->width = map->total_diskssys/dev/disk/nata/ata-raid.c:2230βraid->total_disks = map->total_disks(up to 255 vs MAX_DISKS 16)sys/dev/disk/nata/ata-raid.c:2243-2263β OOB write loop +meta->disk[disk_idx]wild readsys/dev/disk/nata/ata-raid.c:2266-2283β serial-match loop adding OOB.dev/.flagswritessys/dev/disk/nata/ata-raid.h:295-354βstruct intel_raid_conf(1536-byte kmalloc, 48-byte disk entries),struct intel_raid_mapping
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
fixedSingle-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'
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.
No comments yet.