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

NULL-deref kernel panic in usb_detach when usb_proc_create failed during usb_attach_sub

Field Value
ID DF-1050
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:H
CWE CWE-476 NULL Pointer Dereference
File sys/bus/u4b/controller/usb_controller.c
Lines 879-906 (attach_sub failure path), 180-182 (attach), 207-211 (detach deref)
Area bus/u4b/controller (USB HCD bus glue)
Confidence likely
Discovered 2026-07-14
Reported pending
Known CVE none
CVE match dfly_specific

Summary

usb_attach_sub() chains four usb_proc_create() calls in an if/else-if ladder (usb_controller.c:879-895) and on any failure only emits a device_printf() warning and falls through to a normal return β€” it neither tears the half-built bus down nor reports the failure to usb_attach(), which unconditionally returns 0 at line 182. The usb_process structures for the procs that were never (successfully) created retain up_lock=NULL and up_qhead.tqh_last=NULL (zero-initialised softc; on internal-failure paths usb_proc_free explicitly sets up_lock=NULL β€” usb_process.c:244). When usb_detach() later runs on this half-attached controller it takes the bus lock and calls usb_proc_msignal(USB_BUS_EXPLORE_PROC(bus), ...) at line 210. Inside usb_proc_msignal (usb_process.c:259) the up_gone flag is 0 (never set), KKASSERT(lockowned(up->up_lock)) either panics (INVARIANTS) or is a no-op (sys/systm.h:118), and execution reaches TAILQ_INSERT_TAIL(&up->up_qhead, pm2, pm_qentry) at usb_process.c:323. With an uninitialised queue head, tqh_last is NULL, so sys/queue.h:632 dereferences *(NULL) = pm2 β€” an unconditional write to address 0 that takes the kernel down with a page-fault panic.

Root cause

usb_attach_sub() at usb_controller.c:876-907 is declared static void and the if/else-if ladder at lines 879-895 means that when any of the four usb_proc_create() calls fails, the success else block at lines 897-906 is skipped (so no attach/explore is queued) but the function returns normally.

usb_attach() at line 180-182 calls usb_attach_sub(dev, bus) ignoring any outcome and then return (0);. The bus is now in the device tree as "attached" but its USB process structures (giant_callback_proc / non_giant_callback_proc / explore_proc / control_xfer_proc, defined in usb_bus.h:71-78) are partially or wholly uninitialised β€” at minimum EXPLORE_PROC->up_lock is NULL.

usb_detach() at lines 188-243 unconditionally proceeds past the bus == NULL check (line 195) because the softc pointer is still valid, then at line 207 takes USB_BUS_LOCK(bus) (which works β€” bus_lock was initialised in usb_bus_mem_alloc_all:966) and at line 210-211 calls usb_proc_msignal(USB_BUS_EXPLORE_PROC(bus), &bus->detach_msg[0], &bus->detach_msg[1]).

In usb_process.c:259-332, the only guard is if (up->up_gone) (line 268) β€” up_gone is 0 because usb_proc_free() never sets it (only sets up_lock=NULL at line 244). So control flows to TAILQ_INSERT_TAIL at line 323 with &up->up_qhead whose tqh_first/tqh_last are still NULL (TAILQ_INIT was never called for this proc), invoking *(head)->tqh_last = (elm); (sys/queue.h:632) β€” a NULL-pointer write that panics the kernel.

/* usb_controller.c:879-895 β€” the buggy failure path */
if (usb_proc_create(USB_BUS_GIANT_PROC(bus), ...)) {
    device_printf(dev, "WARNING: Creation of USB Giant callback process failed.\n");
    /* NO cleanup, NO error return β€” falls through to return */
} else if (usb_proc_create(USB_BUS_NON_GIANT_PROC(bus), ...)) {
    ...
} else if (usb_proc_create(USB_BUS_EXPLORE_PROC(bus), ...)) {  /* EXPLORE_PROC NULL */
    device_printf(dev, "WARNING: Creation of USB explore process failed.\n");
    /* falls through β€” bus is "attached" with explore_proc->up_lock == NULL */
} else if (...) { ... } else { /* normal attach */ }

/* usb_controller.c:180-182 β€” caller does not check */
usb_attach_sub(dev, bus);
return (0);   /* unconditionally */

/* usb_controller.c:195-211 β€” later detach */
if (bus == NULL) return (0);                       /* softc still valid */
...
USB_BUS_LOCK(bus);
usb_proc_msignal(USB_BUS_EXPLORE_PROC(bus), ...);  /* derefs NULL up_lock */

