bus_generic_delete_resource() dispatches BUS_DELETE_RESOURCE on itself instead of dev->parent: unbounded kernel recursion/livelock for any driver using the shipped generic default
| Field | Value |
|---|---|
| ID | DF-2681 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-674 Uncontrolled Recursion |
| File | sys/kern/subr_bus.c |
| Lines | 3145-3148 (exported via sys/sys/bus.h:274) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-29 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | base:kern |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
Every sibling bus_generic_* wrapper propagates to dev->parent
(e.g. subr_bus.c:3025, 3038, 3076, 3138); bus_generic_delete_resource()
passes dev itself, so kobj dispatch re-selects the same method
forever. With -O2 the tail call is a jmp β unbounded kernel loop
holding the kldload module lock; other frame layouts overflow the
kernel stack.
Threat model & preconditions
Latent in-tree (no current driver installs it) but it is the documented generic default; any kmod using it as designed livelocks the kernel. Root-gated (kldload).
Proof of concept
VERIFIED (findings/poc/DF-2681/): dfrec.ko is a minimal nexus bus
driver with DEVMETHOD(bus_delete_resource, bus_generic_delete_resource)
that calls bus_delete_resource() on its child from attach; kldload
prints "attached, calling bus_delete_resource() on child" and wedges
the machine permanently (kldload never returns, kldstat/ps hang, clean
shutdown times out, power-cycle required).
Recommended fix
One-liner: BUS_DELETE_RESOURCE(dev->parent, child, type, rid) β
dispatch then terminates at the first implementing ancestor or the kobj
default (ENXIO). Diff in findings/poc/DF-2681/fix.diff.
Timeline
- 2026-08-29 Discovered during pass-2 audit of subr_bus.c (GLM 5.3); wedge reproduced same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2681 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| dfrec.c | β | 2.1 KB | view raw | |
| Makefile | β | 70 B | β download | |
| build.sh | β | 237 B | view raw | |
| run.sh | β | 596 B | view raw | |
| run.log | β | 1.2 KB | view raw | |
| env.txt | β | 532 B | view raw | |
| fix.diff | β | 292 B | view raw | |
| VERDICT.md | β | 2.2 KB | β raw | |
| verdict.json | β | 2.4 KB | view raw |
DF-2681 β VERDICT
status: reproduced, impact: dos, confidence: certain.
Reproduced how
dfrec.ko (this pack) is a nexus bus driver that does exactly what the
exported API invites: DEVMETHOD(bus_delete_resource,
bus_generic_delete_resource), then calls bus_delete_resource() on
one of its children from attach.
On the stock INVARIANTS kernel:
dfrec0 on motherboard dfrec: attached, calling bus_delete_resource() on child
kldloadnever returned (> 4 minutes observed;datestill worked,ps -axandkldstathung, clean shutdown timed out, guest had to be force-killed from the host).- The follow-up kprintf (
dfrec: returned (no bug)) never executed.
Mechanism confirmation (source)
sys/kern/subr_bus.c:3144-3149:
bus_generic_delete_resource(device_t dev, device_t child, int type, int rid)
{
if (dev->parent)
BUS_DELETE_RESOURCE(dev, child, type, rid);
}
kobj dispatch (subr_kobj.c:160-191) resolves bus_delete_resource
on the first argument; passing dev re-selects this same method β
unbounded self-recursion. All sibling wrappers in the same file
(bus_generic_setup_intr:3025, teardown_intr:3038, config_intr:3065,
alloc_resource:3076, release_resource:3088, activate_resource:3099,
deactivate_resource:3110, set_resource:3138) pass dev->parent.
The observed livelock (rather than a stack-overflow trap) is explained
by the -O2 tail call: the recursive call is the last statement, so it
compiles to a jmp β the loop never grows the stack and never returns,
keeping the kldload thread's module lock held forever. A build with a
non-tail-call layout overflows the kernel stack instead (same root
cause, trap-flavored).
Severity rationale
Root-gated (kldload) and latent (no in-tree driver currently installs the wrapper) β Low. It is still a shipped, exported kernel API whose contract is "propagate to parent"; any kmod using it as designed livelocks the kernel permanently.
Fix validation
Not rebuilt (Low severity, one-line fix): the diff replaces dev with
dev->parent, matching every sibling wrapper; with the fix the
dispatch terminates at the first ancestor implementing
bus_delete_resource or hits the kobj default (kobj_error_method,
returns ENXIO) β no recursion.
Fix verification
not_testableConfirmed kernel references
- sys/kern/subr_bus.c:3144
- sys/kern/subr_bus.c:3148
- sys/sys/bus.h:274
- sys/kern/subr_kobj.c:160-191 (method dispatch on first arg)
Detail
Evidence (decisive lines)
run.log (kldload wedge narrative with timestamps); dfrec.c (PoC driver); boot.log serial excerpt in VERDICT.md; fix.diff (one-line dev->parent fix)
PoC changes
PoC authored fresh; initial compile needed the 'dragonfly -> /usr/src/sys' kmod symlink and bus_if.h/device_if.h in SRCS.
Verified recommended fix
In bus_generic_delete_resource(), dispatch BUS_DELETE_RESOURCE(dev->parent, ...) instead of BUS_DELETE_RESOURCE(dev, ...).
Verdict
bus_generic_delete_resource() (sys/kern/subr_bus.c:3144-3149) dispatches BUS_DELETE_RESOURCE() on 'dev' instead of 'dev->parent' (every sibling wrapper propagates to the parent), so any driver that installs this exported generic default as its bus_delete_resource method re-enters it forever. Reproduced with dfrec.ko, a minimal nexus bus driver doing exactly that: on kldload the console shows 'dfrec: attached, calling bus_delete_resource() on child' and kldload never returns; kldstat/ps hang, clean shutdown times out, the guest had to be force-killed (the -O2 tail call compiles to a jmp -> unbounded kernel loop holding the module lock; non-tail-call layouts overflow the kernel stack instead). Latent in-tree (no current driver installs it) but shipped as the documented generic default in sys/sys/bus.h:274.
No comments yet.