iicsmb_detach has no quiesce: kldunload during an in-flight SMBus transaction resumes the sleeper in freed module text (UAF) and leaves iicbus_softc->owner dangling
Summary
Detach never checks ownership/drains/child errors and returns 0; the second DRIVER_MODULE registration at :510 gives kldunload a teardown path bypassing detach entirely. Production-chain rig (isa->dfa_iic->iicbus->iicsmb->smbus->smb->/dev/smb0 from stock modules, rdelay-hold): mid-BREAD unload -> sleeper wakes executing inside the unloaded iicsmb.ko text (Fatal trap 12, VA in the module range) / second mode faulting on the freed smbus0; post-unload iicbus_intr dispatches through the dangling owner. Root/ipmi-poller gated; sharper than the amdsmb twin (cross-module text UAF). owner-EBUSY both paths fix validated live.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5521 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.6 KB | β raw | |
| VERDICT.md | β | 5.3 KB | β raw | |
| df5521_rig.c | β | 7.9 KB | view raw | |
| df5521_poc.c | β | 6.8 KB | view raw | |
| build.sh | β | 232 B | view raw | |
| load_chain.sh | β | 294 B | view raw | |
| run.sh | β | 232 B | view raw | |
| build.log | β | 5.4 KB | view raw | |
| env.txt | β | 980 B | view raw | |
| run.log | β | 2.7 KB | view raw | |
| run.fixed.log | β | 1.4 KB | view raw | |
| panic.txt | β | 1.4 KB | view raw | |
| fix.diff | β | 2.6 KB | view raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 6.9 KB | view raw |
DF-5521 β iicsmb detach has no quiesce: kldunload during an in-flight SMBus transaction executes freed module text (UAF) and leaves iicbus_softc->owner dangling
What this is
Evidence pack for the pass-2 finding against sys/bus/iicbus/iicsmb.c
(the I2C-to-SMBus bridge). Verified on the QEMU guest with a software
I2C controller KLD so that the production module chain
isa -> dfa_iic0 -> iicbus0 -> iicsmb0 -> smbus0 -> smb0 (/dev/smb0)
runs entirely on stock/production code paths (only the wire is software).
Files
df5521_rig.c software iicbus controller (isa child, wire-logged, configurable READ sleep = in-flight window, sysctl to inject iicbus_intr events like a real controller ISR) df5521_poc.c userland PoC (root): proto / groom / race / racefixed / fireevent modes against /dev/smb0 build.sh exact guest build commands load_chain.sh module load order bringing the chain up run.sh baseline reproduction (expects panic) panic.txt captured serial-console panic (freed-text execution) run.log baseline session transcript excerpts run.fixed.log fix-validation transcript (EBUSY + clean unload + alive) fix.diff validated fix (iiconf.c accessor + iicsmb.c guard + smbus.c guard) build.log decisive compiler/link lines (guest, -Werror, clean) env.txt guest uname / kldstat / toolchain manifest.json machine-readable catalog verdict.json machine verdict (persist_poc.py schema)
Reproduce (baseline)
vm.sh reset with-src,vm.sh up- push
df5521_rig.c df5521_poc.cto guest/root - guest:
sh /root/build.sh(see build.sh) - guest:
sh /root/load_chain.sh - guest:
sysctl hw.dfa.rdelay=300 - guest:
/root/df5521_poc race - EXPECT:
smb0: detached / smbus0: detached / iicsmb0: detached, then the sleeping child wakes and the console showsFatal trap 12: page fault ... fault virtual address = 0xffffffff8261f0bf(inside the unloaded iicsmb.ko text range) -> DDBdb>prompt.
Reproduce (fix validation)
Apply fix.diff inside the guest's /usr/src, rebuild iicbus.ko +
iicsmb.ko and (because smbus is compiled into X86_64_GENERIC) the
kernel (make nativekernel && make installkernel, reboot), then:
kldload iicbus && kldload iicsmb && kldload smb && kldload df5521_rig.ko sysctl hw.dfa.rdelay=300 /root/df5521_poc racefixed
EXPECT: kldunload: can't unload file: Device busy while the child is
mid-BREAD, child continues unharmed, and after the client stops the
unload succeeds; guest alive.
DF-5521 VERDICT β REPRODUCED (kernel UAF / freed-module-text execution; fix validated)
The bug
iicsmb_detach() (sys/bus/iicbus/iicsmb.c:164-175) has no quiesce of any
kind: no busy count, no drain, no check whether the bridge currently owns
the underlying iicbus, and it ignores bus_generic_detach()'s error before
unconditionally deleting its smbus child. An SMBus client transaction
routed through the bridge (every /dev/smb0 ioctl, every ipmi_ssif
request, every kernel smbus client) holds the iicbus for the whole
transaction (smbioctl -> smbus_request_bus -> iicsmb_callback
-> iicbus_request_bus(parent, iicsmb0) sets iicbus_softc->owner,
smb.c:240-243,356) and can be asleep inside iicbus_read()/iicbus_write()
(controller polling / bit-bang delays) below the bridge.
devclass_delete_driver() (sys/kern/subr_bus.c:853-899) force-calls
device_detach() for every device of the driver and, on success, the
kldunload completes and frees the module text. Because iicsmb.ko also
registers the smbus driver on the iicsmb bus (iicsmb.c:510,
DRIVER_MODULE(smbus, iicsmb, ...)), unloading iicsmb.ko force-detaches
smbus0/smb0 through that registration without ever calling
iicsmb_detach β so even a guard in iicsmb_detach alone does not close
the window (verified: two crashes through exactly that path during fix
development).
Reproduction (baseline, stock modules)
Rig: software iicbus controller on isa (df5521_rig.c) -> production
chain iicbus0/iicsmb0/smbus0/smb0 with /dev/smb0 (0600 root).
hw.dfa.rdelay=300 makes the controller's READ sleep 300 ms so a BREAD
ioctl is observably in flight. PoC race: child loops SMB_BREAD; parent
kldunload mid-transaction. Captured serial (panic.txt):
READ req_len=255 <== bridge *count smb0: detached smbus0: detached iicsmb0: detached df5521-wire: READ gave 255 bytes: 08 a0 a1 a2 ... Fatal trap 12: page fault while in kernel mode fault virtual address = 0xffffffff8261f0bf fault code = supervisor read instruction, page not present current process = 989 Stopped at iicbus_devclass+0x1e3e7:
The faulting address 0xffffffff8261f0bf lies inside iicsmb.ko's unloaded
text range (kldstat before the unload: iicsmb.ko at
0xffffffff8261f000, size 0xe000): the thread that was asleep in the
controller read resumed executing freed module code after
kldunload iicsmb completed. A second crash mode (observed twice during
fix development, same race): the woken sleeper returns into
iicsmb_bread, then smbioctl calls smbus_release_bus() on the
already-freed smbus0 device -> Fatal trap 12 ... fault virtual address
= 0x0 ... Stopped at smbus_release_bus+0x78.
Additionally, after any successful unload iicbus_softc->owner keeps
pointing at the orphaned iicsmb0 device whose driver was unhooked, so the
next iicbus_intr() dispatches through a driverless kobj
(iiconf.c:50-51).
Threat model
Triggering requires root (open /dev/smb0 0600 + kldunload, or root's
kernel clients racing an admin unload β e.g. ipmi_ssif transactions from
/dev/ipmi0 or the SSIF poller). It is a genuine kernel UAF (execution
of freed text + freed-device dereference) but privileged-only: severity
Low, family-consistent with DF-5508 (amdsmb no-quiesce) which this audit
rates Info for the weaker lock-only variant.
Fix (validated)
fix.diff (3 files):
iiconf.c/iiconf.h: newiicbus_bus_owned(bus, dev)accessor.iicsmb.ciicsmb_detach(): return EBUSY while the bridge owns the iicbus; propagatebus_generic_detach()errors; NULL-safe child delete.smbus.csmbus_detach(): return EBUSY while a client holds the smbus β this closes thesmbus/iicsmbdriver-module unload path that bypassesiicsmb_detachentirely (iicsmb.c:510 registration).
Fix validation (guest, patched iicbus.ko/iicsmb.ko/smb.ko + rebuilt kernel because smbus is compiled into X86_64_GENERIC):
poc: >>> kldunload iicsmb mid-BREAD (expect EBUSY) <<< kldunload: can't unload file: Device busy poc: child still alive? yes (in-flight ioctls completing normally) poc: >>> kldunload iicsmb after client stopped (expect success) <<< rc=0 poc: firing INTR_STOP via iicbus_intr after clean unload ... poc: racefixed done -- GUEST ALIVE, fix validated
Baseline (no fix): panic in freed text. Patched: unload refused during the transaction, clean unload once quiesced, post-unload interrupt is a no-op (owner cleared). Residual: a complete fix would also pin the module across request/release (refcount) rather than refusing detach; the EBUSY guards close every demonstrated window.
Cross-references
sys/bus/iicbus/iicsmb.c:164-175(no-quiesce detach),:509-510(dual driver-module registrations).sys/kern/subr_bus.c:853-899(force-detach on unload).sys/bus/iicbus/iiconf.c:45-54,85-148(owner lifecycle, intr dispatch).sys/dev/smbus/smb/smb.c:240-243,356(transaction holds the bus).
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
fixedBaseline (stock iicbus.ko/iicsmb.ko/smb.ko/smbus-in-kernel): 1/1 rig runs panic -- Fatal trap 12 at 0xffffffff8261f0bf (freed iicsmb.ko text) after mid-BREAD kldunload; second crash mode smbus_release_bus+0x8 fault VA 0x0 observed twice during fix development. Patched (fix.diff: iicbus_bus_owned + iicsmb_detach EBUSY + smbus_detach EBUSY; kernel rebuilt because smbus is compiled into X86_64_GENERIC): 1/1 clean -- mid-BREAD kldunload refused with 'Device busy', child's in-flight ioctls complete normally, unload succeeds after the client stops, post-unload INTR_STOP no-op, guest alive. Residual (documented): a complete fix would pin the module across smbus_request_bus/release rather than refusing detach; every demonstrated window is closed by the guards.
run.fixed.log; run.log + panic.txt (before/after); fix.diff
Confirmed kernel references
- sys/bus/iicbus/iicsmb.c:164
- sys/bus/iicbus/iicsmb.c:175
- sys/bus/iicbus/iicsmb.c:509
- sys/bus/iicbus/iicsmb.c:510
- sys/bus/iicbus/iiconf.c:45
- sys/bus/iicbus/iiconf.c:50
- sys/bus/iicbus/iiconf.c:85
- sys/bus/iicbus/iiconf.c:141
- sys/kern/subr_bus.c:853
- sys/kern/subr_bus.c:899
- sys/dev/smbus/smb/smb.c:240
- sys/dev/smbus/smb/smb.c:356
- sys/bus/smbus/smbus.c:108
Detail
Exploit chain
root (or root-driven kernel client e.g. ipmi_ssif poller) issues /dev/smb0 SMB_BREAD -> smbus_request_bus sets iicbus_softc->owner=iicsmb0 for the whole ioctl -> controller polling sleeps inside iicbus_read under the bridge -> concurrent 'kldunload iicsmb' force-detaches smb0/smbus0/iicsmb0 (smbus_detach/iicsmb_detach return 0 unconditionally in stock code) and frees iicsmb.ko text + the smbus0 device -> sleeper resumes at a return address inside freed module text -> Fatal trap 12 (or returns through to smbus_release_bus on the freed smbus0 -> fault VA 0x0). No unprivileged trigger path exists (device node 0600 root:wheel); this is a privileged-user/kernel-lifecycle UAF, not an escalation primitive.
Evidence (decisive lines)
panic.txt (decisive: 'iicsmb0: detached' then sleeper wakes, 'Fatal trap 12 ... 0xffffffff8261f0bf ... supervisor read instruction, page not present', current process = the BREAD child; address inside the unloaded iicsmb.ko range from run.log's kldstat); run.log (full baseline session incl. module addresses); run.fixed.log ('kldunload: can't unload file: Device busy' mid-BREAD, clean unload after client stops, guest alive); fix.diff; df5521_rig.c / df5521_poc.c (race + racefixed modes)
PoC changes
no seed PoC existed (pass-2 discovery, the pcf.c audit's kldunload-not-pinned lead developed here); wrote df5521_rig.c (software iicbus controller on isa registering DRIVER_MODULE(iicbus, dfa_iic,...), wire-logged, hw.dfa.rdelay in-flight window, hw.dfa.fire_event ISR injector) + df5521_poc.c. Fixes during development: (1) extern iicbus_driver decls conflicted with iicbus.h (-Werror=redundant-decls); (2) rig modevent must run AFTER DRIVER_MODULE handlers (SI_ORDER_MIDDLE+1); (3) racefixed resolves the kld id from kldstat (path-loaded modules are not unloadable by name); (4) the PoC's looping child holds stdout -- run it detached with output to a file.
Verified recommended fix
iicsmb.c: refuse detach while the bridge owns the iicbus (new iicbus_bus_owned() accessor in iiconf.c/iiconf.h; return EBUSY) + propagate bus_generic_detach errors; smbus.c: smbus_detach returns EBUSY while sc->owner != NULL (closes the smbus/iicsmb driver-module teardown path of iicsmb.c:510). See fix.diff (validated live).
Verdict
REPRODUCED: iicsmb_detach (iicsmb.c:164-175) performs no quiesce -- no busy refcount, no drain, no iicbus-ownership check, and it ignores bus_generic_detach()'s error before deleting its smbus child -- while every SMBus client transaction routed through the bridge holds the iicbus (smb.c:240-243,356 via iicsmb_callback -> iicbus_request_bus) and can be asleep inside iicbus_read()/write(). devclass_delete_driver (subr_bus.c:853-899) force-detaches and the kldunload then frees module text. Live proof on the QEMU guest with a software-controller rig running the production chain (isa->dfa_iic0->iicbus0->iicsmb0->smbus0->smb0, /dev/smb0): kldunload iicsmb mid-BREAD -> 'smb0/smbus0/iicsmb0: detached' -> the sleeper wakes -> 'Fatal trap 12, fault virtual address = 0xffffffff8261f0bf' == inside the unloaded iicsmb.ko text range (kldstat: 0xffffffff8261f000+0xe000) -> execution of freed module code (2 crash modes observed; second: smbus_release_bus+0x78 on the freed smbus0, fault VA 0x0). Post-unload, iicbus_softc->owner dangles for the next iicbus_intr (iiconf.c:50-51). Root-gated (kldunload + /dev/smb0 0600) => severity Low. FIX VALIDATED: guard iicsmb_detach with EBUSY while iicbus_bus_owned() + the symmetric smbus_detach owner guard (required because iicsmb.c:510's DRIVER_MODULE(smbus, iicsmb) lets the unload tear the smbus child down WITHOUT calling iicsmb_detach -- observed live); patched modules + rebuilt kernel: mid-BREAD kldunload refused ('Device busy'), in-flight ioctls complete normally, clean unload after quiesce, post-unload INTR_STOP is a no-op, guest alive.
No comments yet.