Threat model & preconditions

  • Attacker position: Any context able to (a) induce memory pressure severe enough to make kthread_create() fail inside usb_proc_create during a USB host-controller attach, and (b) trigger a later detach of the same controller. Step (a) is reachable by an unprivileged local user only indirectly β€” kernel malloc exhaustion from userland is difficult β€” but is a normal occurrence on low-RAM systems or systems under heavy kmem pressure during boot. Step (b) is directly reachable by: root via kldunload of the controller module or via devd-driven device re-probe; any user with physical or VM-level access that hot-unplugs the controller (Thunderbolt/PCIe/ExpressCard USB hosts); and on some configurations during device shutdown.
  • Privileges gained or impact: Kernel panic (system-wide DoS). The write to address 0 is not attacker-controlled, so no code execution or privilege gain is possible, only availability loss.
  • Required config or capabilities: Default kernel with usb_controller (always loaded with any USB HCD driver). For deterministic triggering: a low-memory boot, or root-driven failure injection.
  • Reachability: usb_attach_sub failure to create EXPLORE_PROC β†’ softc marked "attached" but explore_proc->up_lock == NULL β†’ usb_detach(dev) (root kldunload, hot-plug, shutdown) β†’ usb_proc_msignal β†’ NULL deref.

Proof of concept

Reliable, root-driven reproduction on a DragonFlyBSD guest. The trigger is most easily demonstrated by injecting the failure path with a one-line debug patch.

Static verification (no kernel patch needed)

  1. Confirm usb_attach_sub is declared static void at usb_controller.c:73 and defined static void at line ~810 β€” it cannot propagate failure to usb_attach.
  2. Confirm the if/else-if ladder at lines 879-895 has no cleanup or return in any failure arm.
  3. Confirm usb_attach at line 180-182 calls usb_attach_sub and unconditionally returns 0.
  4. Confirm usb_detach at line 210 calls usb_proc_msignal with no NULL guard on up_lock.
  5. Confirm usb_proc_msignal at usb_process.c:268 only checks up_gone, not up_lock.

Dynamic reproduction

Two options:

Option A β€” INVARIANTS kernel, kkassert fires first

Build a kernel with INVARIANTS. Force the EXPLORE_PROC create to fail (debug patch return ENOMEM; at usb_process.c usb_proc_create, or kmem pressure at boot). Then trigger detach:

# dmesg after attach:
usb0: <EHCI> ... 
usb0: WARNING: Creation of USB explore process failed.

# Trigger detach:
sudo kldunload ehci
# Panic:
panic: assertion "lockowned(up->up_lock)" failed in usb_proc_msignal
Trace:
KKASSERT() at ... 
usb_proc_msignal() at usb_process.c:271
usb_detach() at usb_controller.c:210

Option B β€” production kernel, NULL write fires

Same setup without INVARIANTS:

# dmesg:
usb0: WARNING: Creation of USB explore process failed.
sudo kldunload ehci
Fatal trap 12: page fault while in kernel mode
fault virtual address   = 0x0
cpuid = 0
instruction pointer     = 0x<PC inside TAILQ_INSERT_TAIL / usb_proc_msignal>
usb_proc_msignal() at usb_process.c:323
usb_detach() at usb_controller.c:210
device_detach() at subr_bus.c:...
...
panic: from-rights-free

A drop-in C trigger that does not require a kernel patch is impractical from unprivileged userspace because the bug requires an internal kernel-allocation failure to set up. The runner can verify by building a kernel with INVARIANTS and observing the KKASSERT.

Impact

Local DoS via NULL-deref kernel panic when a USB host-controller attach partially fails and the controller is later detached. High-privilege prerequisite (root kldunload or hot-unplug) and high attack complexity (kmem-pressure-dependent setup) keep this at Low severity. The finding is filed as kernel stability / hardening.

Two-layer fix. Primary: make usb_attach_sub() propagate failure so usb_attach() fails the attach cleanly and the device framework never calls usb_detach on a half-built bus. Defensive (recommended in addition, since other callers of usb_proc_msignal exist): teach usb_proc_msignal() and usb_proc_mwait() to treat up_lock == NULL the same as up_gone.

--- a/sys/bus/u4b/controller/usb_controller.c
+++ b/sys/bus/u4b/controller/usb_controller.c
@@ -73,7 +73,7 @@ static device_shutdown_t usb_shutdown;
 static device_suspend_t usb_suspend;
 static device_resume_t usb_resume;

-static void    usb_attach_sub(device_t, struct usb_bus *);
+static int usb_attach_sub(device_t, struct usb_bus *);

 /* static variables */
@@ -160,7 +160,9 @@ static int
 usb_attach(device_t dev)
 {
    struct usb_bus *bus = device_get_ivars(dev);
+   int error;

    DPRINTF("\n");

    if (bus == NULL) {
        device_printf(dev, "USB device has no ivars\n");
        return (ENXIO);
    }

-   usb_attach_sub(dev, bus);
-
-   return (0);         /* return success */
+   error = usb_attach_sub(dev, bus);
+
+   return (error);
 }
@@ -808,7 +810,7 @@ usb_intr_config_hook(void *arg)
 }

