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

Slave-controlled block-read count overflows fixed 32-byte sc->block_data buffer in ISR

Field Value
ID DF-1076
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE CWE-787 Out-of-bounds Write
File sys/bus/smbus/ichsmb/ichsmb.c
Lines 575 (count read), 580 (loop bound), 583 (indexed write)
Area bus/smbus/ichsmb (Intel ICH SMBus controller)
Confidence certain
Discovered 2026-07-14
Reported pending
Known CVE none
CVE match dfly_specific

Summary

On an SMBus block-read transaction, the ichsmb interrupt handler stores the slave-supplied count byte (read from ICH_D0) directly into sc->block_count with no clamp, then uses that value as the upper bound for writing received data bytes into the fixed-size sc->block_data[32] array. A malicious or spec-violating peripheral that reports a count greater than 32 causes an out-of-bounds heap write past block_data, corrupting the adjacent struct lock mutex and whatever follows in the ichsmb_softc, with fully attacker-controlled byte values and length up to ~223 bytes.

Root cause

In ichsmb_device_intr() the block-read branch executes, on the first BYTE_DONE_STS interrupt:

/* ichsmb.c:573-585 */
/* First interrupt, get the count also */
if (sc->block_index == 0) {
    sc->block_count = bus_read_1(sc->io_res, ICH_D0);    /* !!! no clamp */
}

/* Get next byte, if any */
if (sc->block_index < sc->block_count) {
    /* Read next byte */
    sc->block_data[sc->block_index++] =                    /* !!! OOB write */
        bus_read_1(sc->io_res, ICH_BLOCK_DB);
    ...
}

ICH_D0 is the count byte supplied by the SMBus/I2C slave during a block-read response; it is an 8-bit register and may take any value 0..255. sc->block_data is declared u_char block_data[32] in sys/bus/smbus/ichsmb/ichsmb_var.h:64. There is no check that sc->block_count <= 32 (or <= sizeof(block_data)) anywhere between line 575 and the indexed store at line 583.

ichsmb_bread() (the caller that initiates the transaction) only validates the USER-supplied *count (ichsmb.c:424) and explicitly OVERWRITES sc->block_count = 0 at line 427 before issuing the command, so the user-side bound has no effect on the value the ISR later reads from the slave.

Once block_count is set to, say, 255, every subsequent BYTE_DONE_STS interrupt writes one attacker-controlled byte at sc->block_data[block_index] for block_index from 0 up to 254 β€” i.e. up to 223 bytes past the end of the 32-byte array. Immediately adjacent in the softc (ichsmb_var.h:65) is struct lock mutex, which is held by the ISR at the time of corruption (lockmgr LK_EXCLUSIVE at ichsmb.c:505); corrupting it yields an immediate panic on the next lock op, and because the overflow bytes are the actual data bytes sent by the slave, the contents of the corruption are fully attacker-steered.

Note this driver also covers I2C pass-through (ICH_HOSTC_I2C_EN, ichsmb_reg.h:54), where the SMBus 32-byte spec limit does not apply and slaves legitimately send more, so the bug is reachable even with compliant I2C peripherals.

Threat model & preconditions

  • Attacker position: A malicious SMBus/I2C peripheral electrically reachable on the bus, OR any code path (kernel-internal consumer at boot such as DIMM SPD probe, ACPI battery / EMS / sensor reads, devd-triggered sensor probing) or local root with /dev/smb0 (mode 0600 root:wheel per sys/dev/smbus/smb/smb.c:135-141) that drives a block-read to such a peripheral.
  • Privileges gained or impact: Kernel heap memory corruption with attacker-controlled length (up to 223 bytes) and attacker-controlled contents, in a known offset relative to the softc allocation. Reliable kernel panic (DoS) is trivial; arbitrary kernel write / privilege escalation is plausible given the attacker controls exactly which bytes overwrite the lockmgr state and anything beyond it. On the panic side this is a 100% reproducible single-transaction DoS; on the exploit side it is a high-quality kernel-heap corruption primitive that can be groomed against the static ichsmb_softc allocation.
  • Required config or capabilities: Default kernel with ichsmb configured. Reachable vectors:
  • (a) supply-chain or hotplug DIMM with malicious SPD EEPROM returning count = 255 during the auto-probe at boot;
  • (b) malicious laptop battery / ACPI SMBus device;
  • (c) Thunderbolt or USB-C dock exposing an SMBus peripheral;
  • (d) PCI-passthrough of the SMBus controller to an untrusted guest.
  • Reachability: Triggered by any block-read SMBus transaction (kernel-initiated probe or ioctl(/dev/smb0, SMB_BREAD, &c) as root) where the slave supplies count > 32.

