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 insideusb_proc_createduring 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 viakldunloadof the controller module or viadevd-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_subfailure to create EXPLORE_PROC β softc marked "attached" but explore_proc->up_lock == NULL βusb_detach(dev)(rootkldunload, 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)
- Confirm
usb_attach_subis declaredstatic voidatusb_controller.c:73and definedstatic voidat line ~810 β it cannot propagate failure tousb_attach. - Confirm the
if/else-ifladder at lines 879-895 has no cleanup orreturnin any failure arm. - Confirm
usb_attachat line 180-182 callsusb_attach_suband unconditionally returns 0. - Confirm
usb_detachat line 210 callsusb_proc_msignalwith no NULL guard onup_lock. - Confirm
usb_proc_msignalatusb_process.c:268only checksup_gone, notup_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.
Recommended fix
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
sys/bus/u4b/controller/usb_controller.c:879-906βusb_attach_subfailure pathsys/bus/u4b/controller/usb_controller.c:180-182βusb_attachunconditional successsys/bus/u4b/controller/usb_controller.c:207-211βusb_detachderef sitesys/bus/u4b/usb_process.c:244βusb_proc_freesetsup_lock = NULLon internal failuresys/bus/u4b/usb_process.c:268, 323βusb_proc_msignalchecksup_gonenotup_locksys/sys/queue.h:632βTAILQ_INSERT_TAILderefstqh_last- CWE-476 NULL Pointer Dereference
Timeline
- 2026-07-14 Discovered during automated audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1050 Β· 3 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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/.
Recommended fix
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
fixedVALIDATED 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
Confirmed kernel references
- s
- y
- s
- /
- b
- u
- s
- /
- u
- 4
- b
- /
- c
- o
- n
- t
- r
- o
- l
- l
- e
- r
- /
- u
- s
- b
- _
- c
- o
- n
- t
- r
- o
- l
- l
- e
- r
- .
- c
- :
- 1
- 8
- 0
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.
No comments yet.