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

acpi_cst_cx_mwait_setup executes global C3-preamble side effects (cpu_mwait_cx_no_bmsts/no_bmarb) on the rejected-descriptor error path: an invalid _CST MWAIT hint still disables the system-wide C3 BM_STS check and bus-master arbitration discipline

Summary

The goto-done control flow runs the descriptor's global BM fixups even when error != 0 (the EOPNOTSUPP paths correctly return without side effects): a descriptor the kernel itself rejected globally degrades C3 preamble discipline on every CPU. Differential live repro with a CPUID-substitution helper: attack boot shows the C2 rejection x6 AND machdep.mwait.CX.c3_preamble drops 3->0; C1-only control stays 3; fixed kernel preserves rejection semantics with side effects gone. Requires MON/MWAIT-capable silicon (QEMU TCG hides them - helper used).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5620 Β· 16 files
FileTypeDescriptionSize
README.md β€” 3.5 KB ↓ raw
VERDICT.md β€” 3.6 KB ↓ raw
mwaitforce.c β€” 2.4 KB view raw
mkdsdt.py β€” 3.6 KB view raw
gen_cst.py β€” 5.9 KB view raw
dsdt_cst_badhint.dsl β€” 4.3 KB ↓ download
dsdt_cst_badhint.aml β€” 577 B ↓ download
run.control_sysctl.txt β€” 446 B view raw
run.attack_sysctl.txt β€” 800 B view raw
run.fixedkernel_badhint.txt β€” 284 B view raw
build.sh β€” 245 B view raw
run.sh β€” 288 B view raw
env.txt β€” 1.6 KB view raw
fix.diff β€” 961 B view raw
manifest.json β€” 1.7 KB view raw
verdict.json β€” 6.2 KB view raw

DF-5620 β€” rejected _CST MWAIT descriptors still mutate the global C3

preamble (cpu_mwait_cx_no_bmsts/bmarb on the error path)

Root cause

acpi_cst_cx_mwait_setup() (sys/platform/pc64/acpica/acpi_cstate_machdep.c) jumps to its done: block on the invalid-mwait-hint rejection path (:161-165) and the done: block unconditionally executes the descriptor's global side effects:

    if (!cpu_mwait_hint_valid(eax_hint)) {
        kprintf("C%d: invalid mwait hint 0x%08x\n", ...);
        error = EINVAL;
        goto done;                    /* <-- side effects still run */
    }
    ...
done:
    if ((cx->gas.AccessWidth & ACPI_GAS_INTEL_ARG1_BM_STS) == 0 && ...) {
        cpu_mwait_cx_no_bmsts();     /* clears cpu_mwait_c3_preamble BM_STS */
        ...
    }
    if (cx->type < ACPI_STATE_C3 && MWAIT_EAX_TO_CX(eax_hint) >= 3) {
        cpu_mwait_cx_no_bmsts();     /* again */
        cpu_mwait_cx_no_bmarb();     /* clears BM_ARB  (deep-mapping) */
    }

cpu_mwait_c3_preamble governs the C3 entry discipline of the machine-dependent MWAIT idle loop on every CPU (sys/platform/pc64/x86_64/machdep.c:3400-3409). A descriptor the kernel itself rejected as invalid therefore globally disables the C3 BM_STS check and bus-master-arbitration avoidance. The EOPNOTSUPP paths (no MONITOR / no MWAIT extensions, :149-153) already return without side effects β€” only the hint-rejection path is inconsistent.

Live proof (differential, single variable = the _CST content)