Proof of concept

Two-piece PoC.

(1) Malicious SMBus slave β€” the cleanest lab setup is QEMU's pm_smbus / i2c emulation. Patch QEMU's hw/i2c/pm_smbus.c (or the i2c-ddc slave) so that on a block-read it writes count = 255 to the data register and then streams 255 attacker-chosen bytes.

Equivalent real-world: a $2 ATtiny / Pico wired to the SMBus DATA/CLOCK lines on a test motherboard, or a hotplug USB-C dock with an embedded SMBus slave controller.

(2) Trigger source β€” C program run on the DragonFly guest:

#include <fcntl.h>
#include <sys/ioctl.h>
#include "smb.h"             /* sys/dev/smbus/smb/smb.h */

int main(void) {
    int fd = open("/dev/smb0", O_RDWR);           /* needs euid 0 */
    if (fd < 0) return 1;
    struct smbcmd c = {0};
    c.cmd    = 0x00;          /* subcommand to read from */
    c.slave  = 0x50;          /* typical SPD / malicious-slave address */
    c.rcount = 32;
    char out[32];
    c.rbuf = out;
    for (;;) ioctl(fd, SMB_BREAD, &c);   /* trigger block-read */
    return 0;
}

Build & run

cc -I/usr/src/sys -I/usr/src/sys/dev/smbus/smb trigger.c -o trigger
sudo ./trigger

Expected output

With a malicious slave returning count > 32 the kernel panics on the very next lockmgr op (typically lockmgr: ... / spin_lock / NULL deref inside the corrupted struct lock), or, if the corruption happens to keep the lock looking valid, the system dies on the next SMBus ioctl.

Confirm the OOB by enabling WITNESS / INVARIANTS: an INVARIANTS kernel will trip on the overwritten lock state immediately. To prove the corruption magnitude, set the slave's payload bytes to 0xAA and observe (kgdb) that sc->mutex and the bytes past it are overwritten with 0xAA up to sc->block_data[255].

Impact

Local / hardware-adjacent kernel heap corruption with attacker-controlled contents. Affects any SMBus / I2C block-read from a malicious peripheral; reachable at boot via DIMM SPD probing or at runtime via /dev/smb0 (root only). High severity per "kernel memory corruption" + plausible local privilege escalation with heap grooming.

Clamp the slave-supplied count to the size of sc->block_data before using it as the loop bound.

--- a/sys/bus/smbus/ichsmb/ichsmb.c
+++ b/sys/bus/smbus/ichsmb/ichsmb.c
@@ -572,6 +572,12 @@ ichsmb_device_intr(void *cookie)
                /* First interrupt, get the count also */
                if (sc->block_index == 0) {
                    sc->block_count = bus_read_1(
                        sc->io_res, ICH_D0);
+                   /* The count byte comes from the
+                      SMBus/I2C slave and is attacker-
+                      controlled.  Clamp it to the size
+                      of block_data to prevent an OOB
+                      write into the softc. */
+                   if (sc->block_count >
+                       sizeof(sc->block_data))
+                       sc->block_count =
+                           sizeof(sc->block_data);
                }

                /* Get next byte, if any */

