# DF-5583 VERDICT — REPRODUCED (boot-time kernel NULL-deref panic; fix validated)

**Finding:** `acpi_sci_test()` (sys/platform/pc64/acpica/acpi_fadt.c:183-217)
passes `register_int()`'s return value — which is **NULL** when the exclusive
registration collides with an existing handler (sys/kern/kern_intr.c:276-285,
`goto done` with `rec == NULL`) — straight into `unregister_int()`
(acpi_fadt.c:205), which dereferences it unconditionally
(sys/kern/kern_intr.c:425, `intr = ((intrec_t)id)->intr;`).

**Attacker input:** the FADT `SciInterrupt` field (offset 46) — firmware
table content, delivered via the proven planted-ACPI rig (loader kenv
`hint.acpi.0.rsdp`, accepted with no validation, acpi_sdt.c:71). The value is
bounds-checked only against the legacy-irq map (`machintr_legacy_intr_find`,
acpi_fadt.c:226; both ICU and IOAPIC implementations range-check the number
but know nothing about *registered handlers*), so any irq that is already
exclusively owned when acpi0 attaches reaches `register_int(INTR_EXCL|...)`
and fails.

**Collision provider used in the PoC:** the 8254 clock handler `"clk"` on
IRQ0, registered `INTR_EXCL|INTR_CLOCK` by `i8254_intr_initclock()`
(sys/platform/pc64/isa/clock.c:1289-1298) at `SI_BOOT2_CLOCKREG`
(kern_cputimer.c:466 `cputimer_intr_initclocks`) — i.e. *before* acpi0
attaches at `SI_SUB_CONFIGURE`, where `AcpiOsInstallInterruptHandler`
(sys/dev/acpica/Osd/OsdInterrupt.c:69) calls `acpi_sci_config()`. Any boot
where the i8254 is the interrupt cputimer (no LAPIC timer: PIC/ICU systems,
`hw.lapic_timer_enable=0`, many embedded/older platforms) has this precondition
naturally; the rig reproduces it deterministically on the QEMU guest.

## How it was reproduced (this pack)

1. **Control** (`fadt_control_sci9.bin`, SCI_INT=9, everything else identical):
   planted chain accepted (`ACPI SDT: RSDP in kenv hint`; ACPICA core also
   follows it — `ACPI: RSDP 0x0000000040000000`), `IOAPIC: testing 8254
   interrupt delivery... success`, `vmstat -i` shows `clk` live on IRQ0,
   `ACPI FADT: SCI testing ... SCI select level/high` — the dummy handler
   registers and unregisters cleanly on the *free* irq 9. Guest reaches full
   userspace + ssh. (run.control_boot.log, run.control_vmstat.txt)

2. **Attack** (`fadt_attack_sci0.bin`, only field 46 changed 9→0):
   ```
   ACPI FADT: SCI testing interrupt mode ...
   ACPI FADT: SCI testing edge/high
   Fatal user address access from kernel mode from swapper at ffffffff80620f35
   Fatal trap 12: page fault while in kernel mode
   fault virtual address = 0x28
   fault code = supervisor read data, page not present
   Stopped at unregister_int+0x25: movl 0x28(%rdi),%r12d
   ```
   `0x28` is exactly `offsetof(struct intrec, intr)` (kern_intr.c:50-59:
   next, info, handler, argument, name = 5×8 bytes) and `rdi` is the NULL
   `sci_desc`. Guest sits in DDB (`db>`), ssh never answers —
   `reproduced`/`panic`. (run.attack_panic.log, panic.txt)

## Impact assessment

Boot-time kernel NULL-page read at a fixed offset; the loaded value is only
used as a comparison key in the reclist walk and range-checked afterwards, so
with page 0 unmapped (always on this platform, and DFly does not let userland
map page 0) the result is the **panic/DoS**, not controllable corruption.
Threat model: attacker-controlled firmware or boot-loader environment
(loader.conf / cloud image / physical DMA), the same model under which the
acpi_madt family findings (DF-5430..5433) were filed. Severity Low,
CWE-476, per family calibration (boot panics = Low; DF-5432's arbitrary-
physical MMIO was the Medium outlier).

## Fix validation (fix.diff, applied in guest /usr/src, kernel #1 Oct 8 04:41:58)

* `make -j6 nativekernel KERNCONF=X86_64_GENERIC`: rc=0, `-Werror` clean.
* **Attack blob on patched kernel**: boots to ssh; decisive lines
  `ACPI FADT: SCI testing edge/high` → `ACPI FADT: SCI irq 0 is not
  available, disable`; `clk` ticking at ~250 Hz in `vmstat -i`; no panic, no
  DDB. (run.fixedkernel_attack.txt)
* **Control blob on patched kernel**: `ACPI FADT: SCI select level/high` —
  the normal mode-testing path is unaffected. (run.fixedkernel_control.txt)
* Baseline (stock kernel, same blob): Fatal trap 12 @ 0x28 (run.attack_panic.log).

fix_status: **fixed** (fix_baseline_reproduced=true, fix_patched_reproduced=false
— the panic itself is gone on the patched kernel).

## Negative results recorded during this audit (so the clean parts have weight)

* FADT field reads are bounds-safe in this file: SciInterrupt (offset 46) is
  only read after `Header.Length >= ACPI_FADT_V1_SIZE` (128, acpi_fadt.c:113),
  BootFlags (offset 109) only after `Length >= ACPI_FADT_V3_SIZE && Revision >= 3`
  (acpi_fadt.c:120-121). No V2..V6-specific field is consumed here at all.
* kgetenv/kfreeenv pairing on both trigger/polarity paths is balanced
  (acpi_fadt.c:134-162); no leak.
* The `machintr_legacy_intr_find(acpi_sci_irq, ...)`-before-`< 0`-check order
  wart (acpi_fadt.c:226-235) is cosmetic only: both ICU
  (icu_abi.c:664) and IOAPIC (ioapic_abi.c:967) implementations reject
  negative/out-of-range irq numbers, so a disabled SCI just logs a spurious
  "irq -1 is invalid" line.
* A short-FADT (`Length < 36`) KKASSERT panic candidate at acpi_fadt.c:103
  was **refuted live**: `sdt_search()` maps each candidate via
  `sdt_sdth_map()`, which returns NULL for Length<36 (acpi_sdt.c:147-148),
  so `sdt_search` skips such tables — `fadt_probe` prints "can't locate FADT"
  and the KKASSERT is unreachable from static table content.
* Reset-register and PM1a/b plumbing named in the task brief live in the
  ACPICA core (contrib scope, divergence-note only); this DFly file consumes
  only SciInterrupt, BootFlags, Revision and Length.
* Side observation (not this file, no finding filed): with `lapic_enable` on
  but no MADT, `start_all_aps()` spins forever in the
  `CPUMASK_CMPMASKNEQ(smp_lapic_mask, smp_startup_mask)` wait
  (mp_machdep.c:595-600) — a crafted-table boot hang in mp_machdep.c, noted
  for the acpi_madt/mp_machdep owners.

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