DF-2494 / verdict.json
{ "finding_id": "DF-2494", "status": "reproduced", "reproduced": true, "impact": "none", "confidence": "certain", "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.", "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]+8*raid_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": "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.", "kernel_refs": [ "sys/dev/disk/nata/ata-raid.c:2799-2806", "sys/dev/disk/nata/ata-raid.c:2816", "sys/dev/disk/nata/ata-raid.c:2819-2822", "sys/dev/disk/nata/ata-raid.c:2866-2876", "sys/dev/disk/nata/ata-raid.h:456-513", "sys/dev/disk/nata/ata-raid.c:138" ], "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.", "attempts": 8, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 4700, "guest_dirty": 0, "build_cmd": "cd /root/dfrig && make (KLD: production sys/dev/disk/nata/ata-raid.c #included into dfrig_ata.c)", "run_cmd": "kldload /root/dfrig/dfrig_ata.ko; sysctl kern.dfrig_ata_test=1; sysctl -n kern.dfrig_ata_status (also 2,4,5,6)", "code_hash": "dd86ae7982825f5ff2439adbf9c42fe4724bcdf148476be9b002a4ffbfa2cc1c", "notes": "Baseline runs dirtied the real kernel (silent BSS/heap corruption from tests 6/7); guest reset to clean-source snapshot after fix validation (guest_dirty=0 at return). qcow2+force-kill rolls back unsynced writes -- sync after every guest-side build is part of run.sh discipline. The IDE-disk/SeaBIOS hang was verified with a benign unrelated image, so the boot arm limitation is infrastructural, not payload-specific.", "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).", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_kernel_uname": "DragonFly 6.5-DEVELOPMENT #1: Mon Oct 12 11:14:44 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC", "fix_verdict": "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_evidence": "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." } |