Equivalent one-liner that also hardens the loop: replace if (sc->block_index < sc->block_count) at ichsmb.c:580 with if (sc->block_index < sc->block_count && sc->block_index < (int)sizeof(sc->block_data)). The clamp-at-load version above is preferred because it keeps the LAST_BYTE logic at lines 589-596 correct (it computes against block_count).

Defense-in-depth: also assert sc->block_index < sizeof(sc->block_data) in INVARIANTS builds at line 583. The same class of fix should be applied to any other vendor SMBus driver that trusts a slave count byte.

References

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1076 Β· 14 files
FileTypeDescriptionSize
harness.c trigger-source userspace harness reproducing exact ISR block-read logic; built with/without -DCLAMP_FIX to model unpatched/patched kernel 9.4 KB view raw
trigger.c trigger-source original trigger (SMB_BREAD on /dev/smb0); builds cleanly but is a no-op on this guest (no /dev/smb0) 1.5 KB view raw
build.sh build-script builds harness + harness_clamped 454 B view raw
run.sh run-script runs both harness variants and shows the contrast 638 B view raw
fix.diff suggested-fix git-apply-able unified diff: clamp block_count to sizeof(block_data) at ichsmb.c:586 968 B view raw
VERDICT.md verdict full narrative: mechanism, harness, hard blocker, fix validation 9.1 KB ↓ raw
README.md readme original PoC README (two-piece QEMU-patch + trigger) 3.0 KB ↓ raw
malicious_slave_qemu_patch.txt trigger-source conceptual QEMU pm_smbus patch sketch (for a guest with ichsmb emulated) 1.1 KB view raw
run.log run-log harness output: UNPATCHED shows 223-byte OOB, PATCHED shows 0 OOB 2.2 KB view raw
trigger_build_attempt.log build-log original trigger.c build + run output (builds, exits with 'No such file or directory') 42 B view raw
fix_build.log build-log full make nativekernel output, NK_DONE rc=0 5.6 MB ↓ download
env.txt environment uname, kern.version, cc version, PCI SMBus scan (none), /dev/smb* (none), ichsmb module presence 679 B view raw
../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 original PoC README (two-piece QEMU-patch + trigger)
↓ download raw

DF-1076 PoC β€” ichsmb block-read count OOB write

Trigger

The ichsmb interrupt handler at ichsmb.c:575 reads the slave-supplied count byte from ICH_D0 directly into sc->block_count with no clamp. The loop at :580-585 then writes up to sc->block_count bytes into the fixed-size sc->block_data[32] array. A slave that supplies count > 32 overflows block_data into the adjacent struct lock mutex and beyond, with fully attacker-controlled contents.

Two-piece PoC

(1) Malicious SMBus slave

The cleanest lab setup is QEMU's pm_smbus / i2c emulation. Patch hw/i2c/pm_smbus.c (or the i2c-ddc slave) so that on a block-read it writes count = 255 to the data register and then streams 255 attacker-chosen bytes.

Equivalent real-world: a $2 ATtiny / Pico wired to the SMBus DATA/CLOCK lines on a test motherboard, or a hotplug USB-C dock with an embedded SMBus slave controller.

A simple QEMU patch sketch is provided alongside this README as malicious_slave_qemu_patch.txt.

(2) Trigger source

#include <fcntl.h>
#include <sys/ioctl.h>
#include "smb.h"

int main(void) {
    int fd = open("/dev/smb0", O_RDWR);  /* needs euid 0 */
    if (fd < 0) return 1;
    struct smbcmd c = {0};
    c.cmd    = 0x00;
    c.slave  = 0x50;
    c.rcount = 32;
    char out[32];
    c.rbuf = out;
    for (;;) ioctl(fd, SMB_BREAD, &c);   /* trigger block-read */
    return 0;
}

Build & run

cc -I/usr/src/sys -I/usr/src/sys/dev/smbus/smb -o trigger trigger.c
sudo ./trigger

Expected output

