acpi_sci_test(): unchecked register_int() return passed to unregister_int() β kernel NULL-deref panic from crafted FADT SciInterrupt
Summary
register_int legally returns NULL on exclusive-registration collision and unregister_int derefs the descriptor unguarded: a FADT whose SciInterrupt names an irq already exclusively owned before acpi0 attaches (the 8254 'clk' on IRQ0 when the LAPIC timer is off) makes unregister_int read *(int*)0x28. Planted-ACPI rig with a cloned SeaBIOS table chain (--sci 0 + hw.lapic_timer_enable=0): 'Fatal trap 12, fault VA 0x28, Stopped at unregister_int+0x25' - deterministic boot panic; control (--sci 9) boots to full userspace. register_int-NULL check + kern_intr defense-in-depth fix validated on a rebuilt kernel (attack blob boots to ssh with 'SCI irq 0 is not available'). Fixed-offset NULL-page read, panic is the ceiling; crafted-firmware/loader threat model, DF-5430 tier.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5583 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| gen_fadt_sci.py | β | 8.0 KB | view raw | |
| mindSDT.dsl | β | 597 B | β download | |
| mindSDT.aml | β | 67 B | β download | |
| fadt_control_sci9.bin | β | 4.1 KB | β download | |
| fadt_attack_sci0.bin | β | 4.1 KB | β download | |
| fadt_short_len16.bin | β | 4.1 KB | β download | |
| build.sh | β | 134 B | view raw | |
| run.sh | β | 340 B | view raw | |
| run.control_boot.log | β | 29.0 KB | view raw | |
| run.control_vmstat.txt | β | 999 B | view raw | |
| run.attack_panic.log | β | 13.6 KB | view raw | |
| panic.txt | β | 448 B | view raw | |
| run.fixedkernel_attack.txt | β | 725 B | view raw | |
| run.fixedkernel_control.txt | β | 386 B | view raw | |
| env.txt | β | 271 B | view raw | |
| fix.diff | β | 1.4 KB | view raw | |
| VERDICT.md | β | 6.3 KB | β raw |
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)
-
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 -ishowsclklive 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) -
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),%r12d0x28is exactlyoffsetof(struct intrec, intr)(kern_intr.c:50-59: next, info, handler, argument, name = 5Γ8 bytes) andrdiis the NULLsci_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,-Werrorclean.- 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;clkticking at ~250 Hz invmstat -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 afterLength >= 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 viasdt_sdth_map(), which returns NULL for Length<36 (acpi_sdt.c:147-148), sosdt_searchskips such tables βfadt_probeprints "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_enableon but no MADT,start_all_aps()spins forever in theCPUMASK_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.
Fix verification
fixedfix.diff applied to guest /usr/src (patch -p0, hunks at acpi_fadt.c:198-205 and kern_intr.c:425), make -j6 nativekernel KERNCONF=X86_64_GENERIC rc=0 -Werror clean, installkernel, reboot. Attack blob (SciInterrupt=0) on the patched kernel: NO panic, boots to ssh, prints 'ACPI FADT: SCI irq 0 is not available, disable', clk interrupt alive at ~250Hz in vmstat -i. Control blob on the patched kernel: 'ACPI FADT: SCI select level/high' -- the normal SCI mode-testing path is unchanged. Baseline on the stock kernel with the same blob: Fatal trap 12 at 0x28 in unregister_int.
run.fixedkernel_attack.txt (uname #1 Oct 8 04:41:58 + graceful disable line + vmstat clk + root shell) and run.fixedkernel_control.txt (SCI select level/high), versus run.attack_panic.log (stock kernel Fatal trap 12); diff in fix.diff; build was -Werror clean (df5583_build.log on guest, env.txt).
Confirmed kernel references
- sys/platform/pc64/acpica/acpi_fadt.c:132
- sys/platform/pc64/acpica/acpi_fadt.c:198
- sys/platform/pc64/acpica/acpi_fadt.c:205
- sys/platform/pc64/acpica/acpi_fadt.c:226
- sys/kern/kern_intr.c:276
- sys/kern/kern_intr.c:284
- sys/kern/kern_intr.c:425
- sys/platform/pc64/isa/clock.c:1289
- sys/kern/kern_cputimer.c:466
- sys/dev/acpica/Osd/OsdInterrupt.c:69
- sys/platform/pc64/acpica/acpi_sdt.c:71
- sys/platform/pc64/acpica/OsdEnvironment.c:72
Detail
Evidence (decisive lines)
run.attack_panic.log + panic.txt: 'ACPI FADT: SCI testing edge/high' immediately followed by 'Fatal trap 12: page fault while in kernel mode', 'fault virtual address = 0x28' (= offsetof(struct intrec, intr)), 'Stopped at unregister_int+0x25: movl 0x28(%rdi),%r12d' with rdi==NULL sci_desc; run.control_boot.log + run.control_vmstat.txt: same rig with SCI_INT=9 boots, vmstat -i shows clk ticking on IRQ0 (the collision owner) and 'SCI select level/high'; run.fixedkernel_attack.txt: patched kernel boots to ssh, prints 'ACPI FADT: SCI irq 0 is not available, disable', no panic; run.fixedkernel_control.txt: patched kernel still does normal SCI mode selection.
PoC changes
No seeded PoC existed (pass-2 finding). Built gen_fadt_sci.py from the proven DF-5483 chain generator with three deltas: (1) MADT reduced to 1 LAPIC but keeping the real QEMU intsrc overrides so ioapic+MSI-X keep working, (2) loader.conf adds hw.lapic_timer_enable=0 so the i8254 (not the LAPIC timer) registers its INTR_EXCL clk handler on IRQ0 before acpi0 attaches (a first attempt with no MADT/PIC mode failed twice: mp_machdep.c:595 smp_lapic_mask wait hangs forever; a second attempt with hw.lapic_enable=0 died in 'panic: MSI-X is not setup yet' in pci_ht_map_msi), (3) the FADT SCI_INT field is the only difference between control and attack blobs. Minimal inert DSDT (mindSDT.dsl) compiled with the guest's iasl 20251212.
Verified recommended fix
In acpi_sci_test(), check register_int()'s return: on NULL print 'ACPI FADT: SCI irq %d is not available, disable', set acpi_sci_irq=-1 and return TRUE (stop mode testing, never unregister the NULL descriptor); defense-in-depth NULL guard at the top of unregister_int() (kern_intr.c).
Verdict
REPRODUCED on the stock guest kernel: a crafted FADT (planted via the proven hint.acpi.0.rsdp + QEMU-loader rig) whose SciInterrupt=0 names the legacy IRQ of the already-registered INTR_EXCL 8254 'clk' handler makes acpi_sci_test() call register_int(0, ..., INTR_EXCL|...) which returns NULL (kern_intr.c:276 goto done), and that NULL is passed to unregister_int() which dereferences it at kern_intr.c:425 -- boot dies with 'Fatal trap 12: page fault ... fault virtual address = 0x28 / Stopped at unregister_int+0x25: movl 0x28(%rdi),%r12d' and sits in DDB; ssh never answers. Identical rig with SciInterrupt=9 boots to full userspace (differential control). fix.diff (NULL-check in acpi_sci_test + defense-in-depth guard in unregister_int) applied in guest /usr/src, kernel rebuilt -Werror-clean and rebooted: the same attack blob now boots to ssh with the graceful 'ACPI FADT: SCI irq 0 is not available, disable' and clk alive; control blob still mode-selects normally. Impact ceiling is the boot-time kernel NULL-page read/panic (CWE-476, DoS) under the crafted-firmware/loader threat model -- firmware-consistent family calibration Low, same class tier as DF-5430/DF-5431.
No comments yet.