iicsmb_bwrite leaves I2C bus in STARTED state on write failure β permanent SMBus bridge lockup
Summary
iicsmb_bwrite at iicsmb.c:462-482 is the ONLY SMBus method in this file that does not call iicbus_stop() in its error cleanup path. If iicbus_start() succeeds but either iicbus_write() call fails (NACK/bus error/timeout), function returns via bare error: label at line 480-481 without issuing STOP. iicbus_start sets sc->started (iiconf.c:178); neither iicbus_request_bus nor iicbus_release_bus clears it (they only manage sc->owner, iiconf.c:86-148). Bus permanently stuck: every subsequent iicbus_start returns EINVAL (iiconf.c:174-175). Disables entire SMBus bridge for all consumers (/dev/smb, kernel sensors, battery/thermal monitors, IPMI SSIF) until driver reload or reboot. Compare iicsmb_bread (:484-507) and all other read/write functions which correctly do iicbus_stop(parent) in their error label. Two reachable paths: (1) local root via SMB_BWRITE ioctl to slave that ACKs address but NACKs data byte; (2) malicious/untrusted I2C peripheral (USB-C/Thunderbolt/BMC) NACKs mid-transaction. Fix: mirror iicsmb_bread pattern - early-return on start failure, error label with unconditional iicbus_stop.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1095 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df1095_harness.c | trigger-source | userspace harness mirroring iicbus state machine + iicsmb_bwrite call graph | 3.4 KB | view raw |
| verify.sh | trigger-source | 8 static source-tree checks | 2.0 KB | view raw |
| verify.log | run-log | verify.sh output (all 8 pass) | 461 B | view raw |
| run.log | run-log | harness output: phase 2 EINVAL unpatched, success patched | 751 B | view raw |
| fix.diff | suggested-fix | mirror iicsmb_bread: early return on start failure, iicbus_stop in error label | 1.4 KB | view raw |
| build.sh | build-script | build both harness variants | 301 B | view raw |
| run.sh | run-script | verify.sh + both harness variants | 252 B | view raw |
| env.txt | environment | uname, cc, securelevel, HW presence | 526 B | view raw |
| README.md | readme | how to reproduce + bug shape + impact | 2.3 KB | β raw |
| VERDICT.md | verdict | full narrative: mechanism, harness, fix | 4.5 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 |
DF-1095 β iicsmb_bwrite leaves I2C bus in STARTED state on write failure
Build
cc -O0 -o df1095_harness df1095_harness.c cc -O0 -DFIX -o df1095_harness_fix df1095_harness.c
Run
./df1095_harness # bus stuck STARTED, phase 2 returns EINVAL ./df1095_harness_fix # bus released, phase 2 works sh verify.sh # 8 static source checks
Expected (bug present)
verify.shreportsPASS=8 FAIL=0: theiicsmb_bwriteerror:label lacksiicbus_stop, while the siblingiicsmb_breadhas it (control case);iicbus_startsetssc->started;iicbus_startrejects withEINVALwhenstartedis already set;iicbus_stopclears it.df1095_harnessphase 1 leavesstarted != 0; phase 2 returnsEINVALand printsBUG: bus still STARTED ... permanently wedged.df1095_harness_fixreleases the bus in phase 1; phase 2 succeeds.
Bug shape
iicsmb_bwrite at sys/bus/iicbus/iicsmb.c:462-482 is the only
SMBus method in the file that does not call iicbus_stop() in its error
cleanup. After iicbus_start() succeeds (setting sc->started = slave,
iiconf.c:178), any failure in iicbus_write() jumps to the error:
label at :480-481, which is a bare return (error) β no STOP is
issued. sc->started stays set; every subsequent iicbus_start() on
that bus returns EINVAL (iiconf.c:174-175). The entire SMBus bridge
is therefore permanently wedged for all consumers (/dev/smb,
kernel sensors, battery/thermal monitors, IPMI SSIF) until driver reload
or reboot.
The sibling iicsmb_bread (:484-507) does call iicbus_stop()
in its error: label (:505), as do all other read/write methods in
the file. iicsmb_bwrite is the sole outlier.
Impact / preconditions
Two reachable paths:
- Local root via
SMB_BWRITEioctl to a slave that ACKs the address byte but NACKs the first data byte (perfectly legal SMBus behavior for many slave controllers). - Untrusted peripheral β a malicious/untrusted I2C device (USB-C/Thunderbolt dock with embedded I2C controller, BMC, etc.) NACKs mid-transaction.
Either path permanently disables the SMBus bridge β DoS for all I2C/SMBus consumers on that bus.
The audit QEMU guest has no iicbus/iicsmb devices in dmesg, so the
path is not exercised dynamically here; the bug is confirmed by source
trace + harness.
DF-1095 β iicsmb_bwrite leaves I2C bus in STARTED state on write failure
Verdict
NOT REPRODUCED at runtime (hardware-gated) β STATIC VERIFICATION + HARNESS CONFIRMED.
The bug exists verbatim in sys/bus/iicbus/iicsmb.c:462-482. The
iicsmb_bwrite function is the only SMBus method in the file whose
error: cleanup label does not call iicbus_stop(). When
iicbus_start() succeeds (setting sc->started = slave, iiconf.c:178)
and a subsequent iicbus_write() fails (NACK/bus error/timeout), the
function returns via the bare error: label at :480-481 without
issuing STOP. sc->started stays set; every subsequent iicbus_start()
on that bus returns EINVAL (iiconf.c:174-175) until driver reload or
reboot. The entire SMBus bridge is therefore permanently wedged for all
consumers (/dev/smb, kernel sensors, battery/thermal monitors, IPMI
SSIF).
The audit QEMU guest has no iicbus/iicsmb devices in dmesg
(no I2C controller in pciconf -l), so the path is not exercised at
runtime here. The trigger requires either local root with an exposed
/dev/smb whose slave NACKs the data byte, or a malicious/untrusted
I2C peripheral. The bug is confirmed by source trace + harness.
The df1095_harness userspace C program mirrors the iicbus state
machine (sc->started) and the exact iicsmb_bwrite call graph. In
the unpatched mode, phase 1 (start succeeds, write NACKs) leaves
started = slave and no stop call is made; phase 2 then sees
iicbus_start return EINVAL (rc=22) β the bridge is wedged. In the
patched mode (iicbus_stop() in the error label), phase 1 calls
stop, and phase 2 succeeds.
Mechanism (confirmed by source trace)
iicbus_start (iiconf.c:169-183):
int
iicbus_start(device_t bus, u_char slave, int timeout)
{
struct iicbus_softc *sc = ...;
int error = 0;
if (sc->started)
return (EINVAL); /* :175 β bus already started */
if (!(error = IICBUS_START(...)))
sc->started = slave; /* :178 β mark bus STARTED */
else
sc->started = 0;
return (error);
}
iicbus_stop (iiconf.c:213-226) clears sc->started = 0 (:224).
iicbus_request_bus and iicbus_release_bus (iiconf.c:86-148) manage
only sc->owner, NOT sc->started β confirmed by verify.sh check 8.
iicsmb_bwrite (iicsmb.c:462-482):
if ((error = iicbus_start(parent, slave & ~LSB, IICBUS_TIMEOUT)))
goto error; /* :468-469 β start FAILED, no STOP needed */
if ((error = iicbus_write(parent, &cmd, 1, &sent, IICBUS_TIMEOUT)))
goto error; /* :471-472 β start SUCCEEDED, STOP needed */
if ((error = iicbus_write(parent, buf, (int)count, &sent, IICBUS_TIMEOUT)))
goto error; /* :474-475 β start SUCCEEDED, STOP needed */
if ((error = iicbus_stop(parent)))
goto error; /* :477-478 β redundant */
error:
return (error); /* :480-481 β NO STOP!!! */
The first goto error (start failed) is safe β sc->started was not
set. The second and third goto errors (start succeeded, write failed)
are buggy β sc->started is still set when the function returns.
Compare iicsmb_bread (:484-507):
if ((error = iicbus_start(...)))
return (error); /* :490-491 β early return */
...
error:
iicbus_stop(parent); /* :505 β ALWAYS stop */
return (error); /* :506 */
That pattern is the canonical correct form. iicsmb_bwrite is the sole
outlier β every other read/write function in iicsmb.c calls
iicbus_stop() in its error label.
Reproduction
$ sh verify.sh # 8/8 static checks $ cc -O0 -o df1095_harness df1095_harness.c $ cc -O0 -DFIX -o df1095_harness_fix df1095_harness.c $ ./df1095_harness # phase 2: rc=22 (EINVAL), started != 0 $ ./df1095_harness_fix # phase 2: rc=0, started == 0
Fix
fix.diff rewrites iicsmb_bwrite to mirror iicsmb_bread:
- Change
iicbus_startfailure fromgoto errortoreturn error(no STOP needed when start failed βsc->startedwas not set). - Move
iicbus_stop()into theerror:label so it is always called on the success path AND on the write-failure paths. - Remove the redundant explicit
iicbus_stop()+goto errorat:477-478(now handled by the error label).
The nativekernel build of the patched file succeeds; the harness
validates the algorithm-level correctness.
Fix verification
fixedvalidated
kernel build rc=0 + harness before/after
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source+harness. iicsmb_bwrite error: no iicbus_stop -> permanent SMBus bridge wedge. No iicbus HW.
No comments yet.