# DF-1171 — Intel MatrixRAID parser OOB heap write on `ar_softc` via unclamped `map->total_disks`

## Verdict

**REPRODUCED (primitive confirmed at source + harness level).** The bug is a
genuine unbounded heap overflow in `ata_raid_intel_read_meta()` — confirmed by
line-by-line source trace and a faithful userspace harness built from the real
kernel struct definitions (`struct ar_softc`, `struct ar_disk`,
`struct intel_raid_mapping`). The driver is compiled into the **default
`X86_64_GENERIC` kernel** and the Intel-vendor gating path is present on this
guest's PIIX4 IDE controller, but the live in-kernel trigger is blocked by a
QEMU/loader artifact (the DragonFly loader hangs on *any* extra hard disk before
the kernel boots — see "Live-trigger attempt" below). 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_intel_read_meta()` parses on-disk Intel MatrixRAID metadata read from
the last sectors of an ATA disk (`INTEL_LBA = total_secs - 3`,
`sys/dev/disk/nata/ata-raid.h:293`). After validating the magic string and
checksum, it stores the attacker-supplied disk count without bounds checking:

```
ata-raid.c:2230    raid->total_disks = map->total_disks;     // map->total_disks is u_int8_t -> up to 255
```

`map->total_disks` is a `u_int8_t` (`sys/dev/disk/nata/ata-raid.h:351`), so it
can be up to 255. `raid->disks[]` is declared as `disks[MAX_DISKS]` with
`MAX_DISKS = 16` (`sys/dev/disk/nata/ata-raid.h:39,108`). The subsequent loop
writes `raid->disks[disk].*` for `disk < raid->total_disks`:

```
ata-raid.c:2243-2263
    for (disk = 0; disk < raid->total_disks; disk++) {
        ...
        raid->disks[disk].dev     = NULL;                       // :2246
        bcopy(meta->disk[disk_idx].serial,
              raid->disks[disk].serial, ...);                   // :2247-2249  (16 attacker bytes)
        raid->disks[disk].sectors = meta->disk[disk_idx].sectors; // :2250-2251 (attacker u32)
        raid->disks[disk].flags   = ...;                        // :2252-2262
    }
```

For any `map->total_disks > 16`, index `disk` runs past `disks[15]` and writes
into the trailing fields of `struct ar_softc` — `toggle`, `rebuild_lba`, `lock`,
`disk`, `devstat`, `cdev`, `pid` (`ata-raid.h:109-115`) — and then past the end
of the `kmalloc(sizeof(struct ar_softc))` allocation (`ata-raid.c:2171`) into
adjacent kernel heap.

## Primitive characterization (harness, real struct layouts)

`overflow_harness.c` reproduces the exact in-kernel data flow using faithful
copies of the kernel structs. On LP64:

| quantity | value |
|---|---|
| `sizeof(struct ar_disk)` | **48** (8 dev + 16 serial + 8 sectors + 8 last_lba + 4 flags + 4 pad) |
| `sizeof(struct ar_softc)` | 1328 |
| `disks[0]`  offset | 112 |
| `disks[16]` (first OOB) offset | 880 == `ar_softc.toggle` |
| attacker-controlled bytes per OOB entry | 24 of 48 (serial[16] + sectors[8]) |

For the maximal attacker value `map->total_disks = 255`:

```
OOB entries written past disks[15]: 239
OOB bytes written past disks[15]:   11472  ( = 239 * 48 )
  of which INSIDE ar_softc struct : 448 bytes  (toggle/rebuild_lba/lock/disk/devstat/cdev/pid)
  of which PAST the struct boundary: 11024 bytes (into adjacent kernel heap)
```

The finding's "~11,424 bytes" estimate is correct to within the per-entry
rounding (exact is 11,472). The "32 of 48 attacker-controlled bytes" claim is
essentially correct (the loop also NULLs `dev` and zeroes `last_lba`/`flags`,
which the attacker does not shape — so attacker-shaped content is 24 bytes, with
the remaining writes being fixed zero/NULL).

## Reachability & threat model

- **Driver present in default GENERIC:** `device nataraid` is in
  `sys/config/X86_64_GENERIC:83`; `ata-raid.c` is built when `nataraid` is set
  (`sys/conf/files:143`). Confirmed live: `nm /boot/kernel/kernel.debug` shows
  `ata_raid_subdisk_attach` and `ata_raid_rw` in the running `#0` kernel.
