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

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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2681 Β· 9 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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
  • kldload never returned (> 4 minutes observed; date still worked, ps -ax and kldstat hung, 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_testable
↓ fix.diffper-fix-DF-2681

Confirmed kernel references

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.