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

All three amdsmb poll-loop timeouts are dead code (post-decrement off-by-one): wedged EC/SMBus controller is reported as SMB_ENOERR success with the PREVIOUS transaction's data and count after a 5.4s lock-held busy spin; SMB_ETIMEOUT is unreachable

Summary

All three wait loops use while(x-- && cond) + if(x==0): exhaustion leaves x==-1 so the timeout branch NEVER fires - a wedged controller busy-spins 5.44s holding the exclusive lock then reads STS 0x00 and returns SUCCESS with the stale previous data and count (cross-transaction confusion); the EC-level variants accept a stale OBF byte as any register (e.g. BCNT). The dead branches are the ONLY failure returns, so the engine's ignored error codes are moot end-to-end. Production-code KLD rig with a stateful AMD-8111 emulator: T1 wedge -> rc=0 after 5440ms/10002 polls returning previous data+count; T2/T2b EC wedges -> rc=0 past budget; fix.diff -> SMB_ETIMEOUT fires, zero data copied, healthy T0 PASS. ichsmb DF-5461 / ig4 DF-5386 family.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5506 Β· 14 files
FileTypeDescriptionSize
README.md β€” 4.3 KB ↓ raw
VERDICT.md β€” 4.7 KB ↓ raw
amdh_harness.c β€” 12.2 KB view raw
amdsmb.c β€” 14.2 KB view raw
Makefile β€” 311 B ↓ download
build.sh β€” 90 B view raw
run.sh β€” 108 B view raw
fix.diff β€” 1.7 KB view raw
build.log β€” 1.4 KB view raw
build_fixed.log β€” 1.4 KB view raw
run.log β€” 2.3 KB view raw
run_fixed.log β€” 2.2 KB view raw
env.txt β€” 340 B view raw
verdict.json β€” 4.3 KB view raw

DF-5506 PoC β€” amdsmb poll state machine: all three timeout branches are dead code; wedged EC/controller is reported as SUCCESS with stale data

Root cause

Every polling loop in sys/bus/smbus/amdsmb/amdsmb.c detects its timeout with if (x == 0) after a loop whose exit-on-exhaustion leaves x == -1 (post-decrement idiom while (x-- && cond)):

  • amdsmb_ec_wait_write (amdsmb.c:212-221): while (timeout-- && ...IBF) then if (timeout == 0) β€” when the 500-iteration budget is exhausted, timeout == -1, the branch never fires, the function returns 0 ("success") and the caller writes the next EC byte anyway against a wedged input path.
  • amdsmb_ec_wait_read (amdsmb.c:224-235): identical, OBF side β€” after 500 polls the caller reads EC_DATA and accepts the stale byte from the previous EC data read as the requested register's value.
  • amdsmb_wait (amdsmb.c:277-294): do { DELAY(500); ... } while (temp != 0 && count--) then if (count == 0) return (SMB_ETIMEOUT) β€” count exits as -1, so SMB_ETIMEOUT is unreachable dead code. A controller whose SMB_PRTCL never returns to 0 (wedged transaction) makes the driver busy-spin 10001 Γ— 500 Β΅s β‰ˆ 5.45 s while holding the exclusive sc->lock, then read SMB_STS, see 0x00 (OK) and return SMB_ENOERR success β€” whereupon amdsmb_bread returns the stale data and byte count of the previous transaction as the result of the new one.

Compounding it, every amdsmb_ec_read/amdsmb_ec_write return value in the request engine is ignored (amdsmb.c:350-351, 368-370, 388-389, 408-411, 429-431, 450-453, 473-475, 501-506, 528-530) β€” but since the timeout branches are dead code those functions can never report failure anyway: the error plumbing of the whole driver is unreachable.

Hardware-gated (AMD-8111, PCI 1022:746a) and reached through /dev/smbN (root-only on stock installs), same evidence class as the ichsmb/ig4 batches.

Evidence method (production-code KLD harness)

