# DF-4451 — heap OOB write in link_add_crs (and link_add_prs / srs_from_crs / route_irqs) from temporal divergence of _CRS/_PRS evaluations

File: `sys/dev/acpica/acpi_pci_link.c` (pass 2)

## What the bug is

`acpi_pci_link_attach()` sizes the `pl_links[]` heap array from a **counting
walk** of the link device's `_CRS` (`acpi_pci_link.c:465`), then runs
**populating walks** (`link_add_crs` at `:504`, `link_add_prs` at `:522`) that
index `pl_links[req->link_index]` with **no runtime bound check** — only a
`KASSERT` (`:235`) / `KKASSERT` (`:767`, `:896`), which are no-ops on
INVARIANTS-free production kernels (`sys/sys/systm.h:117-119`).

If the `_CRS` (or `_PRS`) method returns **a different resource count on a
later evaluation than on the counting evaluation**, the populate walks write
past the end of `pl_links`. The ACPI specification does not require methods to
be pure: side effects and stateful returns are legal AML, and on this system
`_CRS` of a PNP0C0F link device is evaluated at least **three times per boot by
three independent kernel consumers**:

1. `sys/dev/acpica/acpi.c:1908` — `acpi_parse_resources()` during ACPI child
   enumeration (evaluates `_CRS` via `acpi_resource.c`),
2. `sys/dev/acpica/acpi_pci_link.c:465` — counting walk,
3. `sys/dev/acpica/acpi_pci_link.c:504` — `link_add_crs` walk,
4. (later, at route/resume time) `sys/dev/acpica/acpi_pci_link.c:727` —
   `acpi_pci_link_srs_from_crs()` re-evaluates `_CRS` and walks it with the
   same unbounded `link`/`i` cursor (`:766-789` read side, `:893-913` write
   side `link->l_routed = TRUE`).

This is distinct from the known pass-1 findings: DF-2389 is a *static* count
mismatch between two different buffers (`_PRS` vs `_CRS`), DF-2390 is the DPF
*structural* inconsistency inside one buffer. Here a **single, well-formed,
DPF-free buffer** whose method simply returns different (equally valid) content
on successive evaluations drives the same out-of-bounds writes.

## Threat model

Firmware-supplied ACPI (crafted SSDT/DSDT in a bootable VM image — the same
bar as DF-2389/2390), or a buggy BIOS with a stateful `_CRS`. No unprivileged
runtime trigger is needed: the write fires during boot device attachment
(`device_doattach → acpi_pci_link_attach`).

## Reproduction on the audit guest (done)

* Inject an SSDT defining `\_SB.LNKX` (PNP0C0F) whose `_CRS` returns 1 IRQ
  descriptor on the first two evaluations (acpi.c:1908 pre-parse + counting
  walk) and 128 IRQ descriptors afterwards (the `link_add_crs` walk):
  `ssdt_lnkx_v2.asl` (ASL), `ssdt_lnkx_v2.aml` (compiled with the guest's
  `/usr/sbin/iasl`).
* Boot the guest with the table appended:
  `QEMU_EXTRA="-acptable file=ssdt_lnkx.aml" dfbsd-qemu/vm.sh up`
  (vm.sh passes `$QEMU_EXTRA` through to qemu-system-x86_64).

### Result on stock GENERIC (INVARIANTS kernel): panic — `panic.txt`

```
panic: link_add_crs: array boundary violation
link_add_crs() at link_add_crs+0x18a
AcpiWalkResourceBuffer() at AcpiWalkResourceBuffer+0x60
AcpiWalkResources() at AcpiWalkResources+0x9f
acpi_pci_link_attach() at acpi_pci_link_attach+0x3d1
device_doattach() at device_doattach+0x3af
Debugger("panic")
```

The kernel's own assertion catches `link_index >= pl_num_links` at
`acpi_pci_link.c:235` — proving the overrun is reached.

### Result on production-style kernel (INVARIANTS removed): the write proceeds

`X86_64_GENERIC` minus `options INVARIANTS` (config `NOINV4451`, see
`build-noinv.log`) — with the asserts compiled out (`sys/sys/systm.h:117`)
`link_add_crs` writes 128 `struct link` entries (~13 KB) into a
`sizeof(struct link)` (104-byte) allocation: `run-noinv-*.log`.

## Files

* `ssdt_lnkx_v1.asl` / `ssdt_lnkx_v2.asl` — SSDT sources (v1 = count walk
  divergence attempt that revealed the acpi.c:1908 pre-evaluation; v2 = the
  firing trigger)
* `ssdt_lnkx_v2.aml` — compiled SSDT (iasl 20251212, guest /usr/sbin/iasl)
* `build.sh` — compile the ASL on the guest, fetch the AML back
* `run.sh` — boot the guest with the table injected
* `panic-inv-assert-boot.log` — full serial log, INVARIANTS-kernel panic boot
* `panic.txt` — decisive excerpt
* `verbose-benign-boot.log` — control boot (threshold not yet reached): shows
  pci_link5 happily attaching 128 links when *both* walks see the same buffer
* `run-noinv-boot.log` — no-INVARIANTS kernel, same SSDT (raw OOB write)
* `fix.diff` — bounds guards for all four unbounded cursor walks
* `fix-validation.log` — patched no-INVARIANTS kernel, same SSDT: no panic