With a malicious slave returning count > 32 the kernel panics on the very next lockmgr op:

Fatal trap 12: page fault while in kernel mode
fault virtual address   = 0x<address inside corrupted lockmgr state>
ichsmb_device_intr(...) at ichsmb.c:583
atpic_handle_intr(...) at ...
...
panic: lockmgr: corrupted state / spin_lock assertion failure

If the corruption happens to keep the lock looking valid, the system dies on the next SMBus ioctl. Confirm the OOB by enabling WITNESS/INVARIANTS: an INVARIANTS kernel will trip on the overwritten lock state immediately.

To prove the corruption magnitude, set the slave's payload bytes to 0xAA and observe (kgdb) that sc->mutex and the bytes past it are overwritten with 0xAA up to sc->block_data[255].

Static verification fallback

If dynamic verification is impractical (no QEMU build env):

  1. Confirm ichsmb.c:575 reads bus_read_1(sc->io_res, ICH_D0) into sc->block_count with no clamp.
  2. Confirm ichsmb_var.h:64 declares u_char block_data[32].
  3. Confirm ichsmb_var.h:65 declares struct lock mutex immediately after block_data.
  4. Confirm ichsmb.c:580-585 loop bound uses sc->block_count.

All four line references are confirmed in the finding markdown.

Kernel references

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

DF-1076 β€” Verification verdict

Verdict: REPRODUCED (at source level + algorithmic harness) β€” primitive confirmed; runtime-unreachable on this guest (hardware-gated); fix validated as compile+boot-clean and algorithmically correct.

Severity: High (kernel heap OOB write with attacker-controlled bytes & length). Impact: panic / kernel-heap corruption (DoS) on a system with ichsmb hardware + a malicious SMBus/I2C slave. Not uid0 β€” see "Hard blocker" below.


Mechanism (the bug is real, confirmed line-by-line)

In the block-read branch of ichsmb_device_intr():

  • sys/bus/smbus/ichsmb/ichsmb.c:574-577 β€” on the first BYTE_DONE_STS interrupt, the slave-supplied count byte is read from ICH_D0 directly into sc->block_count with no clamp: c if (sc->block_index == 0) { sc->block_count = bus_read_1(sc->io_res, ICH_D0); /* !!! untrusted */ }
  • sys/bus/smbus/ichsmb/ichsmb.c:580-585 β€” the loop bound is sc->block_count, and the body does an indexed store into the fixed-size array: c if (sc->block_index < sc->block_count) { sc->block_data[sc->block_index++] = bus_read_1(sc->io_res, ICH_BLOCK_DB); }
  • sys/bus/smbus/ichsmb/ichsmb_var.h:64-65 β€” block_data is u_char[32] and the immediately-following field is struct lock mutex, which the ISR holds (lockmgr LK_EXCLUSIVE at ichsmb.c:505) at the time of corruption.
  • sys/bus/smbus/ichsmb/ichsmb.c:424-427 β€” caller ichsmb_bread() validates only the user-supplied count and then zeros sc->block_count, so the user-side bound has no effect on the slave-supplied value the ISR later reads.

ICH_D0 is 8 bits, so a malicious peripheral can return 0..255. With count = 255, the indexed store writes block_data[0..254] β€” i.e. up to 223 bytes past the end of the 32-byte array, all bytes fully attacker-controlled (they are the data bytes the slave streams). The first 64 of those land in struct lock mutex, panicking on the next lockmgr op.

The driver also covers I2C pass-through (ICH_HOSTC_I2C_EN, sys/bus/smbus/ichsmb/ichsmb_reg.h:54), where the SMBus 32-byte spec limit does not apply and slaves legitimately send more β€” so the bug is reachable even with compliant I2C peripherals.

Algorithmic harness (demonstrates the primitive without hardware)

