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

device_delete_child() on a probed-but-unattached (DS_ALIVE) device leaks the driver softc: device_detach()'s state short-circuit skips device_set_driver(dev, NULL) and kobj_delete frees only the device object

Summary

A probed child in DS_ALIVE holds driver+softc; delete calls detach which returns 0 immediately for state!=DS_ATTACHED without the teardown - so device_set_driver(dev,NULL) (the only softc-freeing path) never runs and kobj_delete frees only the device object. Live: 100 add->probe->delete cycles with an 8192-byte softc grew M_BUS 97.7K->898K->1.66M permanent across kldunload (no-probe control grows 0.1K). In-tree window via USB temp_dev churn; 8KB/cycle demonstrated. release-binding-before-delete fix validated (flat at 97.7K).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5790 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 2.8 KB ↓ raw
rig.c β€” 7.5 KB view raw
run.sh β€” 94 B view raw
run5790.sh β€” 892 B view raw
build.log β€” 979 B view raw
run.log β€” 636 B view raw
fixrun.log β€” 636 B view raw
fix.diff β€” 3.0 KB view raw
verdict.json β€” 3.5 KB view raw
manifest.json β€” 856 B view raw

DF-5790 β€” device_delete_child() on a probed-but-unattached (DS_ALIVE) device leaks the driver softc

Root cause

device_delete_child() (sys/kern/subr_bus.c:1283-1310) calls device_detach(child) first. For a child that was probed but never attached (state == DS_ALIVE, exactly the state device_probe_child() leaves at sys/kern/subr_bus.c:1486), device_detach() short-circuits:

    if (dev->state != DS_ATTACHED)
        return(0);          /* subr_bus.c:2136-2137 */

without running the teardown at sys/kern/subr_bus.c:2146-2151 β€” in particular without device_set_driver(dev, NULL), which is the only subr_bus path that frees the softc allocated during probe (sys/kern/subr_bus.c:1952-1954). device_delete_child() then destroys the device object (kobj_delete(child, M_BUS), sys/kern/subr_bus.c:1306), which frees the struct bsd_device only β€” kobj_delete() (sys/kern/subr_kobj.c:264-273) never touches the softc. The probe softc is leaked.

The probed-but-unattached delete is driven in-tree by the USB bus-explore path (sys/bus/u4b/usb_device.c:1223 creates the temp child, :1415 deletes it, with device_probe_and_attach() in between) and by the async-attach machinery; any driver that deletes children between probe and attach leaks driver->size bytes of M_BUS per cycle.

Build / Run

Same rig as DF-5789 (the KLD also registers driver dfleak, softc size 8192):

cd /root/df5789 && make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC
sh run5790.sh     # 100 control cycles (no probe) + 200 leak cycles

