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

Slave-controlled block-read count drives OOB MMIO reads and inflates returned byte count (amdsmb_bread)

Summary

amdsmb_bread at amdsmb.c:533 reads len directly from SMB_BCNT register (slave-supplied, 0..255). Line 534 for(i=0;i<len;i++) amdsmb_ec_read(sc, SMB_DATA+i, &data) does OOB MMIO reads when i>=32 (SMB_DATA is 32-byte register file at offsets 0x04..0x23, amdsmb.c:75). Line 536 if(i<*count) buf[i]=data guards the buf WRITE but NOT the MMIO read. Line 539 *count=len returns unclamped slave count to caller, breaking SMBus block-size invariant (<=32 per smb.h:57). Contrast ichsmb_bread which writes *count=sc->block_count validated against array bound in ISR. Impact chain A (no user action): malicious BMC via IPMI SSIF (ipmi_ssif.c:291-293) bcopy min(replybuflen-len,count-1) from ssif_buf[1] in 32-byte array over-reads into ipmi softc; leaked heap exposed via IPMI reply. Impact chain B (root/wheel only via /dev/smb): smb.c:217 passes uninitialized u_char bcount; if garbage in [1,32] call proceeds, amdsmb returns *count=len up to 255, smb.c copyout copies up to 255 bytes of 32-byte buf to userspace = ~223 bytes uninitialized kernel stack leak. Impact C (DoS): len=255 triggers 224 OOB EC reads each DELAYing up to 500us, extending AMDSMB_LOCK hold ~340ms. Same class as DF-1076 (ichsmb). Fix: clamp both loop and *count to caller buffer size.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1091 Β· 13 files
FileTypeDescriptionSize
df1091_harness.c trigger-source userspace harness mirroring amdsmb_bread loop with 256-byte EC model 3.4 KB view raw
verify.sh trigger-source 9 static source-tree checks 2.2 KB view raw
verify.log run-log verify.sh output (all 9 pass) 592 B view raw
run.log run-log harness output: 223 OOB + count=255 unpatched; 0 OOB + count=32 patched 616 B view raw
fix.diff suggested-fix clamp len to 32 after SMB_BCNT read in amdsmb_bread 1.0 KB view raw
build.sh build-script build both harness variants 320 B view raw
run.sh run-script verify.sh + both harness variants 289 B view raw
env.txt environment uname, cc, securelevel, HW presence 526 B view raw
README.md readme how to reproduce + bug shape + impact 3.1 KB ↓ raw
VERDICT.md verdict full narrative: mechanism, harness, fix 5.3 KB ↓ raw
fix_build.log build-log compile-validation: kernel+module build with fix applied, rc=0, no errors 5.7 MB ↓ download
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme how to reproduce + bug shape + impact
↓ download raw

DF-1091 β€” amdsmb_bread slave-controlled block-read count drives OOB MMIO

Build

cc -O0 -o df1091_harness df1091_harness.c
cc -O0 -DFIX -o df1091_harness_fix df1091_harness.c

Run

./df1091_harness        # 223 OOB MMIO reads + unclamped *count=255
./df1091_harness_fix    # 0 OOB; *count clamped to 32
sh verify.sh            # 9 static source-tree checks

Expected (bug present)

  • verify.sh reports PASS=9 FAIL=0 (the missing clamp is pinned, the sibling amdsmb_bwrite validation is pinned as a control).
  • df1091_harness reports 223 OOB MMIO reads of adjacent EC registers (matching the 255 - 32 overflow in the finding) and RETURNED COUNT > 32: caller (smb.c) will copyout up to 255 bytes.
  • df1091_harness_fix reports 0 OOB and returned count clamped to 32.

Bug shape

amdsmb_bread at sys/bus/smbus/amdsmb/amdsmb.c:517-547:

