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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5521 Β· 15 files
FileTypeDescriptionSize
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)

  1. vm.sh reset with-src, vm.sh up
  2. push df5521_rig.c df5521_poc.c to guest /root
  3. guest: sh /root/build.sh (see build.sh)
  4. guest: sh /root/load_chain.sh
  5. guest: sysctl hw.dfa.rdelay=300
  6. guest: /root/df5521_poc race
  7. EXPECT: smb0: detached / smbus0: detached / iicsmb0: detached, then the sleeping child wakes and the console shows Fatal trap 12: page fault ... fault virtual address = 0xffffffff8261f0bf (inside the unloaded iicsmb.ko text range) -> DDB db> 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.

VERDICT.md
↓ download raw

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: new iicbus_bus_owned(bus, dev) accessor.
  • iicsmb.c iicsmb_detach(): return EBUSY while the bridge owns the iicbus; propagate bus_generic_detach() errors; NULL-safe child delete.
  • smbus.c smbus_detach(): return EBUSY while a client holds the smbus β€” this closes the smbus/iicsmb driver-module unload path that bypasses iicsmb_detach entirely (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

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

Baseline (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
↓ fix.diffper-fix-DF-5521

Confirmed kernel references

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.