Planted _CST = valid C1 + C2 FixedHW/native (Intel FFH encoding, BitOffset=2) with invalid mwait hint 0x99999971 (MWAIT_EAX_TO_CX(0x71) = 8 >= CPU_MWAIT_CX_MAX). QEMU 7.2 never advertises CPUID.05H.ECX.EXT, so the mwaitforce.ko helper substitutes only the missing CPUID advertisement (DF-5155 "honest gap" methodology, same as DF-5483's estforce.ko); every subsequent instruction is the stock driver.

boot console machdep.mwait.CX.c3_preamble
control _CST (C1 only) + mwaitforce (no mwait lines) 3
attack _CST (C1 + badhint) + mwaitforce C2: invalid mwait hint 0x99999971 x6 0
attack _CST, fixed kernel (fix.diff) C2: invalid mwait hint 0x99999971 x6 3 (side effects gone)

Guest stays healthy in all three (the valid C1 is used: cx_supported: C1/1) β€” the rejected C2 never enters the idle table, yet on the stock kernel it rewired the global C3 preamble for the whole system.

Build

# module (guest): /tmp/mwf with KMOD= mwaitforce SRCS= mwaitforce.c
make SYSDIR=/usr/src/sys && cp mwaitforce.ko /boot/kernel/
echo mwaitforce_load=YES >> /boot/loader.conf
# blob: see DF-5619 pack (mkdsdt.py badhint + gen_cst.py -> cst_badhint.bin)

Run

vm.sh down
QEMU_EXTRA="-machine accel=tcg -cpu SandyBridge \
  -device loader,file=/tmp/opencode/cst_badhint.bin,addr=0x40000000,force-raw=on" vm.sh up 240
vm.sh run_root 'sysctl machdep.mwait.CX.c3_preamble; dmesg | grep -a "invalid mwait"'

Expected decisive evidence

  • run.attack_sysctl.txt β€” c3_preamble: 0 + the "invalid mwait hint" rejection lines (state rejected) on the stock kernel.
  • run.control_sysctl.txt β€” c3_preamble: 3 with the same module/CPU/boot.
  • run.fixedkernel_badhint.txt β€” c3_preamble: 3, rejection lines unchanged, on the kernel rebuilt with fix.diff.
VERDICT.md
↓ download raw

DF-5620 VERDICT

REPRODUCED (logic bug live-demonstrated with a differential control; fix validated by kernel rebuild).

What was run

The planted-ACPI rig (see DF-5619 pack) carrying _CST = valid C1 + a C2 FixedHardware/native descriptor (Intel FFH encoding, BitWidth=1 vendor, BitOffset=2 class, Address=0x99999971 = mwait hint arg0) whose hint is INVALID: MWAIT_EAX_TO_CX(0x99999971) = 8 >= CPU_MWAIT_CX_MAX. Because QEMU 7.2 never advertises CPUID.05H.ECX.EXT (leaf ECX = 0x2, INTBRK only, under both TCG and KVM) and also masks CPUID.01H:ECX.MON, the stock code would EOPNOTSUPP at acpi_cstate_machdep.c:149-153 before reaching the code under test. The mwaitforce.ko helper therefore ORs the missing advertisement bits into cpu_feature2/cpu_mwait_feature from a SYSINIT at SI_SUB_CONFIGURE/SI_ORDER_FIRST β€” after every CPU's identify_cpu() has written those globals (they are clobbered per-CPU at identcpu.c:645/649; a first attempt forcing at SI_SUB_DRIVERS was silently undone by the APs) and immediately before acpi0/cpu_cst probe. This is the same "substitute ONLY the missing CPUID advertisement" methodology as DF-5483's estforce.ko; everything after the substitution is the genuine stock driver. (cpu_mwait_cx_info[] subcnts remain zero because cpu_mwait_attach() bailed earlier β€” irrelevant here: the scenario IS the invalid-hint rejection; and the default idle hook stays the static "sti; hlt", so the late feature words cannot destabilize boot idle.)

Observations (exact)

  1. control (C1-only _CST + mwaitforce): full boot, machdep.mwait.CX.c3_preamble: 3, zero mwait console lines.
  2. attack (C1 + badhint C2 + mwaitforce): full boot, console shows C2 -> cpu specific C8 sub state 1 + C2: invalid mwait hint 0x99999971 for each CPU β€” the descriptor is REJECTED (cx_supported stays "C1/1") β€” but machdep.mwait.CX.c3_preamble: 0: the done: block ran cpu_mwait_cx_no_bmsts() twice (AccessWidth lacks the BM_STS bit; deep-mapping block) and cpu_mwait_cx_no_bmarb(), clearing BOTH the BM_STS and BM_ARB bits of the global C3 preamble that governs the machdep MWAIT idle loop's C3 entry discipline on every CPU. (run.attack_sysctl.txt)
  3. fixed kernel (fix.diff: rejection returns EINVAL before any side effects): same attack blob, same module β€” rejection lines unchanged, c3_preamble: 3. Side effects gone. (run.fixedkernel_badhint.txt)

Impact ceiling

Firmware/AML-controlled degradation of C3 power-management discipline system-wide (on pre-Core2 MP Intel platforms, entering C3 without the BM_STS check / arbitration disable risks the cache-coherency hazards the PIIX4-era quirks in this very driver exist to work around). No memory corruption, no user-triggerable path; the demonstrated artifact is the global preamble corruption itself. Severity Low.

Fix validation

fix.diff rewrites the error path: invalid hint -> return EINVAL (no side effects); the BM fixups run only on the success path; behavior for valid descriptors unchanged. Applied (with DF-5619's fix) in the guest's /usr/src, kernel rebuilt -Werror-clean, rebooted: attack blob β†’ c3_preamble: 3 (was 0), invalid mwait hint still printed (rejection semantics preserved), guest healthy. Baseline vs patched: the rejected descriptor no longer mutates global state. 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 applied in guest /usr/src (together with DF-5619's fix), make -j6 nativekernel KERNCONF=X86_64_GENERIC rc=0 -Werror-clean, installkernel, reboot. Attack blob (C1 + badhint C2) on the patched kernel: 'C2: invalid mwait hint 0x99999971' still printed per CPU (rejection semantics unchanged) and machdep.mwait.CX.c3_preamble stays 3 -- the rejected descriptor no longer mutates the global C3 preamble. Baseline stock kernel with the same blob: c3_preamble 0.

run.fixedkernel_badhint.txt (uname #1 Oct 8 20:54:15 + c3_preamble 3 + four 'invalid mwait hint' lines) versus run.attack_sysctl.txt (stock kernel c3_preamble 0 + rejection lines); diff in fix.diff.
↓ fix.diffper-fix-DF-5620

Confirmed kernel references

Detail

Evidence (decisive lines)

run.attack_sysctl.txt: 'machdep.mwait.CX.c3_preamble: 0' together with six 'C2: invalid mwait hint 0x99999971' rejection lines (state rejected, not installed) on the stock kernel; run.control_sysctl.txt: same boot config with C1-only _CST -> c3_preamble 3, zero mwait lines; run.fixedkernel_badhint.txt: kernel rebuilt with fix.diff -> c3_preamble 3, rejection lines unchanged.

PoC changes

No seeded PoC existed (pass-2 finding). Built mwaitforce.c after two failed strategies: (1) forcing cpu_mwait_feature at MOD_LOAD/SI_SUB_DRIVERS is silently undone because every AP's identify_cpu() rewrites the globals (identcpu.c:645/649) after SI_BOOT2_START_APS; (2) QEMU 7.2 has no -cpu mwait property and hides both CPUID.01H:ECX.MON and CPUID.05H:ECX.EXT under TCG and KVM. Final helper re-forces both words at SI_SUB_CONFIGURE/SI_ORDER_FIRST (after all CPU idents, immediately before acpi0 probe). The badhint _CST DSDT was generated with the DF-5619 pack's mkdsdt.py.

Verified recommended fix

In acpi_cst_cx_mwait_setup(), return EINVAL directly on invalid mwait hint (no goto) and run the cpu_mwait_cx_no_bmsts()/cpu_mwait_cx_no_bmarb() fixups only on the success path, matching the EOPNOTSUPP paths' behavior.

Verdict

REPRODUCED as a live differential: a planted _CST whose C2 FixedHardware/native descriptor carries an INVALID mwait hint (0x99999971 -> MWAIT_EAX_TO_CX=8 >= CPU_MWAIT_CX_MAX) is correctly REJECTED by the stock driver (console 'C2: invalid mwait hint 0x99999971' per CPU, cx_supported stays C1/1, guest healthy) -- yet on the stock kernel machdep.mwait.CX.c3_preamble drops 3 -> 0 because acpi_cst_cx_mwait_setup()'s done: block still executed cpu_mwait_cx_no_bmsts() twice and cpu_mwait_cx_no_bmarb() on the rejection path (acpi_cstate_machdep.c:161-190), globally disabling the C3 BM_STS check and bus-master-arbitration discipline of the machine-dependent MWAIT idle loop for the whole system (machdep.c:3400-3409). Identical boot with a C1-only _CST (same CPU, same helper, same rig): c3_preamble stays 3. Because this QEMU 7.2 never advertises CPUID.05H.ECX.EXT (and masks CPUID.01H:ECX.MON), the mwaitforce.ko helper substituted only those missing CPUID advertisement bits from a SYSINIT at SI_SUB_CONFIGURE/SI_ORDER_FIRST (after every CPU's identify_cpu() global write, before acpi probe) -- DF-5483-estforce 'honest gap' methodology; all subsequent instructions are the stock driver. With fix.diff (rejection returns EINVAL before any side effects) on a rebuilt -Werror-clean kernel, the same attack boots with the rejection lines unchanged and c3_preamble stays 3. Ceiling: firmware/AML-driven global degradation of C3 entry discipline (on pre-Core2 MP Intel: C3 entered without BM checks -- the coherency hazard class the PIIX4 quirks in this driver exist for); no memory corruption, no user-triggerable path; severity Low, demonstrated artifact is the preamble corruption itself (impact recorded 'none' among {panic,dos,leak,uid0} since no crash/leak materializes from it on this guest).