READ_PENDING_WAIT spins forever on a stuck FID/VID change-pending bit (unkillable kernel-context CPU spin)
Summary
Unbounded do/while over rdmsr at all five transition wait points - a stuck pending bit pegs a CPU in unkillable kernel context and wedges kldload; the driver's own comment names the hazard. Bounded-wait-with-DELAY fix (Linux checks once and returns -EIO).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4906 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.9 KB | β raw | |
| VERDICT.md | β | 1.7 KB | β raw | |
| env.txt | β | 314 B | view raw | |
| fix.diff | β | 1.2 KB | view raw | |
| manifest.json | β | 725 B | view raw | |
| verdict.json | β | 2.3 KB | view raw |
DF-4906 β READ_PENDING_WAIT spins forever on a stuck FID/VID "change pending" bit
File: sys/dev/powermng/powernow/powernow.c:84-87 (used at 263, 275, 293, 299, 306)
Severity: Low Β· Bucket: base:dev Β· Confidence: certain (code) / speculative (trigger)
Root cause
#define READ_PENDING_WAIT(status) \
do { \
(status) = rdmsr(MSR_AMDK7_FIDVID_STATUS); \
} while (PN8_STA_PENDING(status)) /* powernow.c:84-87 */
Every step of the fid/vid transition in k8_powernow_setperf() ends in
this unbounded do/while. If the CPU's FID/VID status "change pending"
bit (MSR 0xc0010042 bit 31) never clears β stuck due to silicon erratum,
a wedged PLL, or incomplete VM MSR emulation β the calling thread spins
at full tilt forever in kernel context; nothing preempts a
rdmsr-test loop that never sleeps and never checks signals.
The driver's own comment (powernow.c:235-238) acknowledges the hazard β
"need to ensure that the change pending bit isn't stuck" β and then
proceeds to use the unbounded macro at all five wait points
(powernow.c:263, 275, 293, 299, 306). Linux's powernow-k8 equivalent
checks the pending bit once and returns -EIO instead of spinning.
Consequences: one CPU pegged at 100% in kernel; the sysctl write thread is unkillable (never reaches a sleep/checkpoint); on the module-load path (powernow_init:392 β setperf) the boot kldload wedges. Local DoS only; requires root-triggered frequency change or the module-load path, plus a misbehaving CPU/firmware.
Verification status
untested β requires AMD K8 hardware with a stuck pending bit; the
audit guest is Intel/QEMU and the module's CPUID gate refuses
(MOD_LOAD error 2, env.txt).
Recommended fix
Bound the wait (e.g. loop with a deadline; on timeout abort the
transition with an error) β see fix.diff.
DF-4906 β VERDICT
status: untested Β· reproduced: 0 Β· impact: dos (unkillable kernel spin) Β· confidence: speculative (code certain)
Narrative
All five wait points in the fid/vid transition sequence end with the
READ_PENDING_WAIT macro (powernow.c:84-87) β an unbounded
do { rdmsr; } while (pending) loop. Nothing in the loop sleeps,
checks signals, or times out; a FID/VID status register whose
change-pending bit (bit 31) never clears therefore spins the calling
thread at 100% in kernel context forever:
- sysctl-write path (
machdep.powernow.frequency.target, powernow.c:323-335): unkillable thread, one CPU lost; - module-load path (powernow_init:392 β setperf): kldload wedges.
The developer comment at powernow.c:235-238 explicitly flags the stuck
pending bit as a case to guard, but the macro provides no guard. The
upstream Linux powernow-k8 driver checks the pending bit once and
propagates -EIO.
Verification
Not runtime-testable: needs an AMD K8 whose pending bit sticks (silicon
erratum / hostile VM MSR emulation). The Intel/QEMU audit guest refuses
the module at the CPUID gate (MOD_LOAD error 2, env.txt) before any
MSR access. Analysis pack only.
Fix
fix.diff: bounded loop (1e6 iterations, DELAY(1) between polls).
Applies cleanly. Callers already tolerate the macro not forcing an
error return; a full fix should also abort the transition on timeout.
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
not_testableFix validation not run (needs K8 hardware with stuck pending bit). fix.diff converts the macro to a bounded DELAY(1) loop and applies cleanly with patch(1).
fix.diff: checking file powernow.c / DF4906_APPLIES_CLEAN
Confirmed kernel references
Detail
Evidence (decisive lines)
README.md (macro + five call sites), env.txt (gate refusal), VERDICT.md
PoC changes
Analysis-only pack; triggering needs K8 hardware with a stuck pending bit.
Verified recommended fix
Bound READ_PENDING_WAIT (e.g. 1,000,000 iterations with DELAY(1)) and abort the transition on timeout.
Verdict
Code defect is certain: READ_PENDING_WAIT (powernow.c:84-87) is an unbounded do/while over rdmsr(0xc0010042) used at all five transition wait points (powernow.c:263,275,293,299,306); a stuck change-pending bit pegs one CPU at 100% in unkillable kernel context (sysctl writer never sleeps or checks signals; the module-load path wedges kldload). The driver comment at powernow.c:235-238 even names the hazard ('need to ensure that the change pending bit isn't stuck') yet every wait is unbounded; Linux powernow-k8 checks once and returns -EIO. Trigger requires misbehaving K8 silicon/firmware (erratum, wedged PLL, incomplete VM MSR emulation) plus a frequency transition (root sysctl write or the module-load setperf), hence speculative impact on this hardware base; untestable on the Intel/QEMU guest (module CPUID gate refuses, MOD_LOAD error 2, env.txt).
No comments yet.