DF-2497 / verdict.json
{ "finding_id": "DF-2497", "status": "reproduced", "verdict": "CONFIRMED. The JBOD/SPAN branch of production ata_raid_strategy() (ata-raid.c:352-353) walks rdp->disks[drv].sectors with drv++ and no bound against total_disks, MAX_DISKS (16) or the 2424-byte allocation. Driven with crafted HPTv2 SPAN metadata (production parse installs total_disks=2, disks[1].sectors=1000) and a bio at blkno=5000: (1) deterministic in-object run (test 11): the production walk read disks[2..30] -- 29 entries past the configured disks, 15 past MAX_DISKS -- then dispatched I/O with request->this(drv)=30 and request->dev = the OOB-read rdp->disks[30].dev (a single valid stopper was planted at the disks[30] stride so the walk terminates at a mapped address; every read past total_disks is production behavior); (2) past-the-allocation run (test 13): without an in-object stopper the walk continued past index 48 and reached drv=51, reading .sectors/.flags/.dev 168+ bytes past the end of the heap object out of natural kernel heap, then dispatched with that garbage dev pointer. Termination is heap-content-dependent (kernel pointers stop the walk; zero regions make it climb forever -- the filed hang variant; unmapped pages would fault), so a panic is not deterministic on this guest, but the unbounded OOB-read walk and the dispatch to an OOB-read device pointer are fully demonstrated.", "reproduced": true, "impact": "dos", "confidence": "certain", "exploit_chain": "crafted HPTv2 SPAN metadata (type=HPTV2_T_SPAN, small total_sectors) on an nata disk -> array attached with per-disk sector sum far below any bio offset -> unprivileged read/write past the array end through the ar device -> while-loop reads rdp->disks[N].sectors for unbounded N (48-byte stride over kernel heap, extent attacker-influenced via blkno) -> either stops on a heap pointer >= lba and dispatches I/O to the OOB-read .dev pointer (wild device deref in ata_raid_send_request -> panic) or climbs an all-zero region forever (hang) or hits an unmapped page (panic). Demonstrated: drv=30 (deterministic) and drv=51 (past the allocation); guest survived both because the rigs stopper/dev target was a valid fake device.", "evidence": "run_nonfatal.log test11 (PROOF: production strategy dispatched to disk index drv=this=30 ... the walk read 29 entries past the configured disks and 15 past disks[] bounds, parse rc=1); run_test13.log (walk reached disk index 51 ... disks[51].sectors is 168 bytes past the object, serial STRATEGY DISPATCH #1: this(drv)=51 lines); fix_validation.log (fixed kernel: dispatches=0, ar0: I/O beyond end of SPAN/JBOD array, rig_bio_done b_error=5 for both tests)", "kernel_refs": [ "sys/dev/disk/nata/ata-raid.c:348-355", "sys/dev/disk/nata/ata-raid.c:397-411", "sys/dev/disk/nata/ata-raid.c:1844-1894", "sys/dev/disk/nata/ata-raid.h:97-110" ], "poc_changes": "No pass-1 pack existed (empty dir); the SPAN array state comes from a full production HPTv2 parse (no hand-crafted softc): the rig crafts the HPTv2 superblock (magic/order/type/disk_number/total_sectors; no checksum in that format) and lets production ata_raid_hptv2_read_meta() install it. rdp->status=AR_S_READY is set by the rig to mirror what production attach (ata_raid_config_changed) would do before I/O flows. The bio carries a rig bio_done callback so the fixed kernels biofinish(EIO) path terminates safely in the harness instead of running buf completion on a fake buffer.", "attempts": 4, "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": 4300, "guest_dirty": 0, "build_cmd": "guest /root/dfrig3: ./build.sh (make KLD dfrig3_ata.ko from #included production ata-raid.c)", "run_cmd": "kldload ./dfrig3_ata.ko; sysctl kern.dfrig3_ata_test=11; sysctl -n kern.dfrig3_ata_status; sysctl kern.dfrig3_ata_test=13; sysctl -n kern.dfrig3_ata_status", "code_hash": "bc3d761a0b7422afd5ef7908d5acc46c98c41ce5277c858e98ee5b033376407d", "notes": "The findings panic/leak upper bound was not forced: on this guest the walk stopped on mapped heap both times (kernel pointers exceed the test lba). The deterministic proof is the index/extent of the OOB reads plus the dispatch to the OOB-read dev pointer. Hang variant: an all-zero OOB region never terminates the loop (lba -= 0) -- consistent with the walk arithmetic; not separately demonstrated to avoid wedging the shared guest.", "recommended_fix": "Bound the walk: while (drv < (int)rdp->total_disks && lba >= rdp->disks[drv].sectors) ...; and fail the bio with biofinish(bp, NULL, EIO) when drv reaches total_disks (block number past the array end). See fix.diff.", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_kernel_uname": "DragonFly 6.5-DEVELOPMENT #1: Wed Oct 14 20:11:51 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC", "fix_verdict": "Single-fix kernel (walk bound + EIO in fix.diff) validates clean: both test 11 and test 13 show dispatches=0, the production ar0: I/O beyond end of SPAN/JBOD array message, and the bio completed with b_error=5 (EIO) -- the walk never leaves the configured disks; no OOB read, no dispatch to garbage dev.", "fix_evidence": "fix_validation.log: ===== FIXED-KERNEL TEST 11 ===== ... strategy returned 0; dispatches=0, ===== FIXED-KERNEL TEST 13 ===== ... dispatches=0, dmesg section: ar0: I/O beyond end of SPAN/JBOD array + dfrig3: rig_bio_done #1 b_error=5" } |