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

smbus_request_bus returns success without acquiring ownership: wake-path break skips the ownership re-check and the EWOULDBLOCK->0 fixup converts the SMB_DONTWAIT refusal into a grant (phantom grants / double grants)

Summary

The ownership loop never re-checks sc->owner after smbus_poll: a woken waiter executes 'if (error==0) break' and returns 0 without ever setting sc->owner (only the else branch sets it) - every contended woken request yields a phantom holder and the next requester is granted too; additionally the EWOULDBLOCK->0 fixup erases the DONTWAIT fast-fail. The iiconf twin loops back and re-checks; smbconf does not. Because iicsmb's transfer methods perform no arbitration of their own, a phantom grant puts two kernel threads inside one SMBus transaction: ichsmb KASSERT panic class, interleaved I2C wire transactions, silently corrupted IPMI SSIF/cyapa transfers. Reproduced on the unmodified production stack (fakebb rig hosting real iicbb/iicbus/iicsmb + in-kernel smbus): phantom grant after real release+wakeup, double grant 0x3, DONTWAIT returned 0 while owner held, and (with DF-5637) a grant with no release at all. Loop-back + claim-then-zero fix validated on a rebuilt kernel (0x3->0x0, DONTWAIT correctly refused).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5636 Β· 15 files
FileTypeDescriptionSize
df5636_rig.c β€” 22.5 KB view raw
df5636ctl.c β€” 1.5 KB view raw
iicbb_reg.c β€” 491 B view raw
Makefile.rig β€” 279 B ↓ download
Makefile.reg β€” 228 B ↓ download
build.sh β€” 133 B view raw
run.sh β€” 166 B view raw
build.log β€” 1.1 KB view raw
run.log β€” 5.2 KB view raw
run_baseline.log β€” 5.2 KB view raw
run_fixed.log β€” 2.9 KB view raw
env.txt β€” 512 B view raw
fix.diff β€” 2.2 KB view raw
VERDICT.md β€” 5.4 KB ↓ raw
verdict.json β€” 5.1 KB view raw
VERDICT.md
↓ download raw

DF-5636 VERDICT β€” REPRODUCED

Finding: smbus_request_bus() (sys/bus/smbus/smbconf.c:111-144) returns success without acquiring ownership on two paths, because the ownership loop never re-checks sc->owner after smbus_poll() and converts poll's EWOULDBLOCK into success.

Root cause (path:line)

  1. Wake path phantom grant β€” smbconf.c:126-141. When the waiter is woken (smbus_poll returns 0 from tsleep), the loop executes if (error == 0) break; (smbconf.c:137-138) and returns 0 without re-entering the ownership check and without ever setting sc->owner = dev (only the else-branch at :134 sets it). Compare the iiconf.c twin loop (iiconf.c:99-114), which loops back and re-checks. Every contended request that is woken produces a caller that "holds" a bus nobody recorded: the next requester sees owner == NULL (or another device) and is granted too β€” two concurrent granted holders of the SMBus.

  2. DONTWAIT phantom grant β€” smbconf.c:131-132. smbus_poll() returns EWOULDBLOCK for SMB_DONTWAIT (default case, smbconf.c:96-98); the if (error == EWOULDBLOCK) error = 0; fixup erases the only failure signal of the fast-fail path, and the same break returns 0 while sc->owner still belongs to another device.

The unconditional wakeup(sc) at smbconf.c:177 (fired on every release, including the EACCES non-owner path) multiplies the trigger surface: any release anywhere on the bus wakes parked requesters into the phantom-grant path (proved by F3B: a grant while sc->owner verifiably still belonged to another device, with no release at all having occurred).

Reproduction (rig, production modules, stock kernel)

Chain: fakebb0 β†’ iicbb0 β†’ iicbus0 β†’ iicsmb0 β†’ smbus0 (all real; smbus is compiled into the guest's X86_64_GENERIC β€” config line 335 β€” so the code under test was the in-kernel production copy; wire sanity round-trip passed before every test).

  • F1 (run.log): devA owns; devB parks; devA releases β†’ T(devB): request RETURNED 0 with sc->owner=0 (caller=devB) <<< PHANTOM GRANT; devC then granted immediately β†’ double grant (summary=0x3).
  • F2: request(devB, SMB_DONTWAIT)=0 while owner was still another device β€” <<< PHANTOM GRANT (must be EWOULDBLOCK) (summary=0x1).
  • F3B: parked devB woken by a non-owner's EACCES release β†’ error=0 owner_seen=devA <<< GRANTED while another device verifiably OWNS the bus (no release ever happened) (summary=0x1).

Impact

Mutual exclusion is the smbus core's single responsibility: the transfer methods of the bridge (iicsmb.c:276-507 quick/sendb/recvb/writeb/writew/ readb/readw/pcall/bwrite/bread) perform no arbitration of their own β€” they rely entirely on the caller holding the bus. A phantom grant therefore puts two kernel threads inside one SMBus transaction concurrently: interleaved I2C wire transactions on iicbb/ig4-class controllers, and on ichsmb the documented single-transaction state machine trips its KASSERT(sc->ich_cmd == -1) (panic β€” the DF-5459 class, there caused by a driver no-op, here by the core itself). In-tree contending children exist on ordinary hardware (ipmi_ssif vs /dev/smb on server boards; cyapa's poll thread vs anything else on laptops). Triggering requires two bus users and either a wakeup (routine) or SMB_DONTWAIT (/dev/smb nonblocking ioctls, cyapa/atmel O_NONBLOCK opens β€” all root-only devices, or kernel threads), hence Medium severity / local DoS + silent SMBus transaction corruption (e.g. interleaved IPMI SSIF responses) rather than an unprivileged path.

