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

iicbus_detach swallows bus_generic_detach()'s error: a child that fails to detach leaves the bus marked detached + driverless with its softc freed while the child is still attached β€” the child's next iicbus_request_bus faults on device_get_softc(bus)==NULL (sc->owner at NULL+8)

Summary

bus_generic_detach's return is discarded and detach returns 0 unconditionally: a child refusing EBUSY still lets device_detach complete the teardown (softc freed, driver NULL) while the child stays attached - its next iicbus_request_bus reads sc->owner at NULL+8. Reproduced on stock: detach returned 0 despite REFUSING(EBUSY), post-state driverless/softc=0 with child still attached, then Fatal trap 12 fault VA 0x8 at iicbus_request_bus+0x89. The DF-5563 lead answered: the iiconf drain protocol does require this detach to wake/drain waiters (DF-5563's fix adds it); this is the independent swallowed-error defect at the same lines. propagate fix validated (returns 16, bus stays attached, no panic).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5635 Β· 11 files
FileTypeDescriptionSize
README.md β€” 3.5 KB ↓ raw
VERDICT.md β€” 1.6 KB ↓ raw
verdict.json β€” 4.4 KB view raw
manifest.json β€” 968 B view raw
fix.diff β€” 5.5 KB view raw
panic_5635_stock.txt β€” 2.5 KB view raw
run_c_panic_driver.log β€” 518 B view raw
run_d_fixed.log β€” 1.1 KB view raw
run_d_fixed_dmesg.txt β€” 2.0 KB view raw
build.log β€” 30.8 KB view raw
env.txt β€” 354 B view raw

