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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2494 Β· 18 files
FileTypeDescriptionSize
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 0xfffff80116973700 at raidp[29] β€” 104 bytes past the 128-byte ata_raid_arrays[16]; parse returns 1 (array accepted).
  • test 2: first OOB slot raidp[16] (= &atapi_cam_devclass in the real kernel) written and retained.
  • test 4: same parse writes raid->disks[49] 40 bytes past the 2424-byte struct ar_softc allocation (dev pointer, flags=0xb).
  • test 6: with raidp = the REAL kernel ata_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).

VERDICT.md
↓ download raw

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):

  1. rig test 1 (deterministic) β€” raid_number=29: the production parser kmalloc()d an ar_softc and stored the pointer at raidp[0+29], 104 bytes past the end of the 16-entry array; parse returned 1 (array accepted).
  2. 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).
  3. rig test 6 (real-kernel arm) β€” with raidp pointed at the REAL kernel ata_raid_arrays (0xffffffff8114dec0), the production skip-occupied walk (2802-2814: an occupied slot whose object's format/magic_0 fields do not look like a fresh array β†’ continue) walked across the real BSS neighbors (atapi_cam_devclass, acd_/afd_/ast_devclass all occupied) and: - run A (raid_number=23): the target slot raidp[23] = &sysctl__hw_amr_children was occupied (it legitimately points at the hw.amr.force_sg32 oid), so the walk continued to raidp[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 the format/magic_0 checks (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, and curfail.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

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 (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'.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Mon Oct 12 11:14:44 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC

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.