The audit QEMU guest has no ichsmb PCI device (PCI 0:1:3 is the PIIX4 ACPI function, class 0x068000, not SMBus class 0x0c05), so the bug cannot be triggered dynamically here. To prove the primitive and validate the fix without malicious hardware, harness.c reproduces the exact algorithmic logic of the ISR block-read branch in userspace:

  • A struct ichsmb_softc is laid out byte-for-byte per ichsmb_var.h:47-66, with a 64-byte mock struct lock mutex and a 192-byte trailing canary immediately after block_data[32].
  • bus_read_1(ICH_D0) returns 255 (slave's "I'm sending 255 bytes" lie); bus_read_1(ICH_BLOCK_DB) returns 0xAA (attacker's payload bytes).
  • The harness drives the same loop the ISR drives, once with CLAMP_FIX undefined (unpatched model) and once with it defined (patched model).

Run as the unprivileged user (ssh dfbsd-maxx):

=== UNPATCHED ===
post-ISR block_count = 255
post-ISR block_index = 255
payload-bytes (0xAA) found in block_data[32]  : 32 / 32
payload-bytes (0xAA) found in mutex.payload[64]: 64 / 64
payload-bytes (0xAA) found in canary[192]      : 159 / 192
VERDICT: OOB WRITE CONFIRMED β€” 223 payload-bytes past block_data[31] (64 in mutex, 159 in canary).

=== PATCHED (clamp at ichsmb.c:586) ===
post-ISR block_count = 32
post-ISR block_index = 32
payload-bytes (0xAA) found in block_data[32]  : 32 / 32
payload-bytes (0xAA) found in mutex.payload[64]: 0 / 64
payload-bytes (0xAA) found in canary[192]      : 0 / 192
VERDICT: NO OVERFLOW β€” writes stayed inside block_data[32].

The 223-byte OOB write (32 in block_data is in-bounds; 64 in mutex + 159 in canary = 223 past the end) matches the finding's predicted magnitude exactly.

The original trigger.c was also built and run; on this guest it exits with open /dev/smb0: No such file or directory β€” confirming the live kernel path is unreachable here (no ichsmb PCI device β†’ no /dev/smb0).

Why not uid0 β€” hard blocker (Phase 6)

This is a device-only reachability hard blocker:

  • On this guest: no ichsmb PCI device at all (pciconf -lv has no class=0x0c05 entry), ichsmb is not in the running kernel (nm /boot/kernel/kernel | grep -c ichsmb = 0), and /dev/smb0 does not exist. kldload ichsmb.ko as root fails silently (no device to attach).
  • Even on a real ichsmb system, the live trigger requires either: (a) a malicious SMBus/I2C peripheral (hardware-adjacent threat β€” supply-chain DIMM, malicious laptop battery, hotplug USB-C dock, PCI-passthrough of the SMBus controller), or (b) ioctl(/dev/smb0, SMB_BREAD, ...) as root β€” /dev/smb0 is 0600 root:wheel per sys/dev/smbus/smb/smb.c:135-141.
  • There is no unprivileged syscall surface to ichsmb. The realistic worst-case impact is kernel heap corruption β†’ panic (DoS) on a system with malicious SMBus hardware. Privilege escalation would require the attacker to already control a malicious peripheral; that is a hardware-adjacent threat, not a clean unprivβ†’root chain.

This makes the honest impact panic/corruption (DoS), not uid0. The finding's CVSS:3.1/AV:L/AC:L/PR:L/... is appropriately scoped to AV:L (local) with PR:L (low privs) β€” but the live trigger requires malicious hardware, which is more restrictive than a pure local-software privesc. The audit owner may want to revisit severity (the corruption primitive is real and high-quality, but the trigger precondition is hardware-adjacent).

PoC changes

  • Added harness.c β€” userspace C harness that faithfully reproduces the ISR block-read logic and the softc layout, demonstrating both the primitive (223-byte OOB write with attacker bytes) and the closure (clamp prevents any byte past block_data[31]). Necessary because the live kernel path is hardware-gated on this guest.
  • Added build.sh / run.sh β€” reproducible build & run.
  • Original trigger.c β€” kept as-is; it builds cleanly (smb.h is at /usr/src/sys/dev/smbus/smb/smb.h) but is a no-op here because /dev/smb0 does not exist.
  • Added fix.diff β€” standalone git-apply-able unified diff that clamps block_count to sizeof(sc->block_data) after the slave-supplied count read. Matches the finding's recommended fix; tightens it to a single clamp + adds a documenting comment.
  • Added env.txt, run.log, fix_build.log, trigger_build_attempt.log.

Fix validation (Phase 8)

fix_status: not_testable β€” but validated as far as the guest allows:

  1. Diff applies cleanly: patch -p1 sys/bus/smbus/ichsmb/ichsmb.c fix.diff β†’ Hunk #1 succeeded at 570. PATCH_EXIT=0. Also git apply --check passes on the host.
  2. Kernel builds: make -j6 nativekernel KERNCONF=X86_64_GENERIC β†’ === NK_DONE rc=0 ===. No errors or warnings on ichsmb.c.
  3. Kernel boots: installed /usr/obj/usr/src/sys/X86_64_GENERIC/kernel.stripped β†’ /boot/kernel/kernel and rebooted; sysctl kern.version β†’ DragonFly 6.5-DEVELOPMENT #1: Wed Jul 15 00:01:56 UTC 2026.
  4. Patched source verified: grep -A3 'block_count = bus_read_1' /usr/src/sys/bus/smbus/ichsmb/ichsmb.c shows the clamp at the right line.
  5. Algorithmic closure verified: the harness's "PATCHED" variant produces zero OOB bytes β€” the clamp turns the 223-byte OOB into a clean 32-byte in-bounds write.

A dynamic before/after on a running kernel is not possible on this guest because there is no ichsmb PCI device and no /dev/smb0. The closure is proven at the algorithmic level (the only place the bug path can be exercised here), and the patched kernel compiles and boots cleanly. On a guest with a real (or emulated) ichsmb controller + malicious slave, the same harness logic would manifest as: panic on lockmgr (unpatched) vs clean transaction completion (patched).

Apply fix.diff (matches the finding's ## Recommended fix proposal β€” clamp block_count to sizeof(sc->block_data) immediately after the bus_read_1(sc->io_res, ICH_D0) read at ichsmb.c:585). The finding's alternative one-liner (also bounding the loop test at ichsmb.c:580) is equivalent but slightly less preferred because it leaves block_count itself oversized and may interact with the LAST_BYTE logic at ichsmb.c:589-596; the load-time clamp keeps all downstream math correct.

Kernel references (confirmed during verification)

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_testable (no ichsmb PCI). Compile+boot+harness validated: UNPATCHED 223B OOB, PATCHED 0B.

BEFORE: 223B OOB. AFTER: 0B. Build rc=0. Boot #1.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Wed Jul 15 00:01:56 UTC 2026

Confirmed kernel references

Detail

Exploit chain

none -- device-only (malicious SMBus peripheral or root /dev/smb0). No unprivileged syscall surface. Valid hard blocker.

Evidence (decisive lines)

UNPATCHED: 223B OOB (64 mutex + 159 canary). PATCHED: 0B. No /dev/smb0 on guest.

PoC changes

Authored: harness.c (ISR logic + softc layout), fix.diff (clamp block_count to sizeof(block_data) at :586), VERDICT.md, manifest.json.

Verified recommended fix

Clamp sc->block_count to sizeof(sc->block_data) at ichsmb.c:576 after reading from ICH_D0. Matches finding proposal. Full diff in findings/poc/DF-1076/fix.diff.

Verdict

REPRODUCED (harness). ichsmb.c:575 slave count unclamped -> :583 block_data[32] indexed with count=255 -> 223B OOB write (64 into mutex + 159 canary). No ichsmb PCI on guest. Harness: UNPATCHED 223B OOB, PATCHED 0B.