BSS out-of-bounds write on global ata_raid_arrays[] via untrusted LSI v2 raid_number
Summary
ata_raid_lsiv2_read_meta indexes global pointer array raidp[] (ata_raid_arrays[MAX_ARRAYS=16]) at array+meta->raid_number where array ranges 0..15 and meta->raid_number is raw u_int8_t from disk. Sum can reach 270 so parser writes freshly kmallocd ar_softc pointer into arbitrary BSS up to ~2KB past end of global array corrupting adjacent kernel globals. No check array+meta->raid_number<MAX_ARRAYS. array+=meta->raid_number further corrupts loop counter. Attacker: crafted LSI v2 superblock magic XIDE at LSIV2_LBA.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2494 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.9 KB | β raw | |
| VERDICT.md | β | 5.9 KB | β raw | |
| dfrig_ata.c | β | 23.7 KB | view raw | |
| Makefile | β | 442 B | β download | |
| craft_meta.py | β | 4.4 KB | view raw | |
| build.sh | β | 166 B | view raw | |
| run.sh | β | 189 B | view raw | |
| build.log | β | 955 B | view raw | |
| run.log | β | 5.5 KB | view raw | |
| run.2.log | β | 1.6 KB | view raw | |
| env.txt | β | 701 B | view raw | |
| fix.diff | β | 1.5 KB | view raw | |
| fix_build.log | β | 513 B | view raw | |
| fix_run.log | β | 1.8 KB | view raw | |
| fix_tests_123.log | β | 1.3 KB | view raw | |
| fix_tests_67.log | β | 1.1 KB | view raw | |
| manifest.json | β | 1.7 KB | view raw | |
| verdict.json | β | 7.3 KB | view raw |
DF-2494 β ata_raid_lsiv2_read_meta() stores ar_softc* out of bounds past the global ata_raid_arrays[16] via untrusted on-disk raid_number (plus same-parse disks[disk_number] heap OOB)
File: sys/dev/disk/nata/ata-raid.c:2770-2884 Β· Severity: High Β· Bucket: memcorrupt Β· Confidence: certain
Status: reproduced on the QEMU guest (production-code KLD rig, DF-5302/DF-5321/DF-5413
evidence class; the nata ata-bus subdisk attach path that feeds this parser is only
reachable with an ATA disk on an nata controller, and the standard vm.sh QEMU guest
cannot take an IDE hard disk at all β see VERDICT.md).
Build (guest, root)
cd /root/dfrig && ./build.sh # compiles production ata-raid.c into dfrig_ata.ko
Run
sh run.sh # rig tests 1, 2, 4 (survivable proofs) sysctl kern.dfrig_ata_test=5 # +2040 store then configs[255] OOB read (serial log) sysctl kern.dfrig_ata_test=6 # REAL kernel ata_raid_arrays target (boot-attack arm)
Expected (see run.log)
- test 1: production parse stores
0xfffff80116973700atraidp[29]β 104 bytes past the 128-byteata_raid_arrays[16]; parse returns 1 (array accepted). - test 2: first OOB slot
raidp[16](=&atapi_cam_devclassin the real kernel) written and retained. - test 4: same parse writes
raid->disks[49]40 bytes past the 2424-bytestruct ar_softcallocation (devpointer,flags=0xb). - test 6: with
raidp= the REAL kernelata_raid_arrays(0xffffffff8114dec0), the production skip-occupied walk plants/uses pointers in real kernel BSS and heap past the array β cross-subsystem corruption, silent.
Fix
fix.diff (shared shape with the DF-2495 pack): validate meta->raid_number <
MAX_ARRAYS and meta->disk_number < MAX_DISKS in ata_raid_lsiv2_read_meta().
Validated on a rebuilt kernel (fix_build.log / fix_run.log).
DF-2494 VERDICT β reproduced (production-code rig; real-kernel BSS target)
Bottom line
The pass-1 claim is real and worse than filed: ata_raid_lsiv2_read_meta()
(sys/dev/disk/nata/ata-raid.c:2799-2879) indexes the caller's raidp[]
(production callers pass the 16-entry global ata_raid_arrays,
ata-raid.c:138/1477) at array + meta->raid_number where meta->raid_number
is a raw u_int8_t taken from the last sector of the disk
(LSIV2_LBA, ata-raid.h:456-457). There is no bound check anywhere on the
path. On the guest this was proven three ways, all through 100% production
parser code (the only interposed seam is ata_queue_request, which completes
the metadata READ the way the disk would):
- rig test 1 (deterministic) β
raid_number=29: the production parser kmalloc()d anar_softcand stored the pointer atraidp[0+29], 104 bytes past the end of the 16-entry array; parse returned 1 (array accepted). - rig test 2 (deterministic) β
raid_number=16: the store landed on the FIRST byte past the array (offset +128). In the real guest kernel that exact address is&atapi_cam_devclass(nm:ata_raid_arrays=0xffffffff8114dec0,atapi_cam_devclass=0xffffffff8114df40). With an accepted RAID type the pointer is RETAINED (only the unknown-RAID-type cleanup path at 2849-2850 frees/nulls it). - rig test 6 (real-kernel arm) β with
raidppointed at the REAL kernelata_raid_arrays(0xffffffff8114dec0), the production skip-occupied walk (2802-2814: an occupied slot whose object'sformat/magic_0fields do not look like a fresh array βcontinue) walked across the real BSS neighbors (atapi_cam_devclass,acd_/afd_/ast_devclassall occupied) and: - run A (raid_number=23): the target slotraidp[23]=&sysctl__hw_amr_childrenwas occupied (it legitimately points at thehw.amr.force_sg32oid), so the walk continued toraidp[24]/raidp[25]and the array landed there β real kernel BSS 192+ bytes past the array. - run B (raid_number=16): the first slot whose pointed-to heap object happened to pass theformat/magic_0checks (raidp[20], +160) got the parse's field writes into that unrelated live heap object (cross- subsystem type-confusion write:magic_0=timestamp,type,total_disks,format,disks[2].dev/.sectors/.flags). rc=1, guest stayed up β silent corruption of an unknown victim allocation.
Corroboration in the same parse function: meta->disk_number (u_int8_t) is
used unvalidated at raid->disks[meta->disk_number] (2866-2876) and
meta->configs[raid_number] is read with raid_number up to 255 against the
30-entry table (2819-2822). rig test 4 (disk_number=49) showed the
disks[49] writes landing 40 bytes past the 2424-byte ar_softc allocation
(readback dev=<parent ptr> flags=0xb); rig test 5 (raid_number=255) showed
the +2040 store followed by the configs[255] OOB read ~3.5 KB past the
512-byte metadata object (in that run the garbage type=0 took the
unknown-type cleanup path: pointer freed, OOB slot re-nulled β a transient
write; the persistent-corruption range is raid_number in [16..29], and for
raid_number >= 30 persistence depends on adjacent-heap garbage).
Threat model reality check
- The trigger is on-disk crafted metadata read at subdisk attach (boot or disk insertion on an nata-controlled ATA channel). No privileges needed in the OS β the DF-5339-style crafted-disk model.
- The authentic boot arm could not be armed on the standard guest:
attaching ANY IDE hard disk (even a benign unrelated image, tried
usbstick.img) to the vm.sh QEMU invocation hangs SeaBIOS before the loader produces a single serial byte (CPU observed HLT'd/spinning in the BIOS). This is an infrastructure limitation of the standard snapshot, not a property of the bug; the rig arm above drives the same production parser against the same real kernel BSS target instead. - Practical primitive: an 8-byte kernel-heap-pointer plant at attacker-chosen
offset +128..+2040 past
ata_raid_arrays(skip-occupied semantics), or β when the landed slot already holds a heap pointer whose object passes the format/magic checks β silent field writes into that live object (type confusion). Deterministic panic was not achieved on this guest (the obvious victim,sysctl__hw_amr_children, happens to be occupied at boot, andcurfail.19623's object survived the field writes).
Fix validation
fix.diff adds, right after the $XIDE$ magic check:
if (meta->raid_number >= MAX_ARRAYS || meta->disk_number >= MAX_DISKS) goto lsiv2_out;
This simultaneously bounds raidp[array + raid_number] (array < 16),
meta->configs[raid_number] (< 30), and β with the companion disk_count
check β raid->disks[...]. Validated on a single-fix rebuilt kernel
(fix_build.log / fix_run.log): rig tests 1/2 return rc=0 with the witness
slots untouched, and the real-kernel test 6 leaves the real BSS neighbors
unchanged.
Kernel references
sys/dev/disk/nata/ata-raid.c:2799-2806βraidp[array + meta->raid_number]OOB storesys/dev/disk/nata/ata-raid.c:2816βarray += meta->raid_numberloop-counter corruptionsys/dev/disk/nata/ata-raid.c:2819-2822βmeta->configs[raid_number]OOB read (table is [30])sys/dev/disk/nata/ata-raid.c:2866-2876βraid->disks[meta->disk_number]OOB writesys/dev/disk/nata/ata-raid.h:456-513βLSIV2_LBA,struct lsiv2_raid_conf(raid_number/disk_number u_int8_t)sys/dev/disk/nata/ata-raid.c:138βstatic struct ar_softc *ata_raid_arrays[MAX_ARRAYS]
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 (fix.diff: raid_number/disk_number validation after the magic check) built with nativekernel, installed, rebooted; rig rebuilt against the fixed production source. Baseline behaviors are gone: rig test1 raid_number=29 now returns rc=0 with witness[29] unchanged (was rc=1, heap pointer stored 104 bytes past the array); test2 raid_number=16 returns rc=0 with the first OOB slot untouched; real-kernel test6 leaves raidp[16..21] byte-identical (baseline planted/used a pointer at raidp[20] and wrote fields into the heap object it pointed to).
fix_run.log: 'ata_raid_lsiv2_read_meta() returned 0' + 'after: witness[29] = 0 (unchanged)' + 'before: raidp[16..21] = ... 0 0 / after: raidp[16..21] = ... 0 0 / parse rc=0' vs run.log baseline 'returned 1 ... CHANGED'.
Confirmed kernel references
Detail
Exploit chain
crafted LSI v2 ($XIDE$) superblock in the last sector of an ATA disk on an nata channel -> subdisk attach -> ata_raid_read_metadata -> ata_raid_lsiv2_read_meta (ata-raid.c:1477/2770) -> [1] 8-byte store of a kmalloc()d ar_softc at &ata_raid_arrays[0..15]+8raid_number, raid_number 16..255 => BSS OOB write +128..+2040 bytes past the 128-byte global (skip-occupied walk relocates it to the first empty/accepting slot); [2] if that slot already holds a heap pointer whose object passes the format(0x40)/magic_0(0x28) checks, the parse WRITES array fields into that live object (type confusion, silent); [3] same parse: raid->disks[disk_number] (disk_number up to 255 vs disks[16]) and meta->configs[raid_number] (up to 255 vs [30]) are further OOB read/write surfaces; [4] post-parse the hidden array (lun = 0+raid_number > 15) is invisible to the 0..15 attach loop but persists. On differently-populated kernels the +128..+208 window covers devclass pointers (atapi_cam/acd/afd/ast), linux_no_adapter, sysctl__hw_amr_children and CAM failmap statics -- silent cross-subsystem corruption from unprivileged on-disk input.
Evidence (decisive lines)
run.log: rig test1 'before: witness[29] = 0 / after: witness[29] = 0xfffff80116973700 (CHANGED -- production code kmalloc()d an ar_softc and stored the pointer OUT OF BOUNDS)' and test2 first-OOB-slot retention; run.log test6 section: real-kernel raidp[16..21] walk with nm symbol map (ata_raid_arrays=0xffffffff8114dec0, atapi_cam_devclass=+128, sysctl__hw_amr_children=+184) and the run-B type-confusion narrative (rc=1, fields written into the heap object at raidp[20], guest up); fix_run.log: 'returned 0 / unchanged / REAL kernel BSS unchanged' on the fixed kernel.
PoC changes
No pass-1 pack existed (empty dirs); rig authored fresh per the DF-5413 KLD pattern: production ata-raid.c #included with DRIVER_MODULE/DECLARE_MODULE/MODULE_DEPEND/MODULE_VERSION neutralized (hand-rolled module registration for the rig itself), ata_queue_request interposed at module link time as the disk-I/O seam (synchronous completion from a crafted 512-byte $XIDE$ image, plus the callout_init_lk the real queue path performs), fake device graph (subdisk->ad parent with ata_device softc + ad_softc ivars), 300-slot zeroed witness raidp array for deterministic proofs, and a real-kernel arm passing the true kernel address 0xffffffff8114dec0 as raidp. GCC -Warray-bounds warning on the intentional OOB addressing kept in build.log.
Verified recommended fix
In ata_raid_lsiv2_read_meta(), after the $XIDE$ magic check, reject meta->raid_number >= MAX_ARRAYS or meta->disk_number >= MAX_DISKS (bounds raidp[], configs[30] and disks[] simultaneously).
Verdict
CONFIRMED, worse than filed. ata_raid_lsiv2_read_meta() (ata-raid.c:2799-2879) indexes raidp[] at array+meta->raid_number with raid_number a raw u_int8_t from the crafted $XIDE$ superblock and no bound check: production code (compiled verbatim into the dfrig_ata KLD, only the disk-read completion seam interposed) stored a freshly kmalloc()d ar_softc* at raidp[0+29] = 104 bytes past the 16-entry ata_raid_arrays (rig test1, rc=1, before=0 after=0xfffff80116973700) and at raidp[0+16] = the first byte past the array (= &atapi_cam_devclass in the real kernel, rig test2, pointer retained). Driving the same production parse with raidp = the REAL kernel ata_raid_arrays (0xffffffff8114dec0, rig test6) demonstrated the boot-attack target: the skip-occupied walk crossed the real BSS neighbors (atapi_cam_devclass/acd_/afd_/ast_devclass at +128..+152) and either planted the pointer in the first empty OOB slot or -- run B -- type-confused the heap object already pointed to by raidp[20] (+160, ast_total area) and wrote array fields into that live unrelated object (magic_0=timestamp, type, total_disks, format, disks[2].dev/.sectors/.flags), silently (guest stayed up). Corroborating same-parse primitives: disks[meta->disk_number] with disk_number=49 writes 40 bytes past the 2424-byte ar_softc object (rig test4, flags=0xb read back from behind the object); raid_number=255 executes the +2040 store and then OOB-reads configs[255] ~3.5KB past the 512-byte metadata object (rig test5: garbage type 0x00 took the unknown-type cleanup, transient write; persistent range is raid_number 16..29). The authentic crafted-disk-at-boot arm could not be armed on the standard guest: ANY IDE hard disk (even a benign unrelated image) hangs SeaBIOS before the loader -- documented infrastructure limitation (VERDICT.md). Impact ceiling on this guest: silent kernel BSS pointer-planting / heap type-confusion writes at attacker-chosen offsets +128..+2040 past ata_raid_arrays from unprivileged on-disk metadata; no panic or uid0 was achieved (hence impact=none beyond the proven write primitive). fix.diff (reject raid_number>=MAX_ARRAYS / disk_number>=MAX_DISKS after the magic check) validated on a single-fix rebuilt kernel: rig tests 1/2 return rc=0 with witness slots untouched and the real-kernel test 6 leaves raidp[16..21] byte-identical.
No comments yet.