DF-5635 β€” iicbus_detach swallows bus_generic_detach() failure β†’ driverless NULL-softc bus with an attached child β†’ NULL-page fault in iicbus_request_bus

  • File under audit: sys/bus/iicbus/iicbus.c (pass 2)
  • Class: lifecycle error-handling β†’ kernel NULL-page out-of-bounds read β†’ panic (local DoS)
  • Severity: Low (requires a child whose detach fails β€” root-gated setup; in-tree iicsmb's DF-5521-guarded detach is the natural producer)
  • Verdict: REPRODUCED (Fatal trap 12, fault VA 0x8, at iicbus_request_bus+0x89: movq 0x8(%r12),%rax = sc->owner, iiconf.c:101), fix validated

The defect

static int
iicbus_detach(device_t dev)                     /* iicbus.c:156-162 */
{
    iicbus_reset(dev, IIC_FASTEST, 0, NULL);
    bus_generic_detach(dev);                 /* return value DISCARDED */
    return (0);                              /* always success */
}

bus_generic_detach (subr_bus.c) detaches children and returns the first child's error (e.g. EBUSY from a busy client). iicbus_detach ignores it and returns 0, so device_detach(iicbus0) believes the driver detached cleanly and completes the teardown:

  • device_set_driver(iicbus0, NULL) β€” softc kfree'd (subr_bus.c, device_set_driver) and kobj switched to null_class;
  • dev->state = DS_NOTPRESENT.

…while the failing child is still DS_ATTACHED under the now-driverless bus. The child's very next bus operation, iicbus_request_bus(bus, dev, how) (iiconf.c:86-117), does

struct iicbus_softc *sc = device_get_softc(bus);   /* iiconf.c:88: NULL */
...
if (sc->owner && sc->owner != dev)                 /* iiconf.c:101: *0x8 */

β†’ read at address offsetof(struct iicbus_softc, owner) == 8 on a NULL base β†’ Fatal trap 12.

The DF-5563 question ("does the iiconf drain/dying protocol need anything from this file's detach?") is answered yes: DF-5563's validated fix already patches iicbus_detach with the dying-flag + wakeup(sc) + drain loop this file owes the waiter protocol β€” that half is DF-5563's and is not re-reported here. This finding covers the independent defect living at the same lines: the swallowed child-detach error.

Reproduction (guest, stock modules)

run_c_panic.sh / panic_5635_stock.txt β€” the rig attaches a client child (df5632k0) whose detach returns EBUSY on demand, then drives the production APIs:

[df5632] df5632k0 detach: REFUSING (EBUSY)
iicbus0: detached
[df5632] DETACH_BUS: device_detach(iicbus0) returned 0  (child df5632k0 busy=1)
[df5632] DETACH_BUS-post: ... driver=(NONE-DRIVERLESS) state=0 softc=0
[df5632] DETACH_BUS-post: child[3] devclass=df5632k unit=0 driver=df5632k state=3
[df5632] REQBUS: calling iicbus_request_bus(iicbus0, self, WAIT|INTR)
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x8
Stopped at      iicbus_request_bus+0x89:  movq 0x8(%r12),%rax

The swallow (returned 0 while the child refused), the inconsistent state (driverless bus with softc=0 + attached child), and the predicted NULL-deref (fault VA exactly offsetof(owner)=8) are all on the serial console.

Fix (validated)

fix.diff propagates the error: error = bus_generic_detach(dev); return (error); (and propagates iicbus_reset's error too; composes trivially with DF-5563's drain block). Fixed module (run_d_fixed.log): device_detach returned 16 (EBUSY), the bus stays driver=iicbus state=3 softc=0xfffff8008d581130 (attached, alive), the child stays attached, and iicbus_request_bus on the still-attached bus returns 0 (request+release clean). No panic anywhere.

VERDICT.md
↓ download raw

DF-5635 VERDICT β€” REPRODUCED (swallowed child-detach error β†’ NULL-softc deref panic), fix validated

See README.md for the full narrative. Decisive evidence (stock, panic_5635_stock.txt):

[df5632] df5632k0 detach: REFUSING (EBUSY)
iicbus0: detached
[df5632] DETACH_BUS: device_detach(iicbus0) returned 0  (child df5632k0 busy=1)   <- THE SWALLOW
[df5632] DETACH_BUS-post: iicbus=... driver=(NONE-DRIVERLESS) state=0 softc=0     <- softc freed
[df5632] DETACH_BUS-post: child[3] devclass=df5632k unit=0 driver=df5632k state=3 <- child still attached
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x8                                    <- offsetof(iicbus_softc, owner)
Stopped at      iicbus_request_bus+0x89:  movq 0x8(%r12),%rax   <- iiconf.c:101 sc->owner

Fixed (run_d_fixed.log): device_detach(iicbus0) returned 16 (EBUSY), bus stays driver=iicbus state=3 softc=0xfffff8008d581130, REQBUS: returned 0 on the attached bus, clean unload β€” the inconsistent state and the panic are gone.

DF-5563 lead answer: the iiconf drain/dying protocol DOES require this file's detach to wake/drain waiters β€” DF-5563's validated fix.diff already adds that block to iicbus_detach; this finding is the independent swallowed-error defect at the same lines.

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 iicbus.ko rebuilt from fix.diff, the same busy-child scenario returns EBUSY (16) from device_detach(iicbus0); the bus remains attached (driver=iicbus state=3, softc non-NULL), the child stays attached, and iicbus_request_bus on the attached bus succeeds (rc=0, clean release). The inconsistent-state NULL-softc panic path is eliminated.

run_d_fixed.log ('dfctl detach_bus: kernel rc=16', 'dfctl reqbus: kernel rc=0'); run_d_fixed_dmesg.txt ('DETACH_BUS: device_detach(iicbus0) returned 16', 'driver=iicbus state=3 softc=0xfffff8008d581130', 'RUN-D-DONE')
↓ fix.diffper-fix-DF-5635

Confirmed kernel references

Detail

Exploit chain

root: attach an iicbus client child that can refuse detach (rig df5632k0; in-tree: iicsmb while bridge-owned per DF-5521) -> force bus driver removal (kldunload path / devclass_delete_driver / device_detach) -> iicbus_detach swallows the child's EBUSY -> newbus completes teardown: softc freed, driverless, state DS_NOTPRESENT, child still attached -> any subsequent iicbus_request_bus(child) reads sc->owner at NULL+8 -> kernel page fault, panic.

Evidence (decisive lines)

panic_5635_stock.txt ('df5632k0 detach: REFUSING (EBUSY)', 'iicbus0: detached', 'device_detach(iicbus0) returned 0', 'driver=(NONE-DRIVERLESS) state=0 softc=0' with 'child[3] ... driver=df5632k state=3', then 'Fatal trap 12', 'fault virtual address = 0x8', 'Stopped at iicbus_request_bus+0x89: movq 0x8(%r12),%rax'); run_d_fixed.log ('DETACH_BUS: device_detach(iicbus0) returned 16', bus stays 'driver=iicbus state=3 softc=0xfffff8008d581130', 'REQBUS: returned 0')

PoC changes

Rig lives in ../DF-5632/ (df5632_rig.c): df5632k child driver whose detach returns EBUSY under hw.df5632.childbusy=1; /dev/df5632 ioctls drive production device_detach(iicbus0) and iicbus_request_bus().

Verified recommended fix

Propagate the failure: error = bus_generic_detach(dev); if (error) return error; (plus propagate iicbus_reset's error) -- see fix.diff; composes trivially with DF-5563's drain block at the head of the same function.

Verdict

REPRODUCED on stock iicbus.ko: iicbus_detach (iicbus.c:156-162) discards bus_generic_detach()'s error and returns 0 unconditionally, so a child that refuses to detach (EBUSY -- in-tree producer: DF-5521-guarded iicsmb) leaves iicbus0 marked detached + driverless with its softc kfree'd (device_set_driver(NULL)) while the child remains DS_ATTACHED; the child's next iicbus_request_bus derefs device_get_softc(bus)==NULL at iiconf.c:88/:101 -- Fatal trap 12, fault VA 0x8 (= offsetof(struct iicbus_softc, owner)), Stopped at iicbus_request_bus+0x89. Also answers the DF-5563 lead: yes, iiconf's drain/dying protocol requires the wake/drain from THIS detach, which DF-5563's validated fix already adds (not re-reported). fix.diff propagates the error; fixed: device_detach returns EBUSY, bus stays attached and servicable.