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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5789 Β· 19 files
FileTypeDescriptionSize
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:

  1. 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;
  2. 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_added only re-probes DS_NOTPRESENT children 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);
  3. the dead units linger as zombie entries in bus_data_devices, visible to unprivileged users through hw.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 busy AFTER dmesg shows dfveto0: detached followed by dfveto1: 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.dfveto shows only unit .1; dev.dfveto.0 subtree gone.
  • Unprivileged ./walker lists the zombie: [110] dfveto0 drv= state=NOTPRESENT parent=0xfffff8008bb7f800.
  • Second kldunload fails 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 via bus_generic_driver_added); during any persistent veto the hardware unit is dead until reboot.

Fixed kernel (fix.diff: snapshot + rollback)

  • kldunload still properly fails with Device busy (the veto is honored), but dev.dfveto.0 is 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: driver dfveto on nexus (2 units, unit 1 vetoes until hw.df5789.cooperate=1) + driver dfleak (softc 8192, used by DF-5790).
  • walker.c β€” unprivileged hw.bus.devices walker (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.
VERDICT.md
↓ download raw

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):

  1. kldload ./rig.ko β†’ dfveto0 + dfveto1 attach (dev.dfveto.0.%desc / dev.dfveto.1.%desc both present).
  2. kldunload rig β†’ kldunload: can't unload file: Device busy. dmesg sequence: dfveto0: detaching OK / dfveto0: detached then dfveto1: DETACH VETO (EBUSY): devclass_delete_driver() already killed dfveto0.
  3. 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-897 returned error after the destructive detach of unit 0).
  4. 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.
  5. kldunload retry #2 fails identically β€” the unload is wedged for as long as the veto persists; unit 0 is skipped forever (dev->driver == NULL fails the loop condition).
  6. Recovery semantics characterized honestly: after clearing the veto (cooperate=1) the module unloads; reloading it revives dfveto0 via bus_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

fixed
baseline reproduced→ patch + rebuild →patched reproduced

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

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.