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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5563 Β· 24 files
FileTypeDescriptionSize
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/iicN 0600 + 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:

  1. timeout 0 β€” the sleep is unbounded;
  2. 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 before device_delete_child() frees the softc;
  3. the parked thread's return PC is inside iicbus.ko module text (iiconf.c compiles into iicbus.ko, sys/bus/iicbus/iicbus/Makefile), which kldunload unmaps (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.

VERDICT.md
↓ download raw

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:63 tsleep(sc, PCATCH, "iicreq", 0) β€” unbounded, channel = softc
  • iiconf.c:67 tsleep(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.c compiles into iicbus.ko, sys/bus/iicbus/iicbus/Makefile);
  • kldunload does not count sleeping threads as module references.

Consequences (both demonstrated or line-traced):

  1. 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.
  2. 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-calls IICBUS_CALLBACK(device_get_parent(bus)) on the freed device (iiconf.c:93) and the owner loop performs sc->owner = dev (iiconf.c:105) β€” an 8-byte write of a device_t into reclaimed memory, plus sc->owner reads at iiconf.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 about iicbus_intr dispatching 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 β†’ parked S2 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). See panic.txt, run_f1_variantB.log.
  • Variant A (in-tree entry): ./park iic β†’ blocking I2CSTART on /dev/iic0 β†’ iicioctl (iic.c:297-300) β†’ parked S0 iicreq; same unload chain (rc=0 Γ—5); kill -9 β†’ identical trap at 0xffffffff826001ab, current process 1480. See panic_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

fixed
baseline reproduced→ patch + rebuild →patched clean

With 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
↓ fix.diffper-fix-DF-5563

Confirmed kernel references

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.