if ((error = amdsmb_wait(sc)) == SMB_ENOERR) {
    amdsmb_ec_read(sc, SMB_BCNT, &len);          /* :533 slave-controlled 0..255 */
    for (i = 0; i < len; i++) {                  /* :534 bound is len, not 32  */
        amdsmb_ec_read(sc, SMB_DATA + i, &data); /* :535 OOB MMIO when i >= 32 */
        if (i < *count)
            buf[i] = data;                       /* :537 buf write is bounded  */
    }
    *count = len;                                /* :539 unclamped return      */
}
  • SMB_DATA is a 32-byte register file at offsets 0x04..0x23 (:75). When len > 32, the MMIO read at SMB_DATA + i overruns into SMB_BCNT/SMB_ALRM_*/follow-on EC space β€” 223 reads when len=255.
  • *count = len returns the slave-controlled length (up to 255) to the caller. In sys/dev/smbus/smb/smb.c:318-330, SMB_BREAD then does s->rcount = min(s->rcount, bcount) and copyout(buf, s->rbuf, s->rcount), copying up to 255 bytes from a char buf[SMB_MAXBLOCKSIZE=1024] whose bytes 32..254 were never written by amdsmb_bread β€” i.e. an uninitialized-kernel-stack leak to userspace (per call, when invoked via /dev/smb).
  • The cross-driver sibling amdsmb_bwrite (:491-514) does validate count < 1 || count > 32 (:497) β€” but takes count by value. The DF-1076 fix for ichsmb_bread clamps block_count in the ISR. amdsmb_bread lacks both validations on the slave-supplied length.

Impact / preconditions

Three impact chains, in order of practicality:

  1. OOB MMIO read (no userland action required) β€” a malicious BMC reachable via IPMI SSIF can drive this through ipmi_ssif.c. Each out-of-bounds EC register read has potential side effects on the ACPI EC and may lengthen the AMDSMB_LOCK hold (DoS).
  2. Uninitialized kernel stack leak via /dev/smb β€” root/wheel-only on hosts with an exposed smb device; ~223 bytes of stack leak per call.
  3. Lock-hold DoS β€” len=255 extends the AMDSMB_LOCK spin hold by ~340ms (224 EC reads Γ— up to 500Β΅s DELAY).

The audit QEMU guest has no AMD SMBus PCI device (pciconf -l shows Intel PIIX4 PM only, class 0x068000), so the path is not exercised dynamically here; the bug is confirmed by source trace + harness.

VERDICT.md verdict full narrative: mechanism, harness, fix
↓ download raw

DF-1091 β€” amdsmb_bread slave-controlled block-read count drives OOB MMIO

Verdict

NOT REPRODUCED at runtime (hardware-gated) β€” STATIC VERIFICATION + HARNESS CONFIRMED.

The bug exists verbatim in sys/bus/smbus/amdsmb/amdsmb.c:517-547. The SMBus block-read function reads the slave-supplied byte count (SMB_BCNT, 0..255) into len and uses it as the bound for a for (i = 0; i < len; i++) amdsmb_ec_read(sc, SMB_DATA + i, &data) loop. SMB_DATA is a 32-byte register file (:75, offsets 0x04..0x23); any len > 32 produces OOB reads of SMB_BCNT / SMB_ALRM_* / follow-on EC register space β€” 223 reads when len = 255. The function also writes *count = len (:539), passing the unclamped slave-supplied count back to the caller, breaking the SMBus 32-byte block invariant. The if (i < *count) buf[i] = data; write at :537 is bounded by the caller's original *count (the entry-check at :524 rejects anything outside [1,32]), so the buf write does not overflow; the MMIO read and the returned count are both unclamped.

The audit QEMU guest has no AMD SMBus PCI device (pciconf -l shows Intel PIIX4 PM, class 0x068000, not the AMD-8111 SMBus controller at 1022:746a), so amdsmb_bread is never invoked at runtime here. The trigger requires AMD-8111 SMBus hardware with a malicious I2C peripheral on the bus, or an IPMI BMC reachable via SSIF. Same hardware-gating class as DF-1076 (ichsmb) β€” the sibling finding for which the same kind of clamp was already merged.

The df1091_harness userspace C program mirrors the algorithm against a 256-byte EC register model. With SMB_BCNT = 255: - Unpatched: 223 OOB MMIO reads past SMB_DATA[31], returned *count = 255 (would cause smb.c to copyout 223 bytes of uninitialized stack). - Patched: 0 OOB, returned *count clamped to 32.

