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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4906 Β· 6 files
FileTypeDescriptionSize
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).

Bound the wait (e.g. loop with a deadline; on timeout abort the transition with an error) β€” see fix.diff.

VERDICT.md
↓ download raw

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_testable
baseline no→ patch + rebuild →patched clean

Fix 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
↓ fix.diffper-fix-DF-4906

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).