- **Intel-vendor gating passes on this guest:** the guest exposes an Intel PIIX4
  IDE controller (`atapci0@pci0:0:1:1`, chip `0x70108086`, vendor `0x8086` ==
  `ATA_INTEL_ID`, `sys/dev/disk/nata/ata-pci.h:146`). Both gating checks in
  `ata_raid_read_metadata` pass for an `ad` device here:
  `device_get_devclass(GRANDPARENT(GRANDPARENT(subdisk))) == atapci_devclass`
  (ata-raid.c:1411,1414) and
  `pci_get_vendor(GRANDPARENT(device_get_parent(subdisk))) == ATA_INTEL_ID`
  (ata-raid.c:1415,1423). So a crafted ATA disk presented on this controller
  would dispatch into `ata_raid_intel_read_meta`.
- **Trigger context:** `ata_raid_subdisk_attach` → `ata_raid_read_metadata` is
  called from `ad_attach` when an ATA disk is attached (`ata-disk.c:151-152` →
  `ata-raid.c:4161`). This is a **device-probe/attach-time** path: it fires at
  boot or on hot-plug of an ATA/SATA disk, NOT in response to any userspace
  syscall.
- **Realistic threat model:** a malicious SATA/IDE disk (USB-attached SATA,
  hot-plug, or a crafted VM disk image) presented to a DragonFlyBSD host on an
  Intel ATA/AHCI controller triggers the overflow automatically at attach with
  no user privilege required. This is a valid, default-config attack surface.

## Live-trigger attempt (why no in-kernel panic is shown)

A crafted raw disk image was built (`build_meta.py`) carrying a valid Intel
MatrixRAID metadata block (correct magic `"Intel Raid ISM Cfg Sig. "`, verified
checksum, `map->total_disks = 32` → 16 OOB entries) placed at `INTEL_LBA`. It
was attached to the running guest three ways:

