iicbus_request_bus parks requesters on the softc with timeout-0 tsleep and no detach drain β kldunload under a parked waiter frees the softc and unmaps iicbus.ko text under it: freed-text execution (UAF) on resume, plus sc->owner write into freed memory on colliding wakeup
Summary
iicbus_poll tsleeps on the softc with timeout 0; iicbus_detach neither wakes nor drains sleepers, and kldunload force-detaches then frees softc+module text while the parked thread's saved RIP is inside iicbus.ko: the pending SIGKILL resumes the thread into unmapped text (both the direct entry and the real /dev/iic0 blocking-I2CSTART variant reproduced: Fatal trap 12 executing iicbus.ko+0x1ab after unload, guest dead), and a colliding wakeup gives an 8-byte sc->owner write into reclaimed slab. Distinct from DF-5521 (which drains only the holder side). ipmi_ssif/cyapa kthreads are in-tree victims. Bounded-PCATCH+drain fix validated (5 unloads rc=0, no panic, ENXIO via dying path).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5563 Β· 24 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df5563_rig.c | β | 12.7 KB | view raw | |
| iicbb_reg.c | β | 491 B | view raw | |
| park.c | β | 3.2 KB | view raw | |
| Makefile.rig | β | 202 B | β download | |
| Makefile.reg | β | 189 B | β download | |
| build.sh | β | 192 B | view raw | |
| build.log | β | 13.5 KB | view raw | |
| run_f1.sh | β | 1.3 KB | view raw | |
| run_f1a.sh | β | 1.2 KB | view raw | |
| run_f1_variantB.log | β | 747 B | view raw | |
| run_f1_variantA.log | β | 518 B | view raw | |
| run_f2.sh | β | 1.3 KB | view raw | |
| run_f2_baseline.log | β | 770 B | view raw | |
| run_f3.sh | β | 1.2 KB | view raw | |
| run_f3_baseline.log | β | 481 B | view raw | |
| run_fixed_validation.log | β | 1.1 KB | view raw | |
| panic.txt | β | 1.7 KB | view raw | |
| panic_variant_a.txt | β | 813 B | view raw | |
| env.txt | β | 400 B | view raw | |
| fix.diff | β | 5.2 KB | view raw | |
| README.md | β | 5.7 KB | β raw | |
| VERDICT.md | β | 5.1 KB | β raw | |
| manifest.json | β | 1.4 KB | view raw | |
| verdict.json | β | 4.8 KB | view raw |
DF-5563 β iicbus_request_bus parks threads on the softc with no timeout and no detach drain β freed-text execution (UAF) on module teardown
- File under audit:
sys/bus/iicbus/iiconf.c(pass 2) - Class: kernel use-after-free / execution of unloaded module text (memcorrupt)
- Severity: Low (root-gated trigger:
/dev/iicN0600 +kldunload; in-tree kthread victims: ipmi_ssif / cyapa racing an admin unload) - Verdict: REPRODUCED (kernel panic, both entry variants), fix validated
What the code does
iicbus_request_bus() (sys/bus/iicbus/iiconf.c:85-117) parks requesting
threads inside iicbus_poll() (iiconf.c:56-76):
case IIC_WAIT | IIC_INTR:
error = tsleep(sc, PCATCH, "iicreq", 0); /* iiconf.c:63 */
case IIC_WAIT | IIC_NOINTR:
error = tsleep(sc, 0, "iicreq", 0); /* iiconf.c:67 */
Three properties combine into a use-after-free:
- timeout 0 β the sleep is unbounded;
- the sleep channel IS the iicbus softc (
sc), and nothing in the detach path (iicbus_detach,sys/bus/iicbus/iicbus.c:156-162) wakes or drains sleepers beforedevice_delete_child()frees the softc; - the parked thread's return PC is inside
iicbus.komodule text (iiconf.ccompiles intoiicbus.ko,sys/bus/iicbus/iicbus/Makefile), whichkldunloadunmaps (devclass_delete_driver()force-detaches,sys/kern/subr_bus.c:853-899; module text freed at unload β same mechanism DF-5521 proved for iicsmb).
So: park a requester (any blocking /dev/iic0 ioctl β iic.c:297-300 β
or the smbus passthrough of SMB_WAIT), then kldunload the i2c stack,
then deliver the pending signal (or any colliding wakeup(sc) after the
softc memory is reused). The thread resumes at an address inside the
unmapped iicbus.ko text β page fault in kernel mode β panic.
A colliding-wakeup variant additionally re-enters the request loops with a
freed sc: sc->owner read (iiconf.c:101), sc->owner = dev write
(iiconf.c:105), and IICBUS_CALLBACK(device_get_parent(bus)) dispatch
through the freed device β a UAF write primitive into reclaimed memory.
Distinct from DF-5521: that finding (and its fix) quiesced the
transaction holder side (iicsmb_detach returning EBUSY while the
bridge owns the iicbus). A waiter β blocked because someone else owns
the bus, or because the bridge callback reports busy β never owns
anything, is invisible to that fix, and is exactly what this PoC parks.
Rig
Fake GPIO bridge (df5563_rig.c, evolution of the DF-5539/5558 fakebb
rig): nexus β fakebb0 β iicbb0 (real iicbb.ko) β iicbus0 (real iicbus.ko,
code under test) β iic0 (real iic.ko, /dev/iic0).
hw.df5563.busy=1 makes fakebb_callback(IIC_REQUEST_BUS) return
EWOULDBLOCK β the exact contract lpbb gives when the ppbus is held by
another client (sys/dev/misc/lpbb/lpbb.c:97-101 β ppb_request_bus β
EWOULDBLOCK, sys/bus/ppbus/ppbconf.c:458-493). /dev/df5563 ioctls call
iicbus_request_bus()/iicbus_release_bus() directly with each how.
/dev/iic0 requires the disclosed setup fixes DF-5559 (ivar accessor) +
DF-5560 (sc->sc_dev) applied to the guest /usr/src β same as the
DF-5558 evidence pack.
Reproduce
# on the guest as root, after staging /root/df5563, /root/df5559, /root/df5560 sh build.sh # rig KLDs + park cd /usr/src patch -p1 --fuzz=2 < /root/df5559/fix.diff patch -p1 --fuzz=2 < /root/df5560/fix.diff ( cd sys/bus/iicbus/iicbus && make ) ; ( cd sys/bus/iicbus/iic && make ) sh /root/df5563/run_f1.sh # variant B (rig device) -> panic # after reset + restage: sh /root/df5563/run_f1a.sh # variant A (/dev/iic0) -> panic
run_f1a.sh = variant A (in-tree entry): ./park iic issues a blocking
I2CSTART on /dev/iic0 β iicioctl β iicbus_request_bus(IIC_WAIT|IIC_INTR)
β parked at iicreq; then kldunload iic df5563_rig iicbb_reg iicbb iicbus
(all rc=0); then kill -9.
Expected output (baseline, both variants)
PID STAT WCHAN COMMAND 1480 S0 iicreq ./park iic <- parked, timeout 0 rc(iic)=0 ... rc(iicbus)=0 <- all five modules unloaded under it Fatal trap 12: page fault while in kernel mode fault virtual address = 0xffffffff826001ab <- iicbus.ko+0x1ab (kldstat: 0xffffffff82600000+0x1000) instruction pointer = 0x8:0xffffffff826001ab current process = 1480 <- the parked requester
panic.txt (variant B) and panic_variant_a.txt (variant A) hold the
serial-console captures; run_f1_variantB.log / run_f1_variantA.log
hold the driving-session output (kldstat before unload).
Fix
fix.diff (3 files: iiconf.c, iicbus.c, iicbus.h):
* iicbus_poll(): always PCATCH, always bounded (hz); timeout treated
as spurious wakeup so the IIC_WAIT contract is preserved;
* iicbus_request_bus(): restructured loops β DONTWAIT does one attempt
(also fixes DF-5565), dying checks after every sleep, waiters count
under a new softc spinlock;
* iicbus_detach(): sets dying, wakes sleepers, drains waiters to 0
before the softc/text go away.
Fix validation (run_fixed_validation.log): with the patched iicbus.ko,
the identical F1 sequence parks the requester, all five modules still
unload (rc=0 β detach now drains), no panic, guest healthy, and the
parker's ioctl returns ENXIO (errno 6) via the dying path. The attach
round trip (SANITY: write_ok=1 read_ok=1) passes on the patched module.
Threat model
Root on both sides (0600 device + kldunload). The kthread variant
(ipmi_ssif/cyapa parked on SMB_WAIT passthrough racing an admin
kldunload) needs no /dev node β an administrator unloading i2c
modules on a busy BMC server hits it with the same panic. Rated Low,
bucket memcorrupt, family-consistent with DF-5521.
DF-5563 VERDICT β REPRODUCED (kernel UAF: parked thread resumes into unloaded iicbus.ko text); fix validated
The bug (path:line)
iicbus_request_bus() (sys/bus/iicbus/iiconf.c:85-117) parks requesters
in iicbus_poll():
iiconf.c:63tsleep(sc, PCATCH, "iicreq", 0)β unbounded, channel = softciiconf.c:67tsleep(sc, 0, "iicreq", 0)β unbounded, no PCATCH (see DF-5564)
Nothing protects that sleep against teardown:
iicbus_detach()(sys/bus/iicbus/iicbus.c:156-162) only resets the bus and detaches children β it neither wakes nor drains request sleepers;devclass_delete_driver()(sys/kern/subr_bus.c:853-899) force-calls detach for every device of the driver when its module unloads, then the linker frees the module text β while the parked thread's saved RIP is inside that text (iiconf.ccompiles intoiicbus.ko,sys/bus/iicbus/iicbus/Makefile);kldunloaddoes not count sleeping threads as module references.
Consequences (both demonstrated or line-traced):
- Freed-text execution (demonstrated, both variants): signal the
parked PCATCH sleeper after unloading
iicbus.koβ it resumes at an unmapped address βFatal trap 12,supervisor read instruction, page not present. - Freed-softc channel + UAF write (line-traced): after the bridge
module unload frees the iicbus softc under the sleeper, any colliding
wakeup(sc)(the freed address reused as another object's sleep channel) wakes it with error==0 β the do-while re-callsIICBUS_CALLBACK(device_get_parent(bus))on the freed device (iiconf.c:93) and the owner loop performssc->owner = dev(iiconf.c:105) β an 8-byte write of a device_t into reclaimed memory, plussc->ownerreads atiiconf.c:101.
Distinction from the known family findings
- DF-5521 fixed the holder side (iicsmb_detach quiesce +
iicbus_bus_owned()). A waiter never owns the bus, so that fix does not cover this path β verified by reading the DF-5521 fix and by this PoC parking a pure waiter (the bridge callback reports busy; ownership is never granted). - DF-5490 (pcf NULL softc in
iicbus_intr) and the DF-5521 note aboutiicbus_intrdispatching through a driverless kobj (iiconf.c:50-51) are about the interrupt path, not the request/sleep path.
Reproduction (guest, stock iiconf.c; both variants)
Rig: df5563_rig.c β fake GPIO bridge under nexus; production chain
fakebb0 β iicbb0 (iicbb.ko) β iicbus0 (iicbus.ko) β iic0 (iic.ko,
/dev/iic0). hw.df5563.busy=1 β fakebb_callback returns EWOULDBLOCK
(lpbb/ppbus-held contract). /dev/df5563 ioctls drive
iicbus_request_bus with each how.
- Variant B (rig device):
./park intrβ parkedS2 iicreq;kldunload iic df5563_rig iicbb_reg iicbb iicbus(all rc=0);kill -9β Fatal trap 12, RIP 0xffffffff826001ab = unloaded iicbus.ko+0x1ab (kldstat before unload: iicbus.ko at 0xffffffff82600000, size 0x1000), current process = the parked pid (2031). Seepanic.txt,run_f1_variantB.log. - Variant A (in-tree entry):
./park iicβ blockingI2CSTARTon/dev/iic0βiicioctl(iic.c:297-300) β parkedS0 iicreq; same unload chain (rc=0 Γ5);kill -9β identical trap at 0xffffffff826001ab, current process 1480. Seepanic_variant_a.txt,run_f1_variantA.log.
The fault address is deterministic across variants and runs: it is the
instruction after tsleep's return path inside iicbus_poll.
Threat model
Root-gated on the trigger side (0600 /dev/iicN, kldunload), same band
as DF-5521. Kthread victims (ipmi_ssif at sys/dev/misc/ipmi/ipmi_ssif.c:81,181,
cyapa at sys/dev/smbus/cyapa/cyapa.c:1176,1624 pass SMB_WAIT through
smbus_request_bus β iicsmb_callback β iicbus_request_bus(parent, dev,
IIC_WAIT)) park at iiconf.c:67 with no PCATCH: an administrator
unloading i2c modules on a busy BMC server cannot even signal them.
Fix (validated)
fix.diff: bounded+PCATCH polling with timeout-as-spurious-wakeup;
restructured request loops (single DONTWAIT attempt β also DF-5565;
dying checks; spinlock-protected owner/dying/waiters);
iicbus_detach wakes and drains waiters before teardown.
Validation (run_fixed_validation.log, guest, patched iicbus.ko):
F1 sequence re-run β parker confirmed parked (S1 iicreq), all five
modules still unload rc=0 (detach now blocks until the waiter drains),
no panic, guest healthy (uptime), and the parker's ioctl returned
ENXIO (errno 6) via the dying path. Attach round trip
SANITY: write_ok=1 read_ok=1 passes on the patched module, and the
whole chain reloads cleanly afterwards.
patch -R -p1 --dry-run of the shipped fix.diff against the validated
tree succeeds β the diff is byte-equivalent to what was tested.
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
fixedWith fix.diff applied (iicbus.ko rebuilt+reloaded), the identical F1 sequence parks the requester, all five module unloads still return rc=0 (iicbus_detach now wakes+drains waiters), kill -9 finds the process already exited with ENXIO, no panic, guest healthy, and the attach sanity round trip (write_ok=1 read_ok=1) plus a full reload of the chain succeed. patch -R --dry-run confirms the shipped diff matches the validated tree.
findings/poc/DF-5563/run_fixed_validation.log (F1V_PASS / F2V_PASS / immediate EWOULDBLOCK / SANITY lines); fix.diff
Confirmed kernel references
- sys/bus/iicbus/iiconf.c:56-76
- sys/bus/iicbus/iiconf.c:63
- sys/bus/iicbus/iiconf.c:67
- sys/bus/iicbus/iiconf.c:85-117
- sys/bus/iicbus/iiconf.c:101-105
- sys/bus/iicbus/iicbus.c:156-162
- sys/bus/iicbus/iicbus/Makefile
- sys/kern/subr_bus.c:853-899
- sys/bus/iicbus/iic.c:297-300
Detail
Exploit chain
root: open /dev/iic0 (0600) blocking -> ioctl(I2CSTART) -> iicbus_request_bus(IIC_WAIT|IIC_INTR) -> fakebb/lpbb-style busy callback -> tsleep(sc, PCATCH, 0) parked forever; kldunload iic df5563_rig iicbb_reg iicbb iicbus (all rc=0, devclass_delete_driver force-detaches, softc+text freed); kill -9 -> PCATCH wake -> resume at unmapped iicbus.ko+0x1ab -> kernel page fault -> panic in DDB. Kthread variant needs no /dev node: ipmi_ssif/cyapa SMB_WAIT sleepers have no PCATCH at all (iiconf.c:67).
Evidence (decisive lines)
panic.txt and panic_variant_a.txt: 'Fatal trap 12 ... fault virtual address = 0xffffffff826001ab, instruction pointer = 0x8:0xffffffff826001ab, current process = 2031/1480' (kldstat in run_f1_variantB.log / run_f1_variantA.log shows iicbus.ko at 0xffffffff82600000 size 0x1000 = the unloaded text). run_fixed_validation.log: fixed kernel parks (S1 iicreq), all 5 unloads rc=0, NO panic, uptime healthy, ioctl returns ENXIO via drain.
PoC changes
Reused the DF-5539/5558 fakebb rig shape; added a busy-callback sysctl (hw.df5563.busy -> EWOULDBLOCK, the lpbb ppbus-held contract), a /dev/df5563 ioctl surface over iicbus_request_bus/release_bus with each how variant, and self/release owner tokens; fixed include set (sys/device.h for struct dev_ops), a missing how argument, and unused-variable errors; needed the disclosed DF-5559/DF-5560 setup fixes for the /dev/iic0 variant. Fix development: added sys/kernel.h (hz), sys/spinlock.h include in iicbus.h (incomplete struct spinlock), explicit softc cast in iicbus_attach, and timeout-as-spurious-wakeup in iicbus_poll after DF's tsleep(EWOULDBLOCK-on-timeout) made WAIT callers bail after 1s.
Verified recommended fix
Bounded PCATCH polling + dying/waiters drain protocol in iiconf.c/iicbus.c/iicbus.h (see fix.diff); also closes DF-5564/DF-5565.
Verdict
iicbus_request_bus() parks requester threads in tsleep(sc, PCATCH, "iicreq", 0) on the iicbus softc with no timeout and no detach drain (iiconf.c:63/67, iicbus.c:156-162); kldunload of the i2c stack frees the softc and unmaps iicbus.ko text under the parked thread, and the pending SIGKILL makes it resume at unloaded iicbus.ko+0x1ab -> Fatal trap 12, supervisor read instruction, page not present (captured for both the rig-device entry and the real /dev/iic0 I2CSTART entry, iic.c:297-300). Line-traced collateral: a colliding wakeup(sc) after softc reuse re-enters the loops and writes sc->owner (iiconf.c:105) through a freed device chain. Root-gated like DF-5521 but on the waiter side, which that fix does not cover.
No comments yet.