# DF-4982 VERDICT — iir(4) GDT_IOCTL_EVENT double-fetch → OOB kernel read

**Status: untested at runtime (hardware-gated). Primitive and reachability proven by source trace.**

## The bug (path:line)

`iir_ioctl()` treats `ap->a_data` as a kernel pointer — on DragonFly it is
the **raw user pointer** (the same fact DF-2028 was filed on). For
`GDT_IOCTL_EVENT` with `erase == 0xff`:

1. `sys/dev/raid/iir/iir_ctrl.c:325-332` — the kernel decides the event
   data size from the *user-visible* `event_source` and **writes the size
   back into user memory**: `p->dvr.event_data.size = sizeof(...)`
   (one of 8/16/16/16 — all in-bounds).
2. `sys/dev/raid/iir/iir_ctrl.c:333-336` — it then passes `&p->dvr.event_data`
   (still user memory) to `gdt_store_event()`.
3. `sys/dev/raid/iir/iir.c:1875-1882` — `gdt_store_event()` re-reads
   `evt->size` from user memory three separate times:
   - `evt->size != 0` (iir.c:1877) — branch decision,
   - `memcmp(&ebuffer[elastidx].event_data.eu, &evt->eu, evt->size)`
     (iir.c:1878-1879) — **length is the freshly re-fetched value**,
   - `evt->size == 0` (iir.c:1880) — `strcmp(&ebuffer[...].event_string,
     &evt->event_string)` branch (unterminated 256-byte arrays on both
     sides if a prior store left no NUL → unbounded strcmp walk).

`eu` is a 16-byte packed union (`iir.h:517-543`; hand-checked: sync =
2+2+2+4+2+3+1 = 16, stream[16] = 16). `size` is `u_int16_t` → a racing
thread flipping `size` to 0xFFFF between step 1's store and step 3's load
makes `memcmp` compare up to **65,535 bytes against a 16-byte object** —
an OOB read of the static `ebuffer[GDT_MAX_EVENTS]` (256 × 294 = 73,504
bytes, `iir.c:145`) and of whatever module statics the linker placed
after it. memcmp stops at the first mismatch — the read of kernel byte
`eu[k]` happens for any k the attacker can reach with a matching prefix,
which is exactly what makes it an oracle.

## Why it is exploitable as a disclosure (not just a crash)

- Match outcome is observable: equality ⇒ **merge** (`same_count++` on
  `ebuffer[elastidx]`, iir.c:1883-1886); mismatch ⇒ **append** (elastidx
  advances, iir.c:1888-1898). A subsequent `erase == 0` read
  (`gdt_read_event`, iir.c:1911-1945) returns the last event with its
  `same_count` — MERGE vs APPEND is a 1-bit oracle per probe.
- The attacker fully controls the ring seed (any `event_source` 1-4,
  any `event_idx` via erase==0xff stores) so the memcmp branch preconditions
  (`ebuffer[elastidx].event_source == source && event_idx == idx`,
  `ebuffer[elastidx].event_data.size != 0`) are trivially satisfied.
- Byte k is learned with ≤256 probes (guess-and-check with learned
  prefix); the OOB side runs from offset 17 (past `eu`) through
  `severity`, `event_string[256]`, the rest of `ebuffer[]` (~73 KB) and
  adjacent module statics — potentially function/data pointers in the
  module's .data/.bss (KASLR-relevant on hardened deployments).

The same double-fetch also breaks the `size == 0` strcmp path
(iir.c:1880-1882): both strings can be non-NUL-terminated
(`event_string[256]` filled by a prior 0xff-store), so strcmp itself
walks out of bounds until it hits a zero byte.

## Threat model / gating (why Low, not higher)

- `/dev/iir` is created only by `iir_attach()` on an ICP vortex (0x1119)
  or Intel 0x600 RAID HBA — `iir_pci.c` probe table; rare hardware.
- Node perms: `UID_ROOT, GID_OPERATOR, S_IRUSR|S_IWUSR` (iir_ctrl.c:105-106)
  → root-only by default. The GID_OPERATOR choice shows operator-group
  access is the intended deployment; a devfs rule granting the group makes
  any operator-group member an attacker. With root-only perms the bug is
  nearly worthless (root reading kernel .bss).
- Driver-internal callers of `gdt_store_event()` (iir.c:1574, 1590, 1699,
  1842-1844) all pass `&gdt->sc_dvr` with driver-set sizes — unaffected.

## Why untested (hardware gate, verified on guest)

Reference guest (DragonFly 6.5-DEVELOPMENT x86_64, X86_64_GENERIC):
- `kldload iir` → "module already loaded or in kernel" (driver resident);
- `/dev/iir*` → No such file or directory;
- `pciconf -l` → only the 7 standard QEMU i440fx devices, no 0x1119/0x600
  RAID controller ⇒ `iir_pci_probe` never matches ⇒ `gdt_make_dev` never
  called ⇒ ioctl interface unreachable.
See `env.txt`. The PoC source in this pack is complete and runnable the
moment an HBA is present; no assertion in this finding rests on runtime
behavior.

## Fix validation

Not applicable — baseline could not be reproduced on the available guest
(no HBA), so there is no A/B possible. `fix.diff` is authored from the
line-accurate root cause: copy the struct in once (`copyin`), clamp
`size <= sizeof(eu)`, use the kernel copy, `copyout` on the way out.
That eliminates both the double-fetch and the unterminated-strcmp
variant (a clamped, kernel-owned copy has no race window).

## Bottom line

Certain OOB-read primitive (attacker-controlled compare length of up to
65535 against a 16-byte kernel object, three separate user-memory
re-fetches) with a workable equality-oracle exploitation strategy;
severity capped Low by double gating (HBA presence + 0600 root-only node).

## 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.