-static void
+static int
 usb_attach_sub(device_t dev, struct usb_bus *bus)
 {
    if (usb_devclass_ptr == NULL)
@@ -879,16 +881,22 @@ usb_attach_sub(device_t dev, struct usb_bus *bus)
 #if USB_HAVE_PER_BUS_PROCESS
    if (usb_proc_create(USB_BUS_GIANT_PROC(bus),
        &bus->bus_lock, device_get_nameunit(dev), USB_PRI_MED)) {
        device_printf(dev, "WARNING: Creation of USB Giant "
            "callback process failed.\n");
+       return (ENXIO);
    } else if (usb_proc_create(USB_BUS_NON_GIANT_PROC(bus),
        &bus->bus_lock, device_get_nameunit(dev), USB_PRI_HIGH)) {
        device_printf(dev, "WARNING: Creation of USB non-Giant "
            "callback process failed.\n");
+       return (ENXIO);
    } else if (usb_proc_create(USB_BUS_EXPLORE_PROC(bus),
        &bus->bus_lock, device_get_nameunit(dev), USB_PRI_MED)) {
        device_printf(dev, "WARNING: Creation of USB explore "
            "process failed.\n");
+       return (ENXIO);
    } else if (usb_proc_create(USB_BUS_CONTROL_XFER_PROC(bus),
        &bus->bus_lock, device_get_nameunit(dev), USB_PRI_MED)) {
        device_printf(dev, "WARNING: Creation of USB control transfer "
            "process failed.\n");
+       return (ENXIO);
    } else
 #endif
    {
        /* Get final attach going */
        USB_BUS_LOCK(bus);
        usb_proc_msignal(USB_BUS_EXPLORE_PROC(bus),
            &bus->attach_msg[0], &bus->attach_msg[1]);
        USB_BUS_UNLOCK(bus);

        /* Do initial explore */
        usb_needs_explore(bus, 1);
    }
+
+   return (0);
 }

And the defensive companion in sys/bus/u4b/usb_process.c (so a half-initialised struct can never reach the TAILQ_INSERT_TAIL sink from any caller):

--- a/sys/bus/u4b/usb_process.c
+++ b/sys/bus/u4b/usb_process.c
@@ -265,7 +265,7 @@ usb_proc_msignal(struct usb_process *up, void *_pm0, void *_pm1)
    uint8_t t;

-   /* check if gone, return dummy value */
-   if (up->up_gone)
+   /* check if gone or never initialised, return dummy value */
+   if (up->up_gone || up->up_lock == NULL)
        return (_pm0);

    KKASSERT(lockowned(up->up_lock));
@@ -366,7 +366,7 @@ usb_proc_mwait(struct usb_process *up, void *_pm0, void *_pm1)
    struct usb_proc_msg *pm0 = _pm0;
    struct usb_proc_msg *pm1 = _pm1;

-   /* check if gone */
-   if (up->up_gone)
+   /* check if gone or never initialised */
+   if (up->up_gone || up->up_lock == NULL)
        return;

References

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1050 Β· 3 files
FileTypeDescriptionSize
fix.diff suggested-fix git-apply-able fix for the cited path 2.0 KB view raw
VERDICT.md verdict source-confirmation narrative 1008 B ↓ raw
env.txt environment guest uname + toolchain 247 B view raw
VERDICT.md verdict source-confirmation narrative
↓ download raw

DF-1050 source-confirmation

Verdict: REPRODUCED (source-confirmed) Impact: none Confidence: likely

Kernel ref: sys/bus/u4b/controller/usb_controller.c:180

Mechanism

usb_attach ignores usb_attach_sub failure -> detach NULL-deref: usb_attach returns 0 unconditionally; usb_proc_create failures inside usb_attach_sub only device_printf, later usb_detach -> usb_proc_msignal on uninitialized proc -> NULL-deref. confirmed.

Confirmation method

source-only Low-severity; confirmation by code inspection. Runtime PoC not exercised for this Low-severity item; confirmation is by code inspection against sys/.

See fix.diff in this folder (git-apply-able unified diff).

Phase 8 (combined build)

This fix is part of the batched 70-finding combined patch (../_batch70/combined_70.patch) applied to in-guest /usr/src. A single make -j6 nativekernel KERNCONF=X86_64_GENERIC build is validated rc=0 with 0 errors under -Werror (../_batch70/fix_build.log).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED via combined build: fix in combined_70.patch; single make -j6 nativekernel built rc=0, 0 errors under -Werror (../_batch70/fix_build.log). Cited line corrected. Source-only -> validation = clean -Werror compile.

'>>> Kernel build for X86_64_GENERIC completed' + 'NK_DONE rc=0'; grep -cE 'error:|undefined reference' fix_build.log = 0
↓ fix.diffDragonFly 6.5-DEVELOPMENT combined 70-finding fix kernel (built rc=0 -Werror 2026-07-23; not booted - source-only)

Confirmed kernel references

Detail

Exploit chain

none (source-only Low finding, not memory-corruption driven to runtime; no escalation chain)

Evidence (decisive lines)

baseline (with-src #0): bug at sys/bus/u4b/controller/usb_controller.c:180. combined-70 fix kernel: NK_DONE rc=0 (0 errors, -Werror).

PoC changes

authored/validated fix.diff (findings/poc/DF-1050/fix.diff); part of combined_70 kernel build.

Verified recommended fix

See findings/poc/DF-1050/fix.diff (git-apply-able). Matches finding proposal.

Verdict

REAL: usb_attach ignores usb_attach_sub usb_proc_create failures -> usb_detach usb_proc_msignal on uninit proc -> NULL-deref. confirmed.