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)
PoC verification
Evidence pack
findings/poc/DF-5790 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 -mbusMemUse: 97.6K baseline β 97.7K after control β 898K after 100 probe cycles (+~800K = 100Γ8192) β 1.66M after 200 β and the 1.66M surviveskldunload(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).
DF-5790 VERDICT β REPRODUCED (unbounded M_BUS leak per probed-then-deleted device)
Mechanism (line-accurate)
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 inDS_ALIVEwith driver+softc bound (sys/kern/subr_bus.c:1486).device_delete_child(parent, child)βdevice_detach(child)β short-circuitif (dev->state != DS_ATTACHED) return(0);(sys/kern/subr_bus.c:2136-2137) β the teardown block at 2146-2151 (which contains the only subr_busdevice_set_driver(dev, NULL)) is skipped.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 ofdriver->sizebytes 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
fixedfix.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/
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.
No comments yet.