Unbounded disk-index loop in SPAN/JBOD I/O strategy causes OOB read / hang
Summary
JBOD/SPAN branch of ata_raid_strategy walks per-disk sector table with drv++ no bound against MAX_DISKS or total_disks. If bio destination block past sum of configured per-disk sectors (trivially with crafted metadata) drv climbs past rdp->disks[16] reads/writes rdp->disks[N].sectors for N up to whatever it takes to fault leaking kernel heap and panicking. If garbage value read is 0 loop never terminates hang.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2497 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.3 KB | β raw | |
| VERDICT.md | β | 3.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_test13.log | β | 1.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.4 KB | view raw | |
| verdict.json | β | 5.4 KB | view raw |
DF-2497 β unbounded SPAN/JBOD disk-index walk in ata_raid_strategy (OOB read / hang)
What was verified
sys/dev/disk/nata/ata-raid.c:352-353:
case AR_T_JBOD:
case AR_T_SPAN:
drv = 0;
lba = blkno;
while (lba >= rdp->disks[drv].sectors)
lba -= rdp->disks[drv++].sectors; /* NO BOUND */
The walk has no bound against rdp->total_disks, MAX_DISKS (16), or
the size of the struct ar_softc allocation (2424 bytes; disks[] at
+112, 48 bytes/entry, so disks[49] is the first entry past the
object). A bio whose block number is beyond the sum of the configured
per-disk sectors β trivially reachable with crafted metadata β makes
drv climb through the whole kernel heap at a 48-byte stride.
Both proven through 100% production code (rig: production
ata-raid.c compiled into a KLD, HPTv2 SPAN metadata crafted exactly
as a crafted disk would present it, only ata_queue_request
interposed):
- test 11 (deterministic) β production
ata_raid_hptv2_read_meta()installstype=AR_T_SPAN, total_disks=2, disks[0].sectors=0, disks[1].sectors=1000from crafted metadata. Productionata_raid_strategy()withblkno=5000: the walk readrdp->disks[drv].sectorsfor drv = 0,1,β¦,29 (all pasttotal_disks=2, indices β₯16 pastMAX_DISKS), stopped on a single harness stopper planted at thedisks[30].sectorsstride, and dispatched I/O withrequest->this(drv)=30andrequest->dev = rdp->disks[30].devβ both OOB reads (run_nonfatal.log,run_test13.log). - test 13 (past the allocation, natural heap) β same setup, no
planted stop within the object: the production walk continued past
index 48 and reached disk index 51, reading
disks[51].sectors/.flags/.dev168+ bytes past the end of the 2424-byte heap object, stopping on natural heap contents and dispatching withthis(drv)=51(run_test13.log+ serialSTRATEGY DISPATCHlines).
Hang aspect (as filed): while climbing, any OOB region that reads as
zero never satisfies the loop condition with a smaller lba, so the
loop keeps advancing (lba -= 0) until it reaches a value β₯ lba
(kernel pointers usually stop it) or unmapped memory (page fault). On
this guest the walk stopped on mapped heap both times; a panic is
heap-layout-dependent, and the "hang" variant is the same walk with an
all-zero region. Impact: kernel heap OOB read at attacker-chosen stride
(blkno is attacker-controlled), plus I/O dispatch to an OOB-read device
pointer.
Build / run (guest, root; requires vm.sh reset with-src)
cd /root/dfrig3 && ./build.sh kldload ./dfrig3_ata.ko sysctl kern.dfrig3_ata_test=11 # SPAN walk, deterministic (drv=30) sysctl -n kern.dfrig3_ata_status sysctl kern.dfrig3_ata_test=13 # walk past the allocation (drv>=49) sysctl -n kern.dfrig3_ata_status
Expected (baseline): status shows PROOF: production strategy
dispatched to disk index drv=this=NN (>= MAX_DISKS / past the
allocation).
Fix validation
fix.diff bounds the walk (while (drv < (int)rdp->total_disks && β¦))
and fails the bio with EIO when blkno is past the array end. On the
single-fix kernel (fix_validation.log): both tests show
dispatches=0, ar0: I/O beyond end of SPAN/JBOD array,
rig_bio_done b_error=5 (EIO) β no OOB read, no dispatch.
DF-2497 VERDICT β reproduced (OOB read walk past total_disks, past MAX_DISKS, past the allocation)
Bottom line
CONFIRMED. The JBOD/SPAN branch of the production
ata_raid_strategy() (sys/dev/disk/nata/ata-raid.c:352-353) walks
rdp->disks[drv].sectors with drv++ and no bound of any kind.
Driven with a bio whose block number is past the array end (the
per-disk sector table comes verbatim from crafted HPTv2 SPAN metadata:
total_disks=2, disks[1].sectors=1000):
- Deterministic in-object proof (test 11): the walk consumed the
configured disks, then kept reading
rdp->disks[2..30].sectorsβ 29 entries pasttotal_disks=2, 15 pastMAX_DISKS=16β and the production code then dispatched I/O using the OOB-readrdp->disks[30].devas the target device (shim capturedthis(drv)=30,dev=0xffffffff8260b3c0). The only harness interposition besides the I/O shim is one planted valid stopper at thedisks[30].sectorsstride so the walk terminates at a mapped, valid address instead of depending on heap luck; every index the production loop read beyond the configured disks is the bug. - Past-the-allocation proof (test 13): without an in-object
stopper, the production walk continued past index 48 (first index
past the 2424-byte
struct ar_softcallocation;disks[]at +112, 48 bytes/entry) and reached drv=51, reading.sectors/.flags/.devfrom unrelated kernel heap 168+ bytes past the object, then dispatched with that OOB-read.devpointer.
The walk extent is attacker-influenced (blkno comes from the bio;
bigger blkno β farther walk). Termination depends on heap contents:
kernel pointers (0xffffβ¦) stop it; zero regions make the loop climb
forever (lba -= 0) β the filed "hang" variant; unmapped pages would
fault. On this guest both runs stopped on mapped heap, so no panic was
forced; the primitive (unbounded heap OOB read + dispatch to garbage
device pointer) is fully demonstrated.
Threat model
Crafted on-disk SPAN/JBOD metadata (e.g., HPTv2 HPTV2_T_SPAN with
small total_sectors) makes the array's usable sector sum far smaller
than any bio offset an unprivileged filesystem user can request once
the array is mounted; every read past the sum walks kernel heap OOB.
Fix validation (single-fix kernel)
fix.diff bounds the walk with drv < (int)rdp->total_disks and
fails out-of-range I/O (biofinish(bp, NULL, EIO) +
"ar%d: I/O beyond end of SPAN/JBOD array"). On the fixed kernel
(fix_validation.log): test 11 and test 13 both show dispatches=0,
the beyond-end message, and rig_bio_done b_error=5 β the walk never
leaves the configured disks. In-range SPAN I/O still works (the shim
dispatched normally in DF-2496 test20's RAID0 path; the SPAN chunk
computation below the new bound is unchanged).
Kernel references
sys/dev/disk/nata/ata-raid.c:348-355β the unbounded JBOD/SPAN walksys/dev/disk/nata/ata-raid.c:397-411β dispatch using OOB-readrdp->disks[drv].flags/.devsys/dev/disk/nata/ata-raid.h:97-110βstruct ar_disk(48 bytes) anddisks[MAX_DISKS=16]sys/dev/disk/nata/ata-raid.c:1844-1894β HPTv2 SPAN parse installing per-disk sectors verbatim
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 (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_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'
Confirmed kernel references
Detail
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 rig's stopper/dev target was a valid fake device.
Evidence (decisive lines)
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)
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 kernel's biofinish(EIO) path terminates safely in the harness instead of running buf completion on a fake buffer.
Verified 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.
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.
No comments yet.