# DF-5502 — VERDICT

## 1. Reproduced? YES — on three kernel configurations

All runs use the DF-5431 injection model: crafted RSDP→RSDT→MADT blob placed at
0x40000000 by `-device loader`, selected with loader kenv
`hint.acpi.0.rsdp=0x40000000` (hinted RSDP accepted with no signature or
checksum validation, sys/platform/pc64/acpica/acpi_sdt.c:71). A RAM page at
0x40001000 acts as a fake IO-APIC register window (select@+0x00, data@+0x10):
the ID readback in `ioapic_set_apic_id()` echoes, and the VER read returns the
preloaded dword, so **npin is attacker-chosen** (ioapic.c:735-738 caller side:
acpi_madt.c:733-738).

### Run matrix (full serial logs in this directory)

| log | kernel | result |
|---|---|---|
| run_control.log | stock GENERIC (INVARIANTS) | clean ioapic stage (46 legacy mappings); later stops in AcpiTbLoadNamespace — rig artifact (minimal table set has no DSDT), identical in healthy runs |
| run_negpanic.log | stock GENERIC | `IOAPIC: idx 0, apic id 2, gsi base -1, npin 1` → **panic: assertion "trig == INTR_TRIGGER_EDGE ..." ioapic_abi.c:809** |
| run_negwrite.log | stock GENERIC | `gsi base -24, npin 24` → **panic: assertion "irq >= 0" ioapic_abi.c:812** |
| run_negpanic_noinv.log | **X86_64_NOINV** (production behaviour; stock GENERIC ships without INVARIANTS) | `IOAPIC: irq 0 -> gsi -1 conform/conform` → **panic: ioapic_pin_prog: trig conform is not supported (ioapic.c:520)** |
| run_negwrite_noinv.log | X86_64_NOINV | **`IOAPIC: irq -1 -> gsi -1 edge/high` — the OOB-write path executed; NO panic**; ioapic stage completed ("legacy irq max 24"), SMP came up, boot continued (silent out-of-bounds writes) |
| run_negwrite_fixed.log | X86_64_NOINV + fix.diff | `ioapic_add: invalid I/O APIC gsi base -24, npin 24, ignored`; single legit ioapic; **no negative-irq line, no panic**; boot proceeds exactly like control |
| run_negpanic_fixed.log | X86_64_NOINV + fix.diff | `... gsi base -1, npin 1, ignored`; **no panic**; full normal legacy mapping (46 lines, identical to control) |

## 2. Root cause (path:line)

`ioapic_add(void *addr, int gsi_base, int npin)` (ioapic.c:229-267) validates
**nothing** about the firmware-supplied GSI base or pin count:

* `ACPI_MADT_IO_APIC.GlobalIrqBase` is a UINT32 handed straight to the `int`
  parameter (acpi_madt.c:738). A base ≥ 0x80000000 becomes a **negative GSI
  base**; `gsi_end = gsi_base + npin - 1` (ioapic.c:234) also overflows for
  large positive bases. The overlap scan (ioapic.c:235-246) compares against
  positive existing ranges and never fires for negative ones — the entry is
  accepted and sorted to the head of the list.
* `ioapic_config()` → `ioapic_setup()` (ioapic.c:553-554) then walks
  `ioapic_gsi_setup(info->io_gsi_base + i)` with negative GSIs:
  * **negpanic path:** the intsrc scan `gsi == int_src->int_gsi`
    (ioapic.c:358) aliases GSI −1 against the `int_gsi = -1` **sentinel**
    installed by ioapic_config itself (ioapic.c:97) and reads
    `int_trig`/`int_pola` from the zeroed struct = `INTR_TRIGGER_CONFORM`
    (bus.h:117) — forwarded via `ioapic_set_legacy_irqmap(0, -1, CONFORM,
    CONFORM)` (ioapic.c:396) into `ioapic_pin_prog()` which panics on CONFORM
    (ioapic.c:519-521). On INVARIANTS builds the KKASSERT at ioapic_abi.c:809
    fires first. Console proof: `irq 0 -> gsi -1 conform/conform`.
  * **negwrite path (memory corruption):** with the 16 sentinels overwritten
    by INTSRC overrides, GSI −1 falls to the default identity mapping:
    `irq = gsi` = −1 passes `if (irq < ISA_IRQ_CNT)` (ioapic.c:370-371 — no
    lower bound!) and reads `ioapic_conf.ioc_intsrc[-1].int_gsi`
    (**ioapic.c:372 — OOB read**: `ioc_intsrc` is at ioapic_conf+16, so index
    −1 lands on the high dword of `ioc_list.tqh_first`, a KVA pointer — always
    negative) → the `>= 0` "mapped elsewhere" guard fails → EDGE/HIGH →
    **`ioapic_set_legacy_irqmap(-1, -1, EDGE, HIGH)`** (ioapic.c:396) →
    ioapic_abi.c: `KKASSERT(irq >= 0)` (:812) is INVARIANTS-only and absent on
    production builds; `ioapic_is_legacy_irqmap_used(-1)` reads
    `ioapic_irqmaps[cpu][-1]` OOB (:780), then **OOB writes**
    `ioapic_irqmaps[cpuid][-1]` fields (:843, :849-853) and `ioapic_irqs[-1]`
    {.io_addr pointer, .io_idx, .io_flags} (:865-871). The bootverbose print
    at :855-860 — `IOAPIC: irq -1 -> gsi -1 edge/high` — is emitted from
    inside the OOB path and appears in run_negwrite_noinv.log:107.
  * For other negative GSIs g ∈ [−24,−2] the :372 OOB read sweeps the 260
    bytes below `ioapic_conf` (lapic/tsc variables at 0x818da520-0x818da53f on
    the audited link layout); any negative dword forwards irq=g the same way,
    and `cpuid = gsi % ncpus` (ioapic_abi.c:1180) goes negative for odd
    negative GSIs (e.g. −23 % 6 = −5 on the 6-CPU host), moving the writes to
    `ioapic_irqmaps[-5][...]` — 23 KB below the array, into foreign .bss.

