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

iicsmb block entry points perform zero count validation while the only /dev/smb caller passes an uninitialized stack byte: kernel-stack junk selects the on-wire transfer length (observed 0 and 255) and is disclosed back via smbcmd.rcount

Summary

bread feeds (int)*count into iicbus_read unchecked and bwrite sends unchecked - no [1,32] bound unlike every sibling; the /dev/smb path passes never-assigned bcount: same binary read req_len=0 in one process and 255 in a forked child (environment-dependent stack junk driving the wire length, disclosed via rcount). No overflow on in-tree paths (1024B buf) - explicitly checked. Bridge [1,32] validation + bcount seeding fix validated (deterministic, garbage rejected).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5523 Β· 13 files
FileTypeDescriptionSize
README.md β€” 2.0 KB ↓ raw
VERDICT.md β€” 3.0 KB ↓ raw
df5521_rig.c β€” 7.9 KB view raw
df5521_poc.c β€” 6.8 KB view raw
build.sh β€” 83 B view raw
load_chain.sh β€” 294 B view raw
build.log β€” 5.4 KB view raw
env.txt β€” 980 B view raw
run.baseline.log β€” 1.4 KB view raw
run.fixed.log β€” 900 B view raw
fix.diff β€” 1.4 KB view raw
manifest.json β€” 836 B view raw
verdict.json β€” 4.5 KB view raw

DF-5523 β€” iicsmb_bread/bwrite trust the caller's count with no validation; the only /dev/smb caller passes an uninitialized stack byte that selects the on-wire transfer length (observed 0 and 255) and leaks back through s.rcount

Files

df5521_rig.c / df5521_poc.c / build.sh / load_chain.sh shared rig (see DF-5521); proto and groom and race modes provide the three observations below. run.baseline.log uninit count = 0 (parent process) and = 255 (forked child) driving the wire length; value disclosed in s.rcount run.fixed.log deterministic after fix (validation + seeding) fix.diff two-part fix: bridge-side validation ([1,32], mirroring ichsmb) + smb.c initializes bcount

Reproduce

  1. chain up (load_chain.sh), rdelay=0
  2. /root/df5521_poc proto β†’ dmesg shows READ req_len=0 (uninit byte)
  3. /root/df5521_poc race-style child (or any different process context) β†’ dmesg shows READ req_len=255 β€” same slot, different junk
  4. fixed modules β†’ proto β†’ deterministic req_len=1/req_len=8, out-of-range counts rejected with SMB_EINVAL.

Why this is a real defect in the audited file

The smbus(4) block API bounds blocks at 32 bytes (SMBus spec; ichsmb enforces count < 1 || count > 32 β†’ SMB_EINVAL at both entry points, ichsmb.c bwrite/bread). iicsmb_bread (iicsmb.c:499) feeds (int)*count straight into iicbus_read() β€” no bound, no minimum β€” and iicsmb_bwrite (iicsmb.c:474) likewise. The bridge is the only smbus driver in the tree that performs zero validation on the caller-supplied count. Combined with the smb.c defect (u_char bcount; at smb.c:217 is never assigned before smbus_bread(bus, ..., &bcount, buf) at smb.c:323), an uninitialized kernel stack byte picks the transaction length β€” and is disclosed to the ioctl caller via the copied-back smbcmd.rcount (s->rcount = bcount side of the min() at smb.c:327-328).

VERDICT.md
↓ download raw

DF-5523 VERDICT β€” REPRODUCED (uninitialized kernel stack byte drives the SMBus transfer length through the unvalidated bridge count; one-byte disclosure to the caller)

The bug (two cooperating halves)

  1. Bridge (this file): iicsmb_bread() uses (int)*count as the iicbus_read() length with no validation (iicsmb.c:499-500), and iicsmb_bwrite() sends (int)count bytes (iicsmb.c:474). Neither enforces the SMBus block bounds that every other smbus driver enforces (ichsmb_bwrite/ichsmb_bread: if (count < 1 || count > 32) return (SMB_EINVAL);).
  2. Caller (sys/dev/smbus/smb/smb.c, routed): smbioctl's u_char bcount; (smb.c:217) is never assigned before smbus_bread(bus, s->slave, s->cmd, &bcount, buf) (smb.c:323). The clamp lines above it touch s->rcount, which is not the value passed down.

Net effect on an iicsmb-backed bus: an uninitialized kernel stack byte selects the on-wire transfer length (0..255), and β€” through s->rcount = min(s->rcount, bcount) + the ioctl copy-back β€” that uninitialized byte's value is disclosed to the caller.

