# DF-5502 — ioapic.c negative-GSI chain (pass-2 verify rig)

**Finding:** `ioapic_add()` performs zero validation of the firmware-supplied
GSI base / pin count; a crafted ACPI MADT IO_APIC entry with
`GlobalIrqBase >= 0x80000000` (u32 → int wrap) plants a **negative GSI space**
that drives `ioapic_gsi_setup()` into an OOB read of
`ioapic_conf.ioc_intsrc[]` (ioapic.c:372) and forwards **negative IRQs** to
`ioapic_set_legacy_irqmap()` → OOB writes to `ioapic_irqmaps[]` /
`ioapic_irqs[]` on production (no-INVARIANTS) kernels, and a
sentinel-aliasing CONFORM panic otherwise.

## Rig (same injection model as the accepted DF-5431 rig)

* `gen_madt.py <scenario> <out.bin>` — crafts RSDP→RSDT→MADT at 0x40000000
  plus, at +0x1000, a RAM-backed fake IO-APIC "register window" (select@+0x00,
  data@+0x10) whose VER preload makes `npin` attacker-chosen.
* guest `hint.acpi.0.rsdp=0x40000000` in /boot/loader.conf (hinted RSDP is
  accepted with no signature/checksum validation, acpi_sdt.c:71).
* `QEMU_EXTRA="-device loader,file=<blob>,addr=0x40000000,force-raw=on"`.
* `run_scenario.sh <scenario>` appends the hint, boots, captures serial.

### Scenarios
* `control`  — 2 CPUs + real IOAPIC base 0 (must configure ioapic cleanly;
  boot later stops in ACPICA because the minimal table set has no DSDT — rig
  artifact, happens *after* the ioapic stage in every run).
* `negpanic` — adds a RAM IOAPIC covering GSI −1 only (GlobalIrqBase
  0xFFFFFFFF, npin=1): gsi −1 aliases the `int_gsi == -1` sentinel.
* `negwrite` — 16 INTSRC overrides kill every sentinel + RAM IOAPIC covering
  GSI[−24,−1] (GlobalIrqBase 0xFFFFFFE8, npin=24): default-branch
  `irq = gsi = -1` OOB read at ioapic.c:372 → negative IRQ forwarded.

## Build / run

```
python3 gen_madt.py control  /tmp/madt_control.bin     # on host
python3 gen_madt.py negpanic /tmp/madt_negpanic.bin
python3 gen_madt.py negwrite /tmp/madt_negwrite.bin
sh run_scenario.sh negpanic     # stock INVARIANTS kernel → panic :809/:812
sh run_scenario.sh negwrite     # stock INVARIANTS kernel → panic :812
# production-behaviour kernel (stock GENERIC ships WITHOUT INVARIANTS):
#   guest: cd /usr/src/sys/config; sed -e 's/^options INVARIANTS/#&/'
#         -e 's/^ident.*/ident X86_64_NOINV/' X86_64_GENERIC > X86_64_NOINV
#   guest: cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_NOINV
sh run_scenario.sh negwrite     # NOINV kernel → silent OOB writes
# fix validation:
#   guest: cd /usr/src && patch < fix.diff && make -j6 nativekernel ...
sh run_scenario.sh negwrite     # fixed kernel → rejected, no OOB, no panic
```

## Expected decisive output

* stock kernel + negpanic: `IOAPIC: idx 0, apic id 2, gsi base -1, npin 1`
  then `panic: assertion "trig == INTR_TRIGGER_EDGE || trig ==
  INTR_TRIGGER_LEVEL" failed in ioapic_set_legacy_irqmap ... ioapic_abi.c:809`
* stock kernel + negwrite: `IOAPIC: idx 0, apic id 2, gsi base -24, npin 24`
  then `panic: assertion "irq >= 0" failed in ioapic_set_legacy_irqmap
  ... ioapic_abi.c:812`
* NOINV kernel + negwrite: bootverbose `IOAPIC: irq -1 -> gsi -1 edge/high`
  printed from inside the OOB-write path (plus any other negative irq lines),
  ioapic stage completes with **no** panic → out-of-bounds writes executed.
* fixed kernel + negwrite: `ioapic_add: invalid I/O APIC gsi base -24,
  npin 24, ignored`, single ioapic configured, no negative-irq lines, no
  ioapic panic.
