# 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`:

```c
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()`
  installs `type=AR_T_SPAN, total_disks=2, disks[0].sectors=0,
  disks[1].sectors=1000` from crafted metadata. Production
  `ata_raid_strategy()` with `blkno=5000`: the walk read
  `rdp->disks[drv].sectors` for drv = 0,1,…,29 (all past
  `total_disks=2`, indices ≥16 past `MAX_DISKS`), stopped on a single
  harness stopper planted at the `disks[30].sectors` stride, and
  **dispatched I/O with `request->this(drv)=30` and
  `request->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/.dev` 168+ bytes past the end of the
  2424-byte heap object, stopping on natural heap contents and
  dispatching with `this(drv)=51` (`run_test13.log` + serial
  `STRATEGY DISPATCH` lines).

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.
