acpi_cst_md_cx_setup accepts SystemIO/SystemMemory C-state descriptors with no operational entry method: crafted _CST panics the kernel unattended at boot (KASSERT 'enter is not set' on INVARIANTS kernels; unconditional panic 'startup setup failed' + enter==NULL resident in the idle table on production kernels)
Summary
The md glue returns SUCCESS for any IO/memory descriptor without requiring the register allocated or an entry method: when acpi_bus_alloc_gas rejects the GAS the caller installs the state anyway - INVARIANTS kernels KASSERT-panic at once, production kernels accept the state and later call cx_next->enter==NULL from the idle loop. Companion hole: an all-rejected _CST still parses as success and the NO_C3-quirk path resurrects the rejected state so the unconditional startup panic fires. Reproduced live via the planted-ACPI vector (DF-5588's loader hint): nullenter blob -> 'panic: C2 enter is not set' before ssh; zerostate variant -> 'panic: C1 startup setup failed: 22' on a verified non-INVARIANTS kernel; C1-only control boots. Firmware-driven boot DoS ceiling; reject-unallocatable + ENXIO-on-empty fix validated on a rebuild (attack blobs boot with FADT C1 fallback).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5619 Β· 26 filesDF-5619 β _CST C-state descriptors with no operational entry method are
accepted: unattended boot panics (KASSERT on INVARIANTS kernels,
unconditional panic() on production kernels)
Root cause chain (all stock kernel code)
crafted _CST (DSDT, planted via hint.acpi.0.rsdp + QEMU loader) ββ acpi_cst_cx_probe_cst() sys/dev/acpica/acpi_cpu_cstate.c:481 ββ acpi_PkgRawGas(pkg,0,&gas) :581 ββ acpi_bus_alloc_gas() rejects SystemIO/addr0 (EINVAL, acpi.c:1280) β -> cx->res stays NULL ββ acpi_cst_cx_setup() -> acpi_cst_md_cx_setup() sys/platform/pc64/acpica/acpi_cstate_machdep.c:67 Intel branch : case SYSTEM_IO/SYSTEM_MEMORY -> break -> return 0 (:91-93) non-Intel : same, return 0 (:81-87) *** SUCCESS for a descriptor whose only possible entry method (bus-space io_enter, installed ONLY when res != NULL, acpi_cpu_cstate.c:601) does not exist -> enter == NULL *** ββ state installed: KASSERT(enter != NULL) (:612) INVARIANTS kernel -> panic "C2 enter is not set" [observed] production kernel -> state accepted; acpi_cst_idle() (:970) calls cx_next->enter == NULL
Companion hole (also fixed): a _CST whose every state is rejected still
parses as SUCCESS with cst_cx_count == 0 (or, with the NO_C3 quirk, has a
rejected state resurrected via cst_cx_count = cst_non_c3 + 1,
acpi_cpu_cstate.c:746-750) -> reaches acpi_cst_startup()'s UNCONDITIONAL
panic("C%d startup setup failed") (:842-844) β fires on production
kernels too. [observed: panic: C1 startup setup failed: 22]
Scenarios (same stock kernel; only the planted _CST differs)
| scenario | _CST content | stock kernel | fixed kernel |
|---|---|---|---|
| control | 1x C1 (halt), lat 1 | boots; cx_supported "C1/1" | boots; "C1/1" (unchanged) |
| nullenter | 1x C2 SystemIO/8/addr=0, lat 10 | panic: C2 enter is not set at boot |
boots; "no usable Cx state in _CST" + FADT C1 fallback |
| zerostate | 1x C2 FixedHW/8/off=0 (BitOffset!=2 => EINVAL) | panic: C1 startup setup failed: 22 at boot |
boots; same FADT fallback |
| (noinv) | nullenter on non-INVARIANTS kernel | state resident in idle table (cx_supported "C1/10"); no KASSERT | β |
| (noinv) | zerostate on non-INVARIANTS kernel | panic: C1 startup setup failed: 22 β unconditional panic, production shape |
β |
Prerequisites
- QEMU guest, DragonFly 6.5-DEVELOPMENT X86_64_GENERIC (this repo's rig)
- planted-ACPI rig (DF-5483/DF-5583 vector):
hint.acpi.0.rsdp=0x40000000in /boot/loader.conf + QEMU-device loader,file=<blob>,addr=0x40000000,force-raw=on - TCG CPU model
-machine accel=tcg -cpu SandyBridge(Intel vendor so the Intel md branch runs; the non-Intel branch has the identical hole) - iasl (guest /usr/sbin/iasl 20251212) for the DSDTs
Build
python3 mkdsdt.py control dsdt_cst_control.dsl python3 mkdsdt.py nullenter dsdt_cst_nullenter.dsl python3 mkdsdt.py zerostate dsdt_cst_zerostate.dsl scp dsdt_cst_*.dsl dfbsd-maxx:/tmp/ # via dfbsd-qemu/config vm.sh run_root 'cd /tmp && for f in dsdt_cst_control dsdt_cst_nullenter dsdt_cst_zerostate; do iasl $f.dsl; done' scp 'dfbsd-maxx:/tmp/dsdt_cst_*.aml' . python3 gen_cst.py dsdt_cst_control.aml /tmp/opencode/cst_control.bin python3 gen_cst.py dsdt_cst_nullenter.aml /tmp/opencode/cst_nullenter.bin python3 gen_cst.py dsdt_cst_zerostate.aml /tmp/opencode/cst_zerostate.bin vm.sh run_root 'printf "hint.acpi.0.rsdp=0x40000000\nboot_verbose=YES\n" >> /boot/loader.conf'
Run
# control (boots, driver engaged) vm.sh down; QEMU_EXTRA="-machine accel=tcg -cpu SandyBridge \ -device loader,file=/tmp/opencode/cst_control.bin,addr=0x40000000,force-raw=on" vm.sh up 240 vm.sh run_root 'sysctl hw.acpi.cpu0.cx_supported' # C1/1 # attack (unattended boot panic) vm.sh down; QEMU_EXTRA="-machine accel=tcg -cpu SandyBridge \ -device loader,file=/tmp/opencode/cst_nullenter.bin,addr=0x40000000,force-raw=on" vm.sh up 240 # -> TIMEOUT, vm.sh status == down, boot.log ends with: # cpu_cst0: <ACPI CPU C-State> [tentative] on cpu0 # panic: C2 enter is not set # (zerostate blob -> "panic: C1 startup setup failed: 22" in # acpi_cst_postattach)
Expected decisive evidence
- run.attack_panic.log / panic.txt β
panic: C2 enter is not setwith backtrace intoacpi_cst_cx_probe_cstduring cpu_cst0 attach. - run.attack_zerostate_panic.log β
C2: BitWidth(vendor) 8 ...bootverbose line (mwait_setup ran on the rejected descriptor) thenpanic: C1 startup setup failed: 22from acpi_cst_postattach. - run.noinv_zerostate_panic.log β same unconditional panic on a non-INVARIANTS kernel (KASSERT string absent from that kernel binary).
- run.control_sysctl.txt β same rig, C1-only _CST: full boot, C1/1.
- run.fixedkernel_*.txt β with fix.diff applied and kernel rebuilt, all attack blobs boot to ssh with the graceful "no usable Cx state in _CST" FADT fallback; control blob unchanged.
DF-5619 VERDICT
REPRODUCED (impact: kernel panic at boot, unattended; both INVARIANTS and non-INVARIANTS kernel shapes proven; fix validated by rebuild).
What was run
The proven planted-ACPI rig (DF-5483/DF-5583: hint.acpi.0.rsdp=0x40000000
+ QEMU -device loader blob carrying RSDP->RSDT->FADT->DSDT with per-CPU
_CST packages) on the stock DragonFly 6.5-DEVELOPMENT X86_64_GENERIC
guest, -machine accel=tcg -cpu SandyBridge (GenuineIntel, 6 vCPUs).
Four _CST variants; the kernel was never modified for the baseline runs.
Observations (exact)
- control (
_CST= 1x C1/halt): full boot to ssh,hw.acpi.cpu0.cx_supported: C1/1, 6x cpu_cst devices attach. Rig sane. - nullenter (
_CST= 1x C2, GAS SystemIO/8/0/addr=0, lat 10):acpi_bus_alloc_gas()rejects the GAS early (gas->Address == 0, sys/dev/acpica/acpi.c:1280) leavingcx->res == NULL;acpi_cst_md_cx_setup()still returns SUCCESS for it (sys/platform/pc64/acpica/acpi_cstate_machdep.c:91-93 β Intel branch; the identical hole exists in the non-Intel branch :81-87); the parser installs the state and hitsKASSERT(cx_ptr->enter != NULL, ...)(sys/dev/acpica/acpi_cpu_cstate.c:612) -> boot dies:panic: C2 enter is not set, backtraceacpi_cst_cx_probe_cst -> acpi_cst_attach -> device_doattach, guest sits in DDB, ssh never answers. (run.attack_panic.log, panic.txt) - zerostate (
_CST= 1x C2 FixedHW with BitOffset!=2 -> mwait_setup EINVAL, state rejected): becauseacpi_cst_cx_probe_cst()setscst_non_c3 = ifor the C2 BEFORE it is rejected (:564) and still returns success,acpi_cst_postattach()'s NO_C3-quirk path resurrects the rejected state (cst_cx_count = cst_non_c3 + 1, :746-750), andacpi_cst_startup()'s re-setup fails with the UNCONDITIONALpanic("C%d startup setup failed")(:842-844) ->panic: C1 startup setup failed: 22in acpi_cst_postattach β this is a plain panic(), no INVARIANTS required. (run.attack_zerostate_panic.log) - non-INVARIANTS kernel (snapshot
noinv-installed, kernel binary verified to lack the KASSERT string "enter is not set"): - nullenter: NO panic at attach (KASSERT compiled out); the enter-less state is RESIDENT in the live idle table (cx_supported: C1/10= index 0, trans_lat 10). The guest stayed up;cx_usageandcst_prev_sleepstayed frozen at their initial values, i.e. the idle loop had not completed a state-machine pass during the observation window (recorded in run.noinv_nullenter_survives.txt as an honest observation β the code pathcx_next->enter(cx_next)with enter==NULL (acpi_cpu_cstate.c:970) remains proven by trace, not by crash). - zerostate:panic: C1 startup setup failed: 22β identical unconditional production-kernel panic. (run.noinv_zerostate_panic.log)
Why this is a real finding
Every input is firmware/AML-controlled (_CST content), so the attacker
model is crafted ACPI tables β the same model as the DF-5430..5588 family
(planted via loader hint, malicious VM image, or compromised firmware). The
panics fire unattended at boot (before any user session), on every kernel
build: INVARIANTS kernels die in the parser, production kernels die in
acpi_cst_startup() (zerostate shape) or carry a NULL entry function in the
idle table (nullenter shape). No memory corruption: the ceiling is a
firmware-driven boot-time kernel panic (CWE-476-adjacent), family-calibrated
Low (same tier as DF-5583).
Fix validation
fix.diff (this pack): (a) acpi_cst_md_cx_setup() rejects non-C1
SystemIO/SystemMemory descriptors whose register resource was not allocated
(both vendor branches of sys/platform/pc64/acpica/acpi_cstate_machdep.c);
(b) acpi_cst_cx_probe_cst() fails with ENXIO when a _CST yields zero
usable states (sys/dev/acpica/acpi_cpu_cstate.c) so the driver falls back to
FADT C1. Combined with DF-5620's mwait fix, applied to the guest's /usr/src,
kernel rebuilt make -j6 nativekernel KERNCONF=X86_64_GENERIC rc=0,
-Werror-clean (322.91s), installed, rebooted:
- nullenter blob: boots to ssh, console
cpu_cst0: no usable Cx state in _CST+switching to FADT Cx mode,cx_supported: C1/0(FADT C1 halt). (run.fixedkernel_nullenter.txt) - zerostate blob: same graceful fallback, boots. (same file)
- control blob: unchanged,
C1/1, no fallback message. (run.fixedkernel_control.txt)
Baseline (stock kernel) vs patched: panic -> boots. Fix validated.
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 (md res==NULL rejection + parser zero-state ENXIO) applied in guest /usr/src together with DF-5620's mwait fix, make -j6 nativekernel rc=0 -Werror-clean (322.91s), installkernel, reboot. nullenter blob on patched kernel: NO panic, boots to ssh, prints 'cpu_cst0: no usable Cx state in _CST' + 'switching to FADT Cx mode', cx_supported C1/0 (FADT C1 halt idle). zerostate blob: same graceful fallback, boots. control blob: unchanged (C1/1, no fallback message). Baseline stock kernel with the same blobs: panics as above.
run.fixedkernel_nullenter.txt (uname #1 Oct 8 20:54:15 + 'no usable Cx state in _CST' + 'switching to FADT Cx mode' + cx_supported C1/0, for both nullenter and zerostate blobs) and run.fixedkernel_control.txt (C1/1 unchanged), versus run.attack_panic.log / run.attack_zerostate_panic.log (stock kernel panics); diff in fix.diff; build was -Werror clean (build.log).
Confirmed kernel references
- sys/platform/pc64/acpica/acpi_cstate_machdep.c:67
- sys/platform/pc64/acpica/acpi_cstate_machdep.c:81
- sys/platform/pc64/acpica/acpi_cstate_machdep.c:87
- sys/platform/pc64/acpica/acpi_cstate_machdep.c:90
- sys/platform/pc64/acpica/acpi_cstate_machdep.c:93
- sys/dev/acpica/acpi_cpu_cstate.c:564
- sys/dev/acpica/acpi_cpu_cstate.c:593
- sys/dev/acpica/acpi_cpu_cstate.c:609
- sys/dev/acpica/acpi_cpu_cstate.c:616
- sys/dev/acpica/acpi_cpu_cstate.c:746
- sys/dev/acpica/acpi_cpu_cstate.c:750
- sys/dev/acpica/acpi_cpu_cstate.c:842
- sys/dev/acpica/acpi_cpu_cstate.c:844
- sys/dev/acpica/acpi_cpu_cstate.c:970
- sys/dev/acpica/acpi.c:1280
Detail
Evidence (decisive lines)
run.attack_panic.log + panic.txt: 'panic: C2 enter is not set' with backtrace acpi_cst_cx_probe_cst -> acpi_cst_attach during cpu_cst0 attach; run.attack_zerostate_panic.log: bootverbose 'C2: BitWidth(vendor) 8 ...' then 'panic: C1 startup setup failed: 22' in acpi_cst_postattach; run.noinv_zerostate_panic.log + panic.noinv.txt: the same unconditional panic on the non-INVARIANTS kernel; run.noinv_nullenter_survives.txt: enter-less state resident ('C1/10') on the production kernel; run.control_sysctl.txt: same rig, C1-only _CST boots to ssh with C1/1; run.fixedkernel_nullenter.txt + run.fixedkernel_control.txt: with fix.diff, both attack blobs boot to ssh with 'no usable Cx state in _CST' + FADT C1 fallback, control unchanged.
PoC changes
No seeded PoC existed (pass-2 finding). Built gen_cst.py (RSDP->RSDT->FADT/DSDT/MADT chain, cloned from the proven DF-5483 generator) + mkdsdt.py (6-Processor DSDT with scenario _CST packages; iasl 20251212 on the guest; first DSL draft used commas between Processor() siblings and was rejected by iasl -- fixed to blank-line separation). Fixed-kernel build applied the combined DF-5619+DF-5620 diff via patch -p1 in the guest's /usr/src.
Verified recommended fix
acpi_cst_md_cx_setup: reject non-C1 SystemIO/SystemMemory descriptors whose register resource was not allocated (cx->res == NULL -> EINVAL, both vendor branches); acpi_cst_cx_probe_cst: fail with ENXIO when a _CST yields zero usable states so the driver falls back to FADT C1.
Verdict
REPRODUCED on the stock guest kernel: a crafted _CST (planted via the proven hint.acpi.0.rsdp + QEMU-loader rig) containing a C2 state whose SystemIO GAS has Address=0 is rejected by acpi_bus_alloc_gas (acpi.c:1280, res stays NULL) yet acpi_cst_md_cx_setup returns SUCCESS for it (acpi_cstate_machdep.c:91-93 Intel branch, identical hole in the non-Intel branch :81-87) even though the bus-space enter method is only installed when res != NULL -- the parser then hits KASSERT(cx_ptr->enter != NULL) (acpi_cpu_cstate.c:612) and boot dies with 'panic: C2 enter is not set' during the unattended cpu_cst0 attach; ssh never answers. A second crafted shape (a _CST whose single C2 FixedHW descriptor fails mwait validation, e.g. BitOffset != 2) exposes the companion hole: acpi_cst_cx_probe_cst still parses as success with the rejected state recorded in cst_non_c3, acpi_cst_postattach's NO_C3-quirk path resurrects cst_cx_count = cst_non_c3+1 (acpi_cpu_cstate.c:746-750), and acpi_cst_startup's UNCONDITIONAL panic("C%d startup setup failed") (:842-844) fires -- 'panic: C1 startup setup failed: 22' -- reproduced identically on a verified non-INVARIANTS kernel (noinv snapshot; KASSERT string absent from the binary), proving the production-kernel boot panic. On the non-INVARIANTS kernel the nullenter shape leaves the enter==NULL state RESIDENT in the live idle table (cx_supported 'C1/10') with the NULL cx_next->enter call at :970 proven by trace. Ceiling: firmware/AML-driven unattended boot-time kernel panic (CWE-476-adjacent DoS) under the crafted-firmware/loader threat model; family-calibrated Low (same tier as DF-5583). No memory corruption; no user-triggerable path without ACPI-table write access.
No comments yet.