# DF-1172 — LSI v2 parser: OOB write on static `ata_raid_arrays[]` + heap overflow via unvalidated `meta->raid_number` / `meta->disk_number`

## Verdict
**REPRODUCED (primitive confirmed at source + harness level).** The bug is a
genuine three-site OOB in `ata_raid_lsiv2_read_meta()` — confirmed by
line-by-line source trace and a faithful userspace harness built from the real
kernel structs (`struct ar_softc`, `struct ar_disk`, `struct lsiv2_raid_conf`).
The driver **is** compiled into the default `X86_64_GENERIC` kernel
(`device nataraid`, `X86_64_GENERIC:83`) and the guest's Intel PIIX4 IDE
controller passes the `read_metadata` vendor gate, but the live in-kernel
trigger is blocked by the same QEMU/loader artifact documented in DF-1171 (the
DragonFly loader hangs on *any* extra hard disk before the kernel boots). The
demonstrated primitive ceiling is **memory corruption / kernel panic (DoS) at
device-attach time**; uid0 escalation is not realistic from this primitive
(see "Exploit chain").

## Mechanism (trigger → primitive → effect)
`ata_raid_lsiv2_read_meta()` (`sys/dev/disk/nata/ata-raid.c:2770`) parses
on-disk LSI v2 RAID metadata (magic `"$XIDE$"`, `ata-raid.h:461`) read from the
last sector of an ATA disk (`LSIV2_LBA = total_secs-1`, `ata-raid.h:456`).
After the magic check it uses the attacker-supplied per-disk indices with no
bounds checking. Three OOB sites:

- **BUG 1 — BSS OOB write of a heap pointer** (`ata-raid.c:2802-2803,2807`):
  ```
  for (array = 0; array < MAX_ARRAYS; array++) {
      if (!raidp[array + meta->raid_number]) {                       // :2802
          raidp[array + meta->raid_number] = kmalloc(sizeof(ar_softc),...); // :2803
      }
      raid = raidp[array + meta->raid_number];                       // :2807
  ```
  `raidp` = `static struct ar_softc *ata_raid_arrays[MAX_ARRAYS]` with
  `MAX_ARRAYS = 16` (`ata-raid.c:138`, `ata-raid.h:37`). `meta->raid_number`
  is `u_int8_t` (0..255, `ata-raid.h:510`). For `raid_number >= 16`,
  `array + raid_number >= 16` stores a `kmalloc`'d heap pointer into BSS past
  `ata_raid_arrays[]`.

- **BUG 2 — `configs[]` OOB read** (`ata-raid.c:2818-2822`):
  `raid_entry = meta->raid_number` (0..255); `meta->configs[raid_entry]` where
  `configs` is an array of **30** (`ata-raid.h:508`). `raid_entry >= 30` reads
  past the configs union array.

- **BUG 3 — `raid->disks[]` heap OOB write** (`ata-raid.c:2866-2869`):
  ```
  raid->disks[meta->disk_number].dev     = parent;                       // :2866
  raid->disks[meta->disk_number].sectors = meta->configs[...].disk.disk_sectors; // :2867
  raid->disks[meta->disk_number].flags   = ...;                          // :2869
  ```
  `disks[MAX_DISKS]` with `MAX_DISKS = 16` (`ata-raid.h:39,108`);
  `meta->disk_number` is `u_int8_t` (0..255, `ata-raid.h:509`). `disk_number
  >= 16` overwrites the trailing `ar_softc` fields (`toggle`, `rebuild_lba`,
  `lock`, `disk`, `devstat`, `cdev`, `pid`, `ata-raid.h:109-115`) and then
  past the `kmalloc(sizeof(ar_softc))` allocation into adjacent kernel heap.
  The `sectors` field is attacker-controlled (from disk metadata).

## Primitive characterization (harness, real struct layouts)
`overflow_harness.c` reproduces the in-kernel indexing using faithful struct
copies. On LP64: `sizeof(struct ar_disk)=48`, `disks[16]` (first OOB) at offset
880. For crafted `raid_number=200, disk_number=200`:

| site | effect |
|---|---|
| BUG 1 | 16 BSS-OOB pointer stores into `ata_raid_arrays[16..]`, worst idx=215 → **1600 B** into adjacent BSS |
| BUG 2 | `configs[200]` read (configs[30]) → **OOB read (CWE-125)** |
| BUG 3 | 240 OOB `ar_disk` entries × 48 B = **11520 B** heap spill (~436 B inside `ar_softc`, ~8444 B into adjacent heap) |

With the fix (reject `raid_number>=MAX_ARRAYS || disk_number>=MAX_DISKS`), all
three sites drop to 0 (loop never runs). (Full output in `harness_output.txt`.)

## Reachability & threat model
- **Driver present in default GENERIC:** `device nataraid` (`X86_64_GENERIC:83`); `ata-raid.c` is built when `nataraid` is set. The LSI v2 parser is tried for **every** ATA subdisk at `ata-raid.c:1480`.
- **Vendor gate passes on this guest:** the guest exposes an Intel PIIX4 IDE controller (`atapci0@pci0:0:1:1`, chip `0x70108086`).
- **Trigger context:** `ata_raid_subdisk_attach` → `ata_raid_read_metadata` → `ata_raid_lsiv2_read_meta` fires at device-probe/attach time (boot or hot-plug of an ATA/SATA disk), not from any userspace syscall.
- **Realistic threat model:** a malicious SATA/IDE disk (USB-attached SATA, hot-plug, or crafted VM disk image) carrying LSI v2 metadata with out-of-range indices triggers the overflow automatically at attach, no user privilege required. This is a valid default-config attack surface.

