β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5619

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5619 Β· 26 files
FileTypeDescriptionSize
README.md β€” 5.4 KB ↓ raw
VERDICT.md β€” 4.8 KB ↓ raw
mkdsdt.py β€” 3.6 KB view raw
gen_cst.py β€” 5.9 KB view raw
dsdt_cst_control.dsl β€” 2.7 KB ↓ download
dsdt_cst_nullenter.dsl β€” 2.7 KB ↓ download
dsdt_cst_zerostate.dsl β€” 2.7 KB ↓ download
dsdt_cst_control.aml β€” 361 B ↓ download
dsdt_cst_nullenter.aml β€” 373 B ↓ download
dsdt_cst_zerostate.aml β€” 373 B ↓ download
run.control_sysctl.txt β€” 152 B view raw
run.attack_panic.log β€” 19.7 KB view raw
panic.txt β€” 1.1 KB view raw
run.attack_zerostate_panic.log β€” 24.7 KB view raw
run.noinv_nullenter_survives.txt β€” 1.3 KB view raw
run.noinv_zerostate_panic.log β€” 24.5 KB view raw
panic.noinv.txt β€” 450 B view raw
run.fixedkernel_nullenter.txt β€” 222 B view raw
run.fixedkernel_control.txt β€” 34 B view raw
build.log β€” 900 B view raw
build.sh β€” 206 B view raw
run.sh β€” 237 B view raw
env.txt β€” 1.6 KB view raw
fix.diff β€” 1.6 KB view raw
manifest.json β€” 2.1 KB view raw
verdict.json β€” 7.2 KB view raw

DF-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=0x40000000 in /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 set with backtrace into acpi_cst_cx_probe_cst during cpu_cst0 attach.
  • run.attack_zerostate_panic.log β€” C2: BitWidth(vendor) 8 ... bootverbose line (mwait_setup ran on the rejected descriptor) then panic: C1 startup setup failed: 22 from 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.
VERDICT.md
↓ download raw

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)

  1. control (_CST = 1x C1/halt): full boot to ssh, hw.acpi.cpu0.cx_supported: C1/1, 6x cpu_cst devices attach. Rig sane.
  2. 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) leaving cx->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 hits KASSERT(cx_ptr->enter != NULL, ...) (sys/dev/acpica/acpi_cpu_cstate.c:612) -> boot dies: panic: C2 enter is not set, backtrace acpi_cst_cx_probe_cst -> acpi_cst_attach -> device_doattach, guest sits in DDB, ssh never answers. (run.attack_panic.log, panic.txt)
  3. zerostate (_CST = 1x C2 FixedHW with BitOffset!=2 -> mwait_setup EINVAL, state rejected): because acpi_cst_cx_probe_cst() sets cst_non_c3 = i for 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), and acpi_cst_startup()'s re-setup fails with the UNCONDITIONAL panic("C%d startup setup failed") (:842-844) -> panic: C1 startup setup failed: 22 in acpi_cst_postattach β€” this is a plain panic(), no INVARIANTS required. (run.attack_zerostate_panic.log)
  4. 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_usage and cst_prev_sleep stayed 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 path cx_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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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).
↓ fix.diffper-fix-DF-5619

Confirmed kernel references

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.