Observations (production chain, stock modules)

  • parent process, fresh stack: READ req_len=0 six times β€” zero-length "successful" block reads; smbcmd.rcount returned 0 (the byte's value).
  • forked child (same binary, different stack history): READ req_len=255 β€” the bridge transferred 255 bytes driven by stack junk.
  • A 255-byte read stays inside smb.c's 1024-byte buf (SMB_MAXBLOCKSIZE, smb.h:62), so there is no overflow on this in-tree path β€” the candidates with 32-byte buffers (ipmi_ssif) pass an initialized 32. (This audit explicitly killed the "uninit bcount overflows a 32-byte stack buffer" hypothesis on that ground.)
  • groom mode (pre-filling smbioctl's stack buf via a prior BWRITE copyin) did not reach the compiler's slot for bcount in this build; the 0-vs-255 cross-process variance is the demonstrated evidence that the value is uninitialized memory.

Impact

Kernel uninitialized-memory disclosure (1 byte) to a root-only caller plus nondeterministic bus behavior (arbitrary-length wire reads). Not exploitable beyond root on the in-tree paths (device node 0600, ipmi's count initialized). Severity Low.

Fix (validated)

fix.diff: (a) validate *count/count in [1,32] at both bridge entry points (mirrors ichsmb; rejects garbage deterministically β€” observed: baseline garbage 0/255 both out of range β†’ SMB_EINVAL, no wire traffic); (b) smb.c: seed bcount = s->rcount after the clamp so the length comes from initialized state. Post-fix proto: deterministic req_len=1/req_len=8, rcount=8, six consecutive identical ioctls.

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

Stock: transfer length = uninitialized byte (observed 0 parent / 255 child; value disclosed via rcount). Patched (fix.diff: bridge [1,32] validation + smb.c bcount seeding): six consecutive BREADs all deterministic (req_len=1 count-byte read then req_len=8, rcount=8, identical rbuf); garbage counts (0 or 255) now rejected with SMB_EINVAL before any wire traffic. Validated as part of the combined fixed-module build shared with DF-5521/5522/5525.

run.fixed.log vs run.baseline.log; fix.diff
↓ fix.diffper-fix-DF-5523

Confirmed kernel references

Detail

Exploit chain

root opens /dev/smb0 (0600) and issues SMB_BREAD -> smbioctl's uninitialized bcount -> iicsmb_bread reads (int)count off the caller's stack byte -> iicbus_read transfers that many bytes on the wire -> count (the byte's value) returns to smbioctl -> s->rcount = min(rcount, bcount) is copied back to userland with the ioctl: a 1-byte uninitialized-kernel-stack disclosure, and a stack-junk-chosen transaction length (0..255) visible on the bus.

Evidence (decisive lines)

run.baseline.log (decisive: 'READ req_len=0' x6 in the parent; 'READ req_len=255 ... READ gave 255 bytes' in the forked child -- same slot, environment-dependent value; rcount=0 disclosed; groom-mode negative result documented); run.fixed.log (deterministic req_len=1/req_len=8, rcount=8, six identical ioctls; out-of-range counts -> SMB_EINVAL before any wire traffic); fix.diff (bridge validation + smb.c seeding)

PoC changes

shared rig with DF-5521; proto mode extended with a groom mode (prior BWRITE copyin 32x0x99 then BREAD) to attempt controlled grooming of the bcount slot -- the compiler placed the slot outside the copyin region in this build (documented); the cross-process 0-vs-255 variance in proto/race modes is the actual demonstration

Verified recommended fix

Validate count/*count in [1,32] at both iicsmb block entry points (mirror ichsmb) AND initialize bcount in smbioctl's SMB_BREAD (bcount = s->rcount after the clamp). See fix.diff (validated live).

Verdict

REPRODUCED: iicsmb_bread feeds (int)*count straight into iicbus_read (iicsmb.c:499) and iicsmb_bwrite (iicsmb.c:474) sends (int)count -- zero validation, unlike every other smbus driver (ichsmb enforces [1,32] with SMB_EINVAL). The only /dev/smb block-read caller passes an UNINITIALIZED stack byte: smb.c:217 'u_char bcount;' is never assigned before smbus_bread at smb.c:323 (the adjacent clamps touch s->rcount, not the passed pointer). Live on the production chain: the bridge's transfer length read 0 in the parent process (zero-length 'successful' block reads, value disclosed to userland in the copied-back smbcmd.rcount) and 255 in a forked child (255-byte wire transfer) -- same binary, same ioctl: the length is uninitialized kernel stack memory. No overflow exists on in-tree paths (smb.c's buffer is SMB_MAXBLOCKSIZE=1024 > u_char max; ipmi_ssif passes an initialized 32 into a 32-byte buffer) -- that hypothesis was explicitly checked and killed. Root-gated disclosure + nondeterministic bus behavior => Low.