## 3. Impact

Firmware/boot-vector (crafted ACPI tables — same accepted threat model as
DF-5431/DF-1042; no unprivileged in-guest path):

* Production kernels: silent kernel-memory OOB writes during boot
  (firmware-controlled negative offsets/row indices, semi-controlled values:
  small enums, GSI values, an MMIO KVA pointer) plus a deterministic
  production panic via the sentinel path (ioapic.c:520).
* INVARIANTS/debug kernels: two distinct assertion panics (ioapic_abi.c:809,
  :812).

Concrete damage on the audited link layout (nm -S, stock kernel):
`ioapic_irqs[-1]` writes land in the 16-byte gap after `ioapic_conf`
(0x818da610) and `ioapic_irqmaps[0][-1]` in the padding after
`ioapic_abi_legacy_irq_max` (0x818db228) — layout-dependent, a different
kernel config or link order places live objects there; the odd-negative-GSI
row variant hits arbitrary earlier .bss unconditionally.

Classification: impact=panic (deterministic hard stop, incl. the production
ioapic.c:520 panic), with the silent OOB write documented above — consistent
with DF-1042/DF-5351 precedent.

## 4. Exploit chain (firmware threat model)

crafted MADT → unvalidated ioapic_add(base=0xFFFFFFE8, npin=24@RAM-window) →
ioapic_gsi_setup(-1) → OOB read ioc_intsrc[-1] (negative KVA dword) →
ioapic_set_legacy_irqmap(irq=-1) → OOB writes ioapic_irqmaps[0][-1] +
ioapic_irqs[-1] → boot continues. Root escalation is not in scope of this
vector (pre-SMP boot phase, firmware-supplied values only); the corruption is
boot-time .bss damage whose blast radius depends on link layout.

## 5. Fix validation (fix.diff — applied in-guest, kernel rebuilt, re-run)

* Baseline (X86_64_NOINV, unfixed): negwrite → OOB print irq −1; negpanic →
  panic ioapic.c:520.
* Patched (X86_64_NOINV + fix.diff): both scenarios → `ioapic_add: invalid
  I/O APIC gsi base -24/-1, npin 24/1, ignored`; negative-irq line gone;
  no panic; legacy mapping byte-identical to the healthy control run
  (46 "IOAPIC: irq" lines).
* fix.diff also closes the containment hole in the overlap test (a new range
  fully swallowing an existing one previously passed — see DF-5503) and adds a
  defensive `gsi < 0` mask-and-return guard in ioapic_gsi_setup() ahead of the
  sentinel scan. `git apply --check` passes against the pristine tree.
* Guest returned to the clean `with-src` snapshot afterwards.

## 6. Honesty notes

* The stock-kernel run logs are the console tail (59 lines) captured by the
  rig script; each contains the complete decisive section (MADT entries, gsi
  base lines, panic line, DDB stop). NOINV/fixed logs are full boot.log copies.
* Every boot that survives the ioapic stage later stops in
  AcpiTbLoadNamespace (kernel page fault, DDB) because the minimal crafted
  table set has no DSDT — a rig artifact present in control and fixed runs
  alike; all ioapic evidence precedes it.
* One earlier negwrite attempt booted the wrong kernel (nativekernel does not
  install; fixed by running installkernel explicitly).

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