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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4737 Β· 12 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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; !count is false; SMB_ETIMEOUT is never set; the function returns SMB_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. Because error == SMB_ENOERR, amdpm_clear() (:331-332) is skipped, so the sticky HST_STS stays latched and the next transfer fails amdpm_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 count has already reached 0; !count is true; a transfer that did complete is reported SMB_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/smbN is 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 amdpm loads 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_testable
baseline no→ patch + rebuild →patched clean

not_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
↓ fix.diffper-fix-DF-4737

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.