devclass_delete_driver() is non-transactional: the first detach veto irreversibly kills every already-detached unit of the driver while the module unload wedges (in-file root of the DF-5544/5562/5635 detach-veto family)
Summary
The per-unit loop returns on the first detach veto with no rollback - but device_detach success is destructive per device (softc freed, sysctl tree freed, devclass entry dropped, DS_NOTPRESENT): a veto from a LATER unit (open umass device, mounted fs, or the proven uhci/ohci/iicbus bugs) leaves every EARLIER unit permanently dead while the driver stays registered - retries skip the dead units and re-hit the same veto, and bus_generic_driver_added only re-probes on NEW driver registration which cannot occur while loaded. Live-verified: two-unit rig, unit-1 veto -> unit-0 dead (state=0/driver=0/softc=0, sysctl subtree gone, zombie visible to unpriv hw.bus.devices readers) with the module wedged. snapshot+rollback-on-veto fix validated on a patched kernel (veto returns EBUSY, earlier unit restored to DS_ATTACHED).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5789 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.2 KB | β raw | |
| VERDICT.md | β | 3.8 KB | β raw | |
| rig.c | β | 7.5 KB | view raw | |
| walker.c | β | 2.2 KB | view raw | |
| Makefile | β | 96 B | β download | |
| build.sh | β | 155 B | view raw | |
| run.sh | β | 94 B | view raw | |
| build.log | β | 979 B | view raw | |
| run5789.log | β | 1.4 KB | view raw | |
| run5790.log | β | 636 B | view raw | |
| fixrun5789.log | β | 1.5 KB | view raw | |
| fixrun5790.log | β | 636 B | view raw | |
| kbuild.log | β | 585.5 KB | β download | |
| kinstall.log | β | 80.4 KB | view raw | |
| env.txt | β | 260 B | view raw | |
| rig.txt | β | 3.5 KB | view raw | |
| fix.diff | β | 3.0 KB | view raw | |
| verdict.json | β | 4.3 KB | view raw | |
| manifest.json | β | 1.1 KB | view raw |
DF-5789 β newbus detach cascade is destructive-irreversible: devclass_delete_driver() aborts mid-loop on the first detach veto, leaving already-detached devices permanently dead while the driver stays registered and module unload is wedged
Root cause
sys/kern/subr_bus.c:888-897 (devclass_delete_driver):
for (i = 0; i < dc->maxunit; i++)
if (dc->devices[i]) {
dev = dc->devices[i];
if (dev->driver == driver && dev->parent &&
dev->parent->devclass == busclass) {
if ((error = device_detach(dev)) != 0)
return(error); /* <-- mid-loop abort */
device_set_driver(dev, NULL);
}
}
device_detach() (sys/kern/subr_bus.c:2128-2154) is destructive and
irreversible per device: on success it frees the softc
(device_set_driver(dev, NULL) at 2150 β kfree(dev->softc) at 1944),
tears down the device's sysctl tree (2151), removes the devclass entry for
non-fixed-class devices (2146-2147) and sets state = DS_NOTPRESENT
(2149). devclass_delete_driver() has no rollback: when a later
unit of the same driver vetoes detach (returns non-zero β e.g. EBUSY
from an open umass device, a mounted filesystem on the driver's disk, an
up network interface, or the driver bugs proven in DF-5544/5562/5635),
the function returns the error immediately, so:
- every unit detached before the veto is now permanently dead β no driver, no softc, no sysctl node β while the driver remains registered and the kldunload fails;
- the module unload is wedged: retries reach the same veto (earlier
units are skipped because
dev->driver == NULL), so the dead units can never be re-attached (bus_generic_driver_addedonly re-probesDS_NOTPRESENTchildren when a new driver is added,sys/kern/subr_bus.c:3007-3016, and no new driver-added event can occur while the module stays loaded); - the dead units linger as zombie entries in
bus_data_devices, visible to unprivileged users throughhw.bus.devices.
This is the in-subr_bus.c root of the detach-veto family proven this
session: DF-5544 (uhci), DF-5562 (ohci), DF-5635 (iicbus) are the
driver-side variants that swallow this same error.
Build
scp rig.c walker.c Makefile run.sh dfbsd:/root/df5789/
vm.sh run_root 'cd /root/df5789 && mv run.sh run5789.sh &&
make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC &&
cc -O -o walker walker.c'
Run
sh run5789.sh (root; leaves the module half-loaded on purpose, then
cooperates and unloads so the guest stays usable).
Expected (stock kernel)
kldunload rigβkldunload: can't unload file: Device busyAFTER dmesg showsdfveto0: detachedfollowed bydfveto1: DETACH VETO (EBUSY).- In-kernel status read:
dfveto0 ... state=0 driver=0 ... softc=0(DS_NOTPRESENT, driver NULL, softc freed) while the module is still loaded. sysctl dev.dfvetoshows only unit.1;dev.dfveto.0subtree gone.- Unprivileged
./walkerlists the zombie:[110] dfveto0 drv= state=NOTPRESENT parent=0xfffff8008bb7f800. - Second
kldunloadfails identically (permanent wedge for unit 1). - After
sysctl hw.df5789.cooperate=1+kldunload rig, the module unloads but dfveto0 was skipped (driver==NULL) β it only comes back if the module is loaded again (partial self-heal viabus_generic_driver_added); during any persistent veto the hardware unit is dead until reboot.
Fixed kernel (fix.diff: snapshot + rollback)
kldunloadstill properly fails withDevice busy(the veto is honored), butdev.dfveto.0is alive again:state=3(DS_ATTACHED), driver bound, sysctl node present β the rollback re-attached the units the aborted loop had detached.
Files
rig.cβ KLD: driverdfvetoon nexus (2 units, unit 1 vetoes untilhw.df5789.cooperate=1) + driverdfleak(softc 8192, used by DF-5790).walker.cβ unprivilegedhw.bus.deviceswalker (libdevctl method).run5789.sh/run5789.logβ baseline run.fixrun5789.logβ run against the fixed kernel.fix.diffβ git-apply-able fix (also fixes DF-5790).build.log,run5790.log,fixrun5790.log,kbuild.log,kinstall.logβ full logs.env.txt,rig.txtβ kernel log excerpts.
DF-5789 VERDICT β REPRODUCED (DoS / kernel state corruption, root- or transient-veto-triggered)
What was run
Rig KLD rig.ko on DragonFly 6.5-DEVELOPMENT (stock kernel #0 Thu Jul 2
06:02:54 UTC 2026, guest dfbsd, QEMU/KVM). Driver dfveto registered
on nexus via DRIVER_MODULE; its identify method adds two children:
unit 0 (detach succeeds) and unit 1 (detach returns EBUSY until
hw.df5789.cooperate=1). This is exactly what
devclass_delete_driver() sees when any device of a multi-unit driver
vetoes during kldunload (or PCI hot-unplug).
Baseline result (stock kernel) β bug reproduced
run5789.log + rig.txt (dmesg):
kldload ./rig.koβ dfveto0 + dfveto1 attach (dev.dfveto.0.%desc/dev.dfveto.1.%descboth present).kldunload rigβkldunload: can't unload file: Device busy. dmesg sequence:dfveto0: detaching OK/dfveto0: detachedthendfveto1: DETACH VETO (EBUSY): devclass_delete_driver() already killed dfveto0.- In-kernel status (sysctl read):
dfveto0=0xfffff80117494cc0 state=0 driver=0 nameunit='dfveto0' softc=0β the device is dead (DS_NOTPRESENT, driver unbound, softc freed, sysctl tree freed) while the module is STILL LOADED (sys/kern/subr_bus.c:888-897returned error after the destructive detach of unit 0). - Unprivileged view (
./walker, uses the same hw.bus.devices walk as devd):[110] dfveto0 drv= state=NOTPRESENT parent=0xfffff8008bb7f800β zombie device permanently in the tree. kldunloadretry #2 fails identically β the unload is wedged for as long as the veto persists; unit 0 is skipped forever (dev->driver == NULLfails the loop condition).- Recovery semantics characterized honestly: after clearing the veto
(
cooperate=1) the module unloads; reloading it revives dfveto0 viabus_generic_driver_added. So the damage is permanent for the duration of the veto; with a persistent veto (open umass device, mounted filesystem β things an unprivileged user can hold open) it is reboot-class.
Impact
Availability/DoS: transient detach vetoes during driver unload (kldunload, PCI/CardBus hot-removal) irreversibly kill every earlier-numbered unit of the driver while wedging the unload. An unprivileged user can cause the veto half (holding a device open) while root triggers the unload half. No memory corruption demonstrated; the proven DF-5544/5562/5635 driver bugs stack on top of this same non-transactional cascade.
Fix validation β FIXED
fix.diff (authored after verification, never applied to the repo's
sys/): devclass_delete_driver() snapshots the devclass device list
(devclass_get_devices) and, on the first detach veto, re-probes the
units it already detached (device_probe_and_attach) before returning
the error.
Applied inside the guest's /usr/src, make quickkernel
KERNCONF=X86_64_GENERIC β kernel #1 Mon Oct 12 18:33:11 UTC 2026,
installkernel, reboot (kbuild.log, kinstall.log).
Fixed-kernel run (fixrun5789.log):
* kldunload still fails with Device busy (veto honored);
* dfveto0 is alive: dev.dfveto.0.%driver: dfveto,
dev.dfveto.0.%parent: nexus0, in-kernel state=3 driver=0xffffffff82600880
softc=0xfffff80117ac7ac0 β no dead unit, no zombie, no wedge damage;
* final cooperative unload completes cleanly.
Baseline (dead unit + zombie + sysctl tree gone) vs patched (unit alive, proper EBUSY): fix confirmed.
Guest
Reset to the clean-source snapshot after validation (vm.sh reset
with-src); guest left up, kernel #0 stock.
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 to guest /usr/src, quickkernel built (#1 Mon Oct 12 18:33:11 UTC 2026), installed, rebooted: kldunload still correctly fails Device busy but the previously-killed unit returns to DS_ATTACHED with its driver bound and sysctl node present (fixrun5789.log); baseline kernel showed the unit dead with no sysctl node. Bad behavior gone.
fixrun5789.log vs run5789.log; kbuild.log/kinstall.log; dmesg 'DF5789: dfveto0 ... state=3 driver=0xffffffff82600880' on patched kernel vs 'state=0 driver=0 softc=0' on stock
Confirmed kernel references
Detail
Exploit chain
kldload rig.ko (dfveto0 detachable + dfveto1 veto) -> kldunload rig -> devclass_delete_driver detaches dfveto0 irreversibly, then dfveto1 returns EBUSY -> unload fails with module still loaded -> dfveto0 dead (state=0/driver=0/softc=0, dev.dfveto.0 gone) -> unpriv walker shows zombie -> every kldunload retry fails -> hardware unit lost until veto clears AND module is manually reloaded; persistent veto => reboot
Evidence (decisive lines)
run5789.log (kldunload 'Device busy' + dev.dfveto.0 subtree gone + walker zombie [110] dfveto0 state=NOTPRESENT); rig.txt dmesg ('dfveto0: detached' then 'dfveto1: DETACH VETO', then 'DF5789: dfveto0 state=0 driver=0 softc=0'); fixrun5789.log (patched kernel: same EBUSY but dfveto0 back to state=3 with driver bound)
PoC changes
PoC authored fresh (no seed): KLD with two drivers on nexus (dfveto: identify adds wired units via BUS_ADD_CHILD so nexus_print_child gets its ivars - a raw device_add_child panicked nexus_print_child, which is a nexus.c assumption, not the target bug; dfleak: 8192-byte softc for DF-5790); walker.c reproduces libdevctl's hw.bus.devices walk for the unprivileged zombie view; kernel-thread not needed since this path does not self-deadlock.
Verified recommended fix
devclass_delete_driver(): snapshot dc's device list with devclass_get_devices() and on the first device_detach() veto re-probe (device_probe_and_attach) the units already detached before returning the error; see fix.diff
Verdict
devclass_delete_driver() (sys/kern/subr_bus.c:888-897) detaches a multi-unit driver's devices one at a time and returns on the first detach veto, after having irreversibly destroyed the bindings of every earlier unit (softc freed by device_set_driver(dev,NULL) at subr_bus.c:2150->1944, sysctl tree freed at 2151, devclass entry dropped at 2146-2147). Verified live: a two-unit driver whose unit 1 vetoes leaves unit 0 permanently dead (state=DS_NOTPRESENT, driver=NULL, softc=NULL, dev.dfveto.0 sysctl tree gone, zombie visible to unprivileged hw.bus.devices readers) while the kld stays loaded and every unload retry fails - the dead unit cannot re-attach because bus_generic_driver_added (subr_bus.c:3007-3016) only re-probes on new-driver registration. Wedge lasts as long as the veto (open umass device / mounted fs / DF-5544-class driver bug) persists; persistent vetoes require reboot. Driver-module unload is root-only, but unprivileged users can hold the veto open. No memory corruption in this path itself.
No comments yet.