Expected (stock kernel)

  • kernel log: DF5790: 100 add/probe/delete cycles ... last child state=1 softc=0xfffff801186b4000 (LEAKED, never kfree'd) (state 1 = DS_ALIVE);
  • vmstat -m bus MemUse: 97.6K baseline β†’ 97.7K after control β†’ 898K after 100 probe cycles (+~800K = 100Γ—8192) β†’ 1.66M after 200 β€” and the 1.66M survives kldunload (permanent).

Fixed kernel (fix.diff: release driver in device_delete_child)

  • same 200 probe cycles: 97.7K β†’ 97.7K β†’ 97.7K (zero growth).

Files

rig.c (shared), run.sh (= run5790.sh), run.log, fixrun.log, build.log, fix.diff (also fixes DF-5789).

VERDICT.md
↓ download raw

DF-5790 VERDICT β€” REPRODUCED (unbounded M_BUS leak per probed-then-deleted device)

Mechanism (line-accurate)

  1. device_set_driver(child, drv) during probe allocates the softc β€” sys/kern/subr_bus.c:1952-1954 (dev->softc = kmalloc(driver->size, M_BUS, M_INTWAIT | M_ZERO)); device_probe_child() leaves the child in DS_ALIVE with driver+softc bound (sys/kern/subr_bus.c:1486).
  2. device_delete_child(parent, child) β†’ device_detach(child) β†’ short-circuit if (dev->state != DS_ATTACHED) return(0); (sys/kern/subr_bus.c:2136-2137) β€” the teardown block at 2146-2151 (which contains the only subr_bus device_set_driver(dev, NULL)) is skipped.
  3. kobj_delete((kobj_t)child, M_BUS) (sys/kern/subr_bus.c:1306) β†’ sys/kern/subr_kobj.c:264-273 β€” frees the device object only; the softc is orphaned β†’ leak of driver->size bytes per cycle.

Baseline result (stock kernel #0) β€” reproduced

run.log: with driver softc size 8192 β€”

stage M_BUS MemUse
baseline 97.6K
100 control add/delete cycles (no probe) 97.7K
100 add/probe/delete cycles 898K (+~800K = 100Γ—8192)
100 more 1.66M
after kldunload rig 1.66M (permanent)

kernel log: DF5790: ... last child state=1 softc=0xfffff801186b4000 (LEAKED, never kfree'd) β€” the deleted child was DS_ALIVE with a live softc pointer.

Control run isolates the cause precisely: without the probe (no softc) the identical churn grows M_BUS by ~0.1K total.

Impact

Unbounded kernel heap leak (M_BUS), permanent for the uptime. In-tree reachability today is narrow (USB temp_dev deletes between probe and attach; async-attach window; any third-party/newbus driver deleting children in the probed state), so rated Low β€” but the protocol flaw is certain: the delete path simply does not own the softc.

Fix validation β€” FIXED

fix.diff: in device_delete_child(), after device_detach(child) succeeds, add

    if (child->driver != NULL)
        device_set_driver(child, NULL);

which releases the driver binding and frees the softc before the object is destroyed. Patched kernel #1 (Oct 12 18:33:11): the identical 200-cycle run shows 97.7K β†’ 97.7K β†’ 97.7K (fixrun.log) β€” zero growth; the "LEAKED" path is gone (kernel still prints the state=1 softc pointer before delete, but the memory is now reclaimed).

Guest

Same rig/kernel builds as DF-5789; guest reset to clean-source snapshot afterwards.

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 reproduced

fix.diff applied in guest /usr/src, quickkernel #1 built+installed+rebooted: identical 200-cycle run shows M_BUS flat at 97.7K (vs +1.56M on stock); the softc is now freed by the added device_set_driver(child,NULL). Leak gone.

fixrun.log (97.7K before == after all cycles) vs run.log (97.7K -> 1.66M); kbuild.log/kinstall.log in ../DF-5789/
↓ fix.diffper-fix-DF-5790

Confirmed kernel references

Detail

Exploit chain

device_add_child(nexus0, dfleak, -1) -> device_probe_child() binds dfleak driver + allocates 8192-byte softc, child state=DS_ALIVE -> device_delete_child() -> device_detach short-circuit (no set_driver(NULL)) -> kobj_delete frees device struct only -> 8192 bytes of M_BUS leaked per cycle, unbounded, permanent for uptime

Evidence (decisive lines)

run.log (vmstat -m bus 97.6K -> 898K -> 1.66M, survives kldunload; control +0.1K); dmesg/rig.txt 'DF5790: 100 add/probe/delete cycles ... last child state=1 softc=0xfffff801186b4000 (LEAKED, never kfree'd)'; fixrun.log (patched kernel: 97.7K -> 97.7K after 200 cycles)

PoC changes

PoC authored fresh (shared rig with DF-5789); leak sysctl hw.df5789.leak drives device_probe_child() directly (exported at sys/bus.h:389) so the child is caught exactly in DS_ALIVE; hw.df5789.leakctl is the no-probe control isolating the softc as the leaked object

Verified recommended fix

device_delete_child(): after device_detach(child) succeeds, add 'if (child->driver != NULL) device_set_driver(child, NULL);' before deleting grandchildren/freeing the object; see fix.diff

Verdict

device_delete_child() (sys/kern/subr_bus.c:1283-1310) destroys a probed-but-unattached (DS_ALIVE) child without freeing its softc: device_detach()'s short-circuit at subr_bus.c:2136-2137 returns 0 for state != DS_ATTACHED, skipping the only teardown that calls device_set_driver(dev,NULL) (subr_bus.c:2150), so the softc kmalloc'd during probe (subr_bus.c:1952-1954) is orphaned when kobj_delete(child) (subr_bus.c:1306, subr_kobj.c:264-273) frees the device object. Verified live on stock kernel: 100 add/probe/delete cycles with an 8192-byte softc grow M_BUS by ~800K (97.7K -> 898K -> 1.66M after 200), permanent across kldunload; control cycles without the probe grow 0.1K. Kernel log proves the deleted child was state=1 (DS_ALIVE) holding a live softc pointer.