No QEMU machine model contains an AMD-8111 SMBus controller, so the rig compiles the real, unmodified production source (amdsmb.c, asserted byte-identical to /usr/src in build.log:1) into a KLD and redirects bus_read_1/bus_write_1 (#undef + re-#define before the #include) into a stateful emulation of the AMD-8111 ACPI-EC / SMBus 2.0 register interface (full EC_CMD_RD/WR handshake, IBF/OBF status flags, SMB register bank 0x00-0x27). The file's own AMDSMB_ECINB/ECOUTB macros expand at their use sites, so the production code executes verbatim, in-kernel, against the emulated controller.

Observed (run.log, baseline)

  • T1 wedge_prtcl: amdsmb_bread on a controller whose PRTCL never clears β†’ rc=0 (SMB_ENOERR) after 5440 ms / 10002 polls, returning the previous transaction's data (0xB0..) and count (32) as the new transaction's result. SMB_ETIMEOUT (0x20) never returned.
  • T2 wedge_ibf: amdsmb_ec_read after 502 EC_SC polls (budget 500) β†’ rc=0 "success"; the write proceeded against the wedged EC.
  • T2b wedge_obf: amdsmb_ec_read of SMB_BCNT after 503 polls β†’ rc=0 with the stale EC data byte 0x5C accepted as the register value.

Observed (run_fixed.log, fix.diff applied to the harness copy only)

  • T1: rc=32 (SMB_ETIMEOUT), no data copied (buffer untouched). The 5.4 s busy-wait remains (the EC interface offers no abort/KILL register; the fix detects, it cannot cancel) β€” noted as residual.
  • T2/T2b: rc=1 β€” the previously-dead device_printf timeout messages now actually fire ("timeout waiting for IBF to clear" / "for OBF to set").
  • Sanity T0 (healthy path) unchanged: rc=0, correct data.

Build & run

guest# cd /root/amdh && sh build.sh          # asserts amdsmb.c == /usr/src copy, builds amdh.ko (-Werror clean)
guest# sh run.sh                             # kldload ./amdh.ko; tests run at MOD_LOAD; output via dmesg
guest# sh build.sh fixed && sh run.sh        # fix.diff applied to the local copy, same tests

Fix

fix.diff: make all three poll loops detect exhaustion (x-- > 0 + if (x < 0)), and propagate amdsmb_ec_read failures in amdsmb_wait as SMB_EBUSERR (plus the DF-5507/DF-1091-family clamp in amdsmb_bread). Authored against the read-only sys/ tree; validated in the fixed harness build above; never applied to sys/.

VERDICT.md
↓ download raw

DF-5506 VERDICT β€” REPRODUCED (in-kernel, production source, emulated AMD-8111 EC); fix validated in the same rig

Bottom line

All three timeout detectors in the amdsmb poll state machine are dead code β€” the while (x-- && cond) / if (x == 0) post-decrement idiom leaves the counter at -1 when the budget is exhausted, so exhaustion is never detected:

  • amdsmb_ec_wait_write (sys/bus/smbus/amdsmb/amdsmb.c:214-219): 500-poll IBF budget β†’ returns 0 ("go ahead") on exhaustion.
  • amdsmb_ec_wait_read (amdsmb.c:228-234): 500-poll OBF budget β†’ returns 0 on exhaustion; caller then reads a stale EC_DATA byte as register data.
  • amdsmb_wait (amdsmb.c:287-293): 10000 Γ— DELAY(500) PRTCL budget β†’ SMB_ETIMEOUT at amdsmb.c:293 is unreachable; the driver proceeds to read SMB_STS of a still-busy/wedged controller and maps whatever it finds (0x00 β†’ SMB_ENOERR) onto the current transaction.

Consequences, all demonstrated by executing the unmodified production source in-kernel against a datasheet-faithful AMD-8111 EC register emulator (T1/T2/T2b in run.log):

  1. A wedged SMBus transaction is reported as SUCCESS after a 5.44 s busy-spin under the exclusive sc->lock, returning the PREVIOUS transaction's data and byte count as the new transaction's result (cross-transaction data confusion; every smbus op is affected, not just block reads).
  2. A wedged EC input path (IBF stuck) never stops the driver: it keeps writing CMD/address/data bytes into a controller that is not consuming them.
  3. A wedged EC output path (OBF never sets) yields stale EC data bytes accepted as arbitrary SMB register values (e.g. SMB_BCNT β€” feeding the known DF-1091 count path with a stale rather than slave-controlled value).

Because the same dead branches are the ONLY failure returns of amdsmb_ec_read/amdsmb_ec_write, the drivers' ignored-return-codes issue (every call site in amdsmb.c:350-530) is moot until this is fixed β€” the error plumbing is unreachable end-to-end.

Impact ceiling: local DoS (a /dev/smbN opener β€” root on stock installs; operator-group on some β€” can serialize 5.4 s lock-held busy-spins per ioctl against stuck hardware) plus silent data corruption for legitimate SMBus clients (sensors, EDID, battery). No memory-safety impact: the stale values feed bounded paths (the unbounded one is known DF-1091). Severity Low, matching the ichsmb sibling (DF-5461, timeout/abort family) and ig4 DF-5386 (silent success family).

Reproduction honesty

The AMD-8111 SMBus 2.0 controller (PCI 1022:746a) has no QEMU machine model β€” no stock or buildable rig executes this driver on emulated hardware. The evidence class is the production-code KLD harness used by the ichsmb/ig4 batches, taken one step further: the harness #includes the real amdsmb.c (byte-identity asserted at build time, build.log:1) and interposes a stateful register-level emulation of the EC interface the driver actually drives (EC_CMD_RD/WR handshake with IBF/OBF flags). The executed code is the production text verbatim; only the ultimate port I/O is emulated. The observed behavior (10002 polls / 5440 ms / rc=0) is deterministic C semantics, not emulator timing: the dead-branch proof is static (the counter provably exits at -1), the run makes it visible.

Fix validation (same rig, fix.diff applied to the harness copy)

  • Baseline run.log T1: rc=0 (fake success), stale data+count, 5440 ms. Fixed run_fixed.log T1: rc=32 (SMB_ETIMEOUT), zero bytes copied.
  • Baseline T2/T2b: rc=0 with 502/503 polls ("success" past exhaustion). Fixed: rc=1, and the previously-dead device_printf timeout messages now execute (they print "unknown:" because the harness fake device_t is minimal β€” cosmetic, noted).
  • Healthy-path regression: T0 PASS in both builds.
  • Residual (documented, not claimed fixed): the fixed driver still busy-waits the full 5.4 s before reporting β€” the EC register interface exposes no transaction-abort (KILL) mechanism; converting the spin to a sleep/timeout scheme is an upstream design decision.

Kernel references

sys/bus/smbus/amdsmb/amdsmb.c:212-221 (wait_write), :224-235 (wait_read), :277-294 (wait; dead SMB_ETIMEOUT at :293), :296-324 (STS read of wedged controller mapped to SMB_ENOERR), :349-356 et al. (lock-held across the entire spin), sys/dev/smbus/smb/smb.c:206-359 (reachability: /dev/smb ioctls, root-gated device).

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Baseline reproduced (rc=0 fake success with stale data after 5440ms; dead SMB_ETIMEOUT). Patched build (fix.diff applied to harness copy, /usr/src untouched): T1 returns SMB_ETIMEOUT with zero data copied, T2/T2b return rc=1 with the timeout messages now firing, healthy-path T0 unchanged. The bad behavior is gone.

run.log vs run_fixed.log (T1/T2/T2b lines); build_fixed.log; fix.diff
↓ fix.diffper-fix-DF-5506

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log T1/T2/T2b (baseline: rc=0 fake success, 5440ms/10002 polls, stale 0xB0.. data accepted, stale 0x5C accepted as SMB_BCNT), run_fixed.log (rc=32 SMB_ETIMEOUT, rc=1, no data copied, dead printfs now fire), build.log:1 (production-source byte-identity assertion), amdh_harness.c (rig: #undef/#define bus_read_1/bus_write_1 before #include of the real amdsmb.c + EC/SMB register emulator), fix.diff, VERDICT.md

PoC changes

no seed existed (pass-2 fresh finding); authored the production-code KLD harness: #include of the real amdsmb.c with bus_read_1/bus_write_1 redirected into a stateful AMD-8111 EC register emulator, fake device_t/resource/softc, tests T0-T4 executed at MOD_LOAD

Verified recommended fix

amdsmb.c: fix all three poll loops to detect exhaustion (while (x-- > 0 && cond) + if (x < 0)), propagate amdsmb_ec_read/write failures in amdsmb_wait as SMB_EBUSERR β€” exactly fix.diff; validated in-rig.

Verdict

REPRODUCED in-kernel against the unmodified production source: all three amdsmb poll-loop timeouts (ec_wait_write amdsmb.c:214-219, ec_wait_read :228-234, wait :287-293) are dead code β€” the post-decrement idiom exits with the counter at -1, so if (x == 0) never fires and SMB_ETIMEOUT (:293) is unreachable. A wedged SMB_PRTCL makes amdsmb_bread busy-spin 10002x500us=5.44s under the exclusive sc->lock and then return SMB_ENOERR with the PREVIOUS transaction's data and count (run.log T1); a wedged IBF lets the driver keep writing to a non-consuming EC (T2); a wedged OBF makes it accept a stale EC data byte as any register value, e.g. SMB_BCNT (T2b). Fix.diff (detect exhaustion via x-- > 0 / x < 0 + propagate ec_read failures) validated in the same rig: fixed build returns SMB_ETIMEOUT / rc=1 and copies no data; the previously-dead device_printf timeout messages now execute; healthy path unchanged (T0 PASS both). Residual: 5.4s busy-wait remains (EC interface has no abort/KILL register). Hardware-gated (AMD-8111 1022:746a, absent from the guest and from every QEMU machine model) β€” evidence class is the production-code KLD harness with a stateful AMD-8111 EC register emulator (amdsmb.c asserted byte-identical to /usr/src at build time). Severity Low like the ichsmb DF-5461 / ig4 DF-5386 siblings.