1. `-drive file=crafted.img,if=ide` on the existing PIIX4 controller
2. same, with a valid empty MBR signature to avoid wrong-device boot
3. `-device ich9-ahci,id=ahci0 -device ide-hd,bus=ahci0.0` (Intel ICH9 AHCI,
   the finding's named vector)

In **all three** cases the guest produced **zero bytes of serial output for
80–90 s** — a hang in SeaBIOS/the DragonFly loader *before* the kernel's console
is initialized. A control experiment with a **blank all-zero 64 MB IDE disk**
(no metadata at all) reproduced the identical hang, proving the hang is a
**QEMU/loader artifact triggered by adding *any* hard disk to this guest**, not
by the crafted metadata. Because the loader never hands control to the kernel,
the `nata` probe path that contains the bug never executes on the crafted disk.

This is a genuine environment blocker for the **live in-kernel demonstration**:
the bug is unambiguously real (source + harness), the driver and Intel-vendor
path are present in the running default kernel, but the live trigger cannot be
exercised on this guest because the loader stalls on extra hard disks. On real
Intel AHCI hardware (or a QEMU config whose loader tolerates the extra disk) the
overflow would fire and, on default GENERIC with INVARIANTS, panic when the
corrupted `lock`/`disk`/`devstat`/`cdev`/`pid` are subsequently used by
`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, not a bail-out:

- The overflow fires **synchronously inside `ata_raid_subdisk_attach`** during
  device probe — i.e. at boot or hot-plug time. No userspace process is
  concurrently running in a position to **groom the slab** so that a chosen
  victim object (`struct file`, `struct ucred`, an ops vector, …) lands adjacent
  to the freshly `kmalloc(sizeof(struct ar_softc))` allocation at
  `ata-raid.c:2171`.
- The attacker's **only** input is the on-disk metadata *content* (the
  `serial[16] + sectors[8]` bytes per overflow entry). The attacker has **no
  influence over the slab layout** at attach time. The overflow is therefore
  *blind*: attacker bytes are written into whatever the kernel happened to place
  adjacent to the `ar_softc`.
- The corruption first lands inside `ar_softc` itself (`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 — and there is no
  userspace grooming window to convert the blind spill into a controlled
  overwrite of a credential/ops-vector target.

This matches the "primitive fires at device-attach, attacker controls content but
not layout, no grooming window" case: the demonstrated and realistic ceiling is
**reliable kernel panic / local DoS** from a malicious disk, not uid0. Escalating
this to uid0 would require a separate heap-observation/leak primitive giving the
attacker slab-layout control at attach time, which this bug does not provide.

## PoC changes

Authored from scratch (the evidence-pack dir was empty). Deliverables:

- `overflow_harness.c` — userspace replica of the in-kernel overflow math using
  the real `struct ar_softc` / `struct ar_disk` / `struct intel_raid_mapping`
  layouts; prints the exact byte offsets and overflow sizes.
- `build_meta.py` — builds a crafted raw disk image with a valid Intel
  MatrixRAID metadata block (magic + verified checksum) whose `map->total_disks`
  is attacker-set, for the live-trigger attempts.

## Recommended fix

`fix.diff` clamps `raid->total_disks` to `MAX_DISKS` immediately after it is
read from `map->total_disks` at `ata-raid.c:2230`, with a `device_printf` for
visibility. This matches the finding markdown's proposed fix ("clamp total_disks
to MAX_DISKS"). The clamp is chosen over rejection-with-free because
`raidp[array]` at that point may be a pre-existing shared INTEL array reused for
the same `config_id` (see ata-raid.c:2178-2180); freeing it would risk a
use-after-free against the existing `kfree(raidp[array])` pattern. Clamping is
provably safe and directly prevents the `disks[]` overflow (the loop bound and
all later `raid->total_disks` uses see the clamped value).

## Fix validation (Phase 8)

- `fix.diff` applies cleanly (`git apply --check` OK; `patch -p1` in-guest OK).
- Single-fix kernel built from `with-src` warm obj: `make -j6 nativekernel
  KERNCONF=X86_64_GENERIC` → **rc=0** (`fix_build.log`, 35304 lines).
- Patched kernel installed over `/boot/kernel/kernel` (bare loader name) +
  `kernel.debug`; booted clean: `kern.version` =
  `DragonFly 6.5-DEVELOPMENT #1: Thu Jul 16 03:20:54 UTC 2026` (was `#0`);
  guest up and answering ssh.
- The fix string `"Intel RAID total_disks %u exceeds MAX_DISKS %d, clamping"` is
  present in the booted `/boot/kernel/kernel` — the clamp is compiled in.
- `fix_status: not_testable` for runtime behavior: the live trigger cannot run on
  this guest (loader hangs on any extra hard disk — see above), so a before/after
  panic comparison is not possible here. The fix is validated at the
  apply + compile + boot + source-trace level: with the clamp, `raid->total_disks`
  is capped at 16, so the loop at `ata-raid.c:2243` iterates `disk < 16` only and
  never indexes past `disks[15]` — the OOB write is eliminated. On real Intel
  AHCI hardware the patched kernel would no longer panic on the crafted disk.

## Kernel references (confirmed during verification)

- `sys/dev/disk/nata/ata-raid.c:2230` — unclamped store of `map->total_disks`
- `sys/dev/disk/nata/ata-raid.c:2243-2263` — OOB write loop into `disks[disk]`
- `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:351` — `map->total_disks` is `u_int8_t` (≤255)
- `sys/dev/disk/nata/ata-raid.c:2171` — `kmalloc(sizeof(struct ar_softc))` victim
- `sys/config/X86_64_GENERIC:83` — `device nataraid` (default GENERIC)
- `sys/dev/disk/nata/ata-pci.h:146` — `ATA_INTEL_ID 0x8086`
- `sys/dev/disk/nata/ata-raid.c:1411-1426` — gating checks (both pass on PIIX4)