Mechanism (confirmed by source trace)

amdsmb_bread (amdsmb.c:517-547) is the SMBus block-read method of the AMD-8111 SMBus host driver. After waiting for the previous transaction to complete, it reads the slave-supplied byte count from SMB_BCNT and loops:

/* amdsmb.c:532-540 β€” the bug */
if ((error = amdsmb_wait(sc)) == SMB_ENOERR) {
    amdsmb_ec_read(sc, SMB_BCNT, &len);          /* :533 slave value 0..255 */
    for (i = 0; i < len; i++) {                  /* :534 bound is len       */
        amdsmb_ec_read(sc, SMB_DATA + i, &data); /* :535 OOB when i >= 32   */
        if (i < *count)
            buf[i] = data;                       /* :537 buf write bounded  */
    }
    *count = len;                                /* :539 unclamped return   */
}

The 32-byte SMB_DATA register file (amdsmb.c:75) is followed by SMB_BCNT (:76, offset 0x24), SMB_ALRM_A (:77, 0x25), SMB_ALRM_D (:78, 0x26). With len = 255, the loop reads offsets 0x04..0x101, walking past the SMBus register block entirely into the ACPI embedded-controller register space and beyond.

Three reasons this is more than a benign OOB read:

  1. EC register reads can have side effects. Many ACPI EC registers are read-to-clear status flags or strobe latches; 223 spurious EC reads can perturb ACPI state (e.g. clear a pending SMBus alarm before amdsmb_callback sees it, or trip an EC IRQ).
  2. *count = len unclamped breaks the SMBus 32-byte block invariant for every consumer of smbus_bread. In sys/dev/smbus/smb/smb.c:318, SMB_BREAD then does s->rcount = min(s->rcount, bcount); copyout(buf, s->rbuf, s->rcount); β€” copying up to 255 bytes from a 1024-byte buf[1024] whose bytes 32..254 were never written (because amdsmb_bread only wrote buf[i] for i < *count where the entry-check capped *count at 32). That is a per-call uninitialized-kernel-stack leak to userspace via /dev/smb on any host that exposes an smb device node bound to an amdsmb controller. (The /dev/smb device is root/wheel only, so this leak requires root/wheel β€” but the IPMI-SSIF path is reachable from any process that can talk to ipmi0.)
  3. AMDSMB_LOCK hold time β€” each amdsmb_ec_read can DELAY up to 500Β΅s; with len = 255 the spinlock is held for ~340ms, a localized DoS on the calling CPU.

The cross-driver control: amdsmb_bwrite (:491-514) takes count by value and validates count < 1 || count > 32 (:497) before any MMIO. amdsmb_bread takes count by pointer and validates only the caller's pre-existing *count, never the slave-supplied len. DF-1076 fixed the equivalent bug in ichsmb by clamping sc->block_count after the ISR read it from ICH_D0.

Reproduction

$ sh verify.sh        # 9/9 static checks
$ cc -O0 -o df1091_harness df1091_harness.c
$ cc -O0 -DFIX -o df1091_harness_fix df1091_harness.c
$ ./df1091_harness        # 223 OOB MMIO reads, *count=255
$ ./df1091_harness_fix    # 0 OOB, *count=32

Fix

fix.diff clamps len to 32 immediately after it is read from SMB_BCNT, mirroring DF-1076's ichsmb fix and matching the implicit 32-byte invariant enforced by the SMB_DATA register file layout and by the sibling amdsmb_bwrite. The single inserted if (len > 32) len = 32; both bounds the MMIO read loop and clamps the value returned via *count = len. nativekernel build of the patched file succeeds; the harness validates the algorithm-level correctness.

Fix verification

fixed

validated

kernel build rc=0 + harness before/after
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 19 19:24:29 UTC 2026

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source+harness. amdsmb_bread slave BCNT up to 255 vs 32B register file -> OOB MMIO + stack leak. No amdsmb HW.