## Live-trigger attempt (why no in-kernel panic is shown)
`build_meta.py` builds a crafted raw disk image (`crafted_lsiv2.img`) carrying
a valid LSI v2 metadata block (magic `"$XIDE$"`, RAID0 type) with
`raid_number=200, disk_number=200` at the last sector. Attaching it to the
guest is blocked by the same QEMU/loader artifact as DF-1171: the DragonFly
loader produces no serial output for ~80–90 s on *any* extra hard disk (a
blank all-zero control disk hangs identically), so the kernel probe path never
runs. This is an environment blocker for the live demo, not a property of the
bug. On real Intel AHCI/SATA hardware the overflow would fire and, on default
GENERIC with INVARIANTS, panic in `ata_raid_attach`.

## Exploit chain (uid0 assessment)
**uid0 escalation is NOT realistic from this primitive.** This is a valid hard
stop, for a structural reason (identical to DF-1171):
- The overflow fires **synchronously inside `ata_raid_subdisk_attach`** during
  device probe (boot/hot-plug). No userspace process is concurrently running in
  a position to **groom the slab** so a chosen victim object lands adjacent to
  the freshly `kmalloc(sizeof(ar_softc))` allocation.
- The attacker controls the on-disk metadata *content* (notably
  `disks[].sectors`) and the *index* (which BSS/heap word is hit), but has **no
  influence over the slab layout** at attach time, so the spill is *blind*.
- BUG 3 first lands inside `ar_softc` (`toggle`/`rebuild_lba`/`lock`/`disk`/
  `devstat`/`cdev`/`pid`); on default GENERIC (INVARIANTS ON) the subsequent
  `ata_raid_attach` use of the corrupted `lock`/`disk`/`devstat` reliably
  **panics** before any privilege-meaningful operation. BUG 1's BSS pointer
  writes are even less convertible (the pointer value is a heap address the
  attacker does not shape).

The realistic ceiling is therefore **reliable kernel panic / local DoS from a
malicious disk**, not uid0.

## PoC changes
Authored from scratch. Deliverables: `overflow_harness.c`, `build_meta.py`,
`crafted_lsiv2.img` (reference artifact), `fix.diff`, `build.sh`, `run.sh`,
`VERDICT.md`, `manifest.json`, `env.txt`, `harness_output.txt`,
`fix_validation.txt`, `fix_build.log`.

## Recommended fix
`fix.diff` inserts, right after the `"$XIDE$"` magic check and before the
conversion loop, a range rejection:
```c
if (meta->raid_number >= MAX_ARRAYS || meta->disk_number >= MAX_DISKS) {
	if (testing || bootverbose)
	    device_printf(parent,
		"LSI (v2) metadata raid_number %u / disk_number %u out of range\n",
		meta->raid_number, meta->disk_number);
	goto lsiv2_out;
}
```
With `raid_number < MAX_ARRAYS(16)`, BUG 1 (`raidp[array+raid_number]`) and BUG 2
(`configs[raid_entry=raid_number]`, 16 < 30) are both in-bounds; with
`disk_number < MAX_DISKS(16)`, BUG 3 (`disks[disk_number]`) is in-bounds. The
secondary `conf_entry = (configs[raid_entry].config_offset>>4) + disk_number - 1`
is then bounded ≤ 15+15-1 = 29 < 30 for valid inputs. This **matches the finding
proposal** ("validate the indices").

## Fix validation (Phase 8)
- `fix.diff` applies cleanly (`git apply --check` OK; Hunk #1 @2795).
- Combined single-fix kernel build (DF-1157 + DF-1172, warm obj): `make -j6 nativekernel KERNCONF=X86_64_GENERIC` → **NK_DONE rc=0**; `ata-raid.o` compiled clean under `-Werror`.
- Patched kernel installed over `/boot/kernel/kernel`; booted `#1: Fri Jul 17 06:10:36 UTC 2026`. Rejection-guard string `"raid_number %u / disk_number %u out of range"` is **present in the booted kernel** (`strings | grep -c` → 1).
- `fix_status: not_testable` for runtime: the live trigger requires an ATA disk the loader cannot add on this guest (see above). Validated at apply + compile + boot + harness level: the range rejection provably prevents all three OOB indexings.

## Kernel references (confirmed)
- `sys/dev/disk/nata/ata-raid.c:2770` — `ata_raid_lsiv2_read_meta` entry
- `sys/dev/disk/nata/ata-raid.c:2802-2807` — BUG 1 (`raidp[array+raid_number]` BSS OOB)
- `sys/dev/disk/nata/ata-raid.c:2818-2822` — BUG 2 (`configs[raid_entry]` OOB read)
- `sys/dev/disk/nata/ata-raid.c:2866-2869` — BUG 3 (`raid->disks[disk_number]` heap OOB)
- `sys/dev/disk/nata/ata-raid.c:138` — `static struct ar_softc *ata_raid_arrays[MAX_ARRAYS]`
- `sys/dev/disk/nata/ata-raid.h:37` — `MAX_ARRAYS 16`
- `sys/dev/disk/nata/ata-raid.h:39` — `MAX_DISKS 16`
- `sys/dev/disk/nata/ata-raid.h:108` — `disks[MAX_DISKS]`
- `sys/dev/disk/nata/ata-raid.h:459-513` — `struct lsiv2_raid_conf` (configs[30], disk_number/raid_number u8)
- `sys/dev/disk/nata/ata-raid.c:1480` — tried for every ATA subdisk
- `sys/config/X86_64_GENERIC:83` — `device nataraid`