No memory corruption is involved (the protocol hands out grants, not bytes), so there is no uid=0 chain; the ceiling is mutual-exclusion bypass with the consequences above.

Fix validation (same guest, kernel rebuilt)

fix.diff (smbconf.c): re-check ownership after every poll return; treat a WAIT poll timeout as a retry, never as success; keep DONTWAIT failures; error = 0 only when ownership is claimed; plus the DF-5637/5638 legs (release validates ownership before the bridge release, no EACCES wakeup, PCATCH + hz-bounded park). Applied to /usr/src, quickkernel rebuilt (smbconf.o recompiled), installed, rebooted (kernel #3):

  • F1 β†’ summary=0x0: F1.3w ... owner=<devB> (the woken waiter claimed), devC parked until released, final owner=0 (clean=NULL).
  • F2 β†’ request(devB, SMB_DONTWAIT)=35 (EWOULDBLOCK: correctly refused).
  • F3B β†’ devB still parked (no bogus wakeup: GOOD on fixed code).
  • Park β†’ S2 smbreq, kill -9 kills it.

Two intermediate fix iterations were required (first version leaked a stale EWOULDBLOCK through the claim path, second returned EWOULDBLOCK after one timeout instead of retrying); the final fix.diff in this pack is the validated one. The residual limitations (crit-section MP-unsafety is addressed separately as DF-5637; no detach drain β€” parking is now bounded and killable, which defuses the DF-5563-class unload UAF for this file) are noted in the pack README.

Classification

status=reproduced, reproduced=1, impact=dos, confidence=certain.

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

fix.diff applied to /usr/src/sys/bus/smbus/smbconf.c, kernel rebuilt (quickkernel, smbconf.o recompiled) and rebooted (kernel #3): F1 summary 0x3->0x0 with the woken waiter CLAIMING owner and devC parking; F2 0x1->0x0 with SMB_DONTWAIT refused EWOULDBLOCK; F3B 0x1->0x0 'no bogus wakeup: GOOD on fixed code'; park S2/killable (was D4/unkillable). Baseline behaviors all gone.

run_fixed.log (full fixed-kernel dmesg section) + run.log baseline differential; fix.diff sha256 346fe6893056aecf467e64d12285a0de337461aee150737964c0c3f5f06fbc2a
↓ fix.diffper-fix-DF-5636

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log F1 lines 'T(devB): request RETURNED 0' + 'sc->owner=0 (caller=devB) <<< PHANTOM GRANT' and F1.4 double grant; F2 line 'SMB_DONTWAIT)=0 ... <<< PHANTOM GRANT (must be EWOULDBLOCK)'; F3B line 'GRANTED while another device verifiably OWNS the bus'; run_fixed.log shows owner=devB claim, EWOULDBLOCK refusals and 'GOOD on fixed code' on kernel #3

PoC changes

seed sketch rewritten as the family-standard fakebb KLD rig: real production chain (iicbb/iicbus/iicsmb modules + in-kernel smbus), three distinct device_t owner tokens, in-kernel kthreads with started/done handshakes and owner-pointer sampling after every request; two rig iterations (build.sh Makefile copy bug; sys/string.h->bzero; robust cleanup after the DF-5637 teardown wedge; hz/2 release timing so the wakeup, not the 1s poll timeout, drives the fixed-kernel F1)

Verified recommended fix

Rewrite the smbus_request_bus ownership loop to re-check sc->owner after every smbus_poll return (continue on wake and on WAIT poll-timeout, unwind the bridge request only on interruption or refused DONTWAIT), set error=0 only when ownership is claimed, keep DONTWAIT EWOULDBLOCK; validate smbus_release_bus ownership BEFORE the parent callback and drop the EACCES-path wakeup; make smbus_poll always PCATCH with an hz bound (see pack fix.diff).

Verdict

smbus_request_bus (sys/bus/smbus/smbconf.c:126-141) returns success without acquiring ownership on two paths: after any wakeup (the loop's 'if (error == 0) break' exits without re-checking sc->owner or setting sc->owner = dev) and for SMB_DONTWAIT (the 'if (error == EWOULDBLOCK) error = 0' fixup converts the fast-fail refusal into a phantom grant). Reproduced on the unmodified production smbus stack (fakebb rig hosting real iicbb/iicbus/iicsmb + in-kernel smbus): a woken waiter returned 0 while sc->owner stayed NULL, a second requester was granted immediately (double grant), SMB_DONTWAIT returned 0 while another device verifiably owned the bus, and a non-owner's EACCES release (smbconf.c:177 unconditional wakeup) woke a parked waiter into a success return while the real owner still held the bus. The iicsmb bridge transfer methods perform no arbitration of their own, so a phantom grant puts two kernel threads inside one SMBus transaction (ichsmb KASSERT panic class, interleaved wire transactions elsewhere). fix.diff (re-check after poll, no EWOULDBLOCK-to-success, PCATCH+hz park, validate-before-bridge-release) was applied to /usr/src, the kernel rebuilt and rebooted: all phantom grants gone, DONTWAIT refused with EWOULDBLOCK, woken waiters claim ownership, park killable.