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)
PoC verification
Evidence pack
findings/poc/DF-5620 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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: 3with the same module/CPU/boot. - run.fixedkernel_badhint.txt β
c3_preamble: 3, rejection lines unchanged, on the kernel rebuilt with fix.diff.
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)
- control (C1-only
_CST+ mwaitforce): full boot,machdep.mwait.CX.c3_preamble: 3, zero mwait console lines. - attack (C1 + badhint C2 + mwaitforce): full boot, console shows
C2 -> cpu specific C8 sub state 1+C2: invalid mwait hint 0x99999971for each CPU β the descriptor is REJECTED (cx_supported stays "C1/1") β butmachdep.mwait.CX.c3_preamble: 0: the done: block rancpu_mwait_cx_no_bmsts()twice (AccessWidth lacks the BM_STS bit; deep-mapping block) andcpu_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) - 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
fixedfix.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.
Confirmed kernel references
- sys/platform/pc64/acpica/acpi_cstate_machdep.c:146
- sys/platform/pc64/acpica/acpi_cstate_machdep.c:155
- sys/platform/pc64/acpica/acpi_cstate_machdep.c:161
- sys/platform/pc64/acpica/acpi_cstate_machdep.c:165
- sys/platform/pc64/acpica/acpi_cstate_machdep.c:171
- sys/platform/pc64/acpica/acpi_cstate_machdep.c:177
- sys/platform/pc64/acpica/acpi_cstate_machdep.c:190
- sys/platform/pc64/x86_64/machdep.c:3400
- sys/platform/pc64/x86_64/machdep.c:3409
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).
No comments yet.