amdpm_wait() off-by-one: while(count--)/if (!count) makes SMB_ETIMEOUT unreachable on genuine SMBus timeouts (silent success with stale HSTDATA/FIFO data) and spuriously fails completions on the final poll
Summary
The post-decrement loop leaves count==-1 on exhaustion, so the !count timeout test can never fire: a wedged controller returns SMB_ENOERR and every caller reads stale HSTDATA/FIFO garbage as valid data (silent wrong sensor data, missed write failures) while the controller stays wedged; a completion on the 10000th poll is spuriously failed. DF-4549 (viapm) twin - same alpm lineage; intpm uses the correct pattern. Root-only /dev/smb 0600 + in-kernel consumers on AMD/nForce. count<0 one-line fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4737 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.6 KB | β raw | |
| VERDICT.md | β | 4.4 KB | β raw | |
| amdpm_wait_offbyone_demo.c | β | 4.1 KB | view raw | |
| build.sh | β | 150 B | view raw | |
| run.sh | β | 92 B | view raw | |
| build.log | β | 160 B | view raw | |
| run.log | β | 996 B | view raw | |
| guest_hw_evidence.txt | β | 1.5 KB | view raw | |
| env.txt | β | 493 B | view raw | |
| fix.diff | β | 283 B | view raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 3.0 KB | view raw |
DF-4737 β amdpm_wait() off-by-one (silent SMBus timeout / spurious last-poll failure)
File: sys/dev/powermng/amdpm/amdpm.c (pass-2 GLM 5.3 re-audit)
Sibling of DF-4549 (viapm.c, same while(count--) / if (!count) defect family).
Build
On the audit QEMU guest (DragonFly 6.5-DEVELOPMENT, cc 8.3):
cc -Wall -Wextra -o /tmp/df4737 /tmp/df4737.c
(amdpm_wait_offbyone_demo.c transferred to the guest first β see build.sh)
Run
/tmp/df4737
Expected output (decisive lines)
A clear_on_read=0 polls=10000 error=0x0 NO-TIMEOUT
-> genuine timeout (controller never clears HST_STS) reported as
SMB_ENOERR; amdpm_recvb/readb/readw/bread then return stale
HSTDATA/FIFO bytes as a SUCCESSFUL transfer
B clear_on_read=10000 polls=10000 error=0x20 TIMEOUT-REPORTED
-> transfer that completes on the final (10000th) poll is spuriously
reported SMB_ETIMEOUT
bugs demonstrated in buggy loop: 2 (exit 0)
The demo's "fixed loop" section re-runs A and B against the one-line fix
(if (count < 0)) and shows both cases behave correctly.
Why a userspace replica
The in-kernel path is hardware-gated: amdpm(4) attaches only to AMD
756/766/768/8111 PMU (1022:740b/7413/7443/746b) or nForce SMBus
(10de:01b4) PCI devices; the QEMU audit guest has none (guest_hw_evidence.txt:
kldload amdpm succeeds, attaches nothing, no /dev/smb*). The loop is
pure C semantics, so a verbatim replica of amdpm_wait()'s control flow
with a fixture register read proves the defect deterministically. Same
method as the accepted DF-4549 evidence pack.
DF-4737 VERDICT β amdpm_wait() off-by-one
Status: not_reproduced at runtime (hardware-gated) β logic defect certain, proven by guest-compiled replica of the exact loop; impact ceiling limited.
Root cause (path:line)
sys/dev/powermng/amdpm/amdpm.c:300-335, amdpm_wait():
int count = 10000; /* :302 */
...
while(count--) { /* :308 */
DELAY(10);
sts = AMDPM_SMBINW(sc, AMDSMB_GLOBAL_STATUS);
if (!(sts & AMDSMB_GS_HST_STS))
break; /* :313 */
}
...
if (!count) /* :319 */
error |= SMB_ETIMEOUT; /* :320 */
while(count--) post-decrements on every test, including the final failing
one. Exhausting the loop therefore leaves count == -1, never 0:
- Case A β genuine timeout: all 10000 polls (~100 ms) see HST_STS still
set; the loop exits with
count == -1;!countis false;SMB_ETIMEOUTis never set; the function returnsSMB_ENOERR. All ten callers (amdpm_quick:366,amdpm_sendb:393,amdpm_recvb:419-420,amdpm_writeb:448,amdpm_readb:475-476,amdpm_writew:504,amdpm_readw:531-532,amdpm_bwrite:576,amdpm_bread:610-622) treat this as success: reads return the stale HSTDATA register / 32-byte-FIFO content as valid data; writes are reported complete although the controller never executed them. Becauseerror == SMB_ENOERR,amdpm_clear()(:331-332) is skipped, so the sticky HST_STS stays latched and the next transfer failsamdpm_idle()with SMB_EBUSY β the controller is wedged but the first failure is silent. - Case B β completion on the final poll: the break at :312-313 fires on
the 10000th iteration when
counthas already reached 0;!countis true; a transfer that did complete is reportedSMB_ETIMEOUT.
This is the identical defect filed as DF-4549 for viapm_wait()
(viapm.c:569/574/587) β both drivers descend from alpm.c. The third sibling,
intpm.c (intsmb_wait, :560-585), uses explicit return (SMB_ETIMEOUT)
and is why intpm passed pass-2 clean.
Proof
amdpm_wait_offbyone_demo.c replicates the loop verbatim (DELAY dropped β
no control-flow effect) with a fixture GLOBAL_STATUS read; compiled with
guest cc 8.3 and executed on the guest (run.log):
- A (perpetual busy):
error=0x0β timeout missing, silent success. BUG. - B (clears on poll #10000):
error=0x20(SMB_ETIMEOUT) β spurious. BUG. - C (clears on poll #5000):
error=0x0β correct (control). - Fixed variant (
if (count < 0)): A β 0x20, B β 0x0 β both correct, validating fix.diff in silico.
Reachability / threat model
- Userspace reachability is via the smbus(4) char device:
/dev/smbNis created UID_ROOT/GID_WHEEL mode 0600 (sys/dev/smbus/smb/smb.c:135-141), so the attacker is already root β OR an in-kernel smbus consumer (e.g. battery/thermal sensors on amd756/766/768/8111 and nForce chipsets) silently reading wrong values with no error. Data-integrity defect, no memory corruption: lengths/buffers are unaffected (bwrite clamps 1..32 at :548; bread's buffer writes are guarded at :618). - Hardware gate: requires an AMD 756/766/768/8111 PMU or nForce SMBus
controller. The QEMU audit guest has none β
kldload amdpmloads the module but nothing attaches, and/dev/smb*does not exist (guest_hw_evidence.txt). Runtime reproduction of the in-kernel path on this guest is impossible; hence not_reproduced with certain-by-C-semantics logic proof (same convention as DF-4549).
Fix
One line, amdpm.c:319: if (!count) β if (count < 0).
Loop exhaustion leaves count == -1 (timeout, correctly flagged); a break on
the final iteration leaves count == 0 (success, correctly not flagged).
See fix.diff. (An equivalent alternative: a done flag set at the break,
as recommended for DF-4549.)
Fix validation
Not testable on this guest (the kernel path needs AMD/nForce PMU hardware; a single-fix kernel build would have no observable input to feed it). The demo's built-in fixed-loop variant executes the exact fixed predicate against both failure modes and behaves correctly β recorded in run.log.
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_testablenot_testable: kernel path requires AMD/nForce PMU hardware absent from the guest; the one-line diff is validated in-silico by the demo's fixed-loop variant (case A would report SMB_ETIMEOUT, case B would report success β run.log).
fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log (guest cc 8.3 build + both failure modes + fixed-loop validation); guest_hw_evidence.txt (pciconf: no 1022/10de PMU device; kldload amdpm succeeds, no attach, no /dev/smb*); VERDICT.md (line-by-line root cause at amdpm.c:308/319)
PoC changes
No seed existed; authored host-side replica of the amdpm_wait loop with fixture register reads plus an in-demo fixed-loop variant, compiled and executed on the guest (no host compiler available).
Verified recommended fix
Change if (!count) to if (count < 0) in amdpm_wait() at amdpm.c:319 (loop exhaustion leaves count == -1; completion on the final poll leaves count == 0 and must not be a timeout).
Verdict
Logic defect certain by C semantics and proven by a guest-compiled replica of the exact amdpm_wait loop (run.log: perpetual-BUSY -> error=0x0, SMB_ETIMEOUT never reported; final-poll completion -> error=0x20, spurious failure). The in-kernel path is hardware-gated: no AMD 756/766/768/8111 PMU (1022:740b/7413/7443/746b) or nForce SMBus (10de:01b4) PCI device exists in the QEMU guest (guest_hw_evidence.txt), so amdpm.ko loads but never attaches and the smbus interface is unreachable (/dev/smb* absent). Surface is root-only where it does exist (0600 root:wheel /dev/smbN, smb.c:135-141) plus in-kernel smbus consumers; impact is silent wrong-data/missing-error (data integrity), not memory corruption β bwrite clamps 1..32 at amdpm.c:548 and bread's buffer writes are guarded at amdpm.c:618. Same while(count--)/if(!count) defect family as DF-4549 (viapm.c:574/587); intpm.c:560-585 shows the correct explicit-return pattern.
No comments yet.