sc->outq mbuf-queue race in ng_h4: IF_DEQUEUE in ng_h4_start (tty ctx) vs IF_DRAIN in disconnect/shutdown (netgraph ctx); NG_H4_LOCK is only per-CPU crit_enter
| Field | Value |
|---|---|
| ID | DF-0589 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-362 Race Condition (Concurrent Execution using Shared Resource); CWE-416 Use After Free |
| File | sys/netgraph7/bluetooth/drivers/h4/ng_h4.c |
| Lines | 88-89 (var.h), 579-647, 729-735, 886, 773 |
| Area | netgraph7 (Bluetooth H4 driver) |
| Confidence | likely |
| Discovered | 2026-07-02 |
| Reported | pending |
Summary
The per-node mbuf output queue sc->outq is mutated from two independent
execution contexts that hold no common cross-CPU synchronization primitive.
ng_h4_start() (invoked from the tty l_start line-discipline callback) performs
IF_DEQUEUE/IF_PREPEND on sc->outq while holding only tp->t_token,
with NG_H4_LOCK not held. ng_h4_disconnect(),
ng_h4_rcvmsg(NGM_H4_NODE_RESET), and ng_h4_shutdown() perform IF_DRAIN on
the same queue while holding only NG_H4_LOCK. But NG_H4_LOCK is defined
(sys/netgraph7/bluetooth/drivers/h4/ng_h4_var.h:88-89) as merely
crit_enter()/crit_exit() โ a per-CPU critical section that provides zero
cross-CPU exclusion. On an SMP system, two CPUs can both read
sc->outq.ifq_head in IF_DEQUEUE before either writes it back, causing both
to "dequeue" the same mbuf. One context then frees it (m_freem in IF_DRAIN),
the other continues to use it (clist_btoq reads m_data/m_len, m_free
re-frees) โ a use-after-free / double-free on kernel mbuf heap objects.
Root cause
-
NG_H4_LOCKis defined at sys/netgraph7/bluetooth/drivers/h4/ng_h4_var.h:88-89 as merelycrit_enter()/crit_exit(). DragonFlyBSD critical sections are per-CPU: they prevent preemption and defer IPIs on the current cpu only, but do not prevent another cpu from concurrently executing inside its own critical section. -
The
outqis astruct ifqueue(sys/net/if_var.h:120-126) โ a plain singly-linked list of mbufs threaded throughm_nextpkt, manipulated by the non-atomic macrosIF_DEQUEUE,IF_PREPEND,IF_ENQUEUE,IF_DRAIN. Each is a multi-instruction read-modify-write sequence with no atomicity. -
ng_h4_start(sys/netgraph7/bluetooth/drivers/h4/ng_h4.c:572) is registered as the ttyl_startmethod. When the tty layer calls it, it acquirestp->t_token(:579) but does not acquireNG_H4_LOCKaround theIF_DEQUEUEat :592 or theIF_PREPENDat :615. The onlyNG_H4_LOCKacquisitions inng_h4_startare briefcrit_enter/crit_exitpairs for stat counter updates (:601-603, :620-622, :638-644). -
ng_h4_disconnect(:716) acquiresNG_H4_LOCK(:729, crit_enter) and callsIF_DRAIN(&sc->outq)at :735. It does not acquiretp->t_token. Sincecrit_enteris per-CPU, this provides no protection againstng_h4_startrunning concurrently on another CPU. -
The concrete interleaving that produces a double-dequeue: both CPUs execute
IF_DEQUEUEsimultaneously, both readifq_head=mbuf_A. CPU0 then writesifq_head=A->m_nextpktand setsA->m_nextpkt=NULL. CPU1 readsA->m_nextpktafter CPU0 nulled it, sees NULL, setsifq_head=NULL. Both CPUs now hold mbuf A. CPU1 (inIF_DRAIN) callsm_freem(A). CPU0 (inng_h4_start) proceeds to use A:clist_btoq(mtod(A,...))reads freed memory, andm_free(A)double-frees. The intermediate mbuf B (formerlyA->m_nextpktbefore CPU0 nulled it) is also leaked from the queue. -
The same race exists between
ng_h4_start'sIF_DEQUEUEand theIF_DRAINinng_h4_rcvmsg'sNGM_H4_NODE_RESEThandler (:886) andng_h4_shutdown(:773, which drains without even holdingNG_H4_LOCK).
Threat model & preconditions
- Attacker position: local user holding the
SYSCAP_NONET_NETGRAPHcapability (required byng_h4_openat sys/netgraph7/bluetooth/drivers/h4/ng_h4.c:157 to set theBTUARTDISCline discipline). This capability is distinct from root and may be delegated to services or non-root users via DragonFlyBSD's caps(9) framework. - Privileges gained or impact:
- Minimum reliable: kernel panic (double-free detected by INVARIANTS allocator, or freed-mbuf KASSERT). System-wide DoS.
- Speculative escalation: with mbuf-cluster heap grooming (spraying
controlled content into the freed mbuf's slab slot), an attacker could
control the mbuf's
m_ext.ext_freefunction pointer, which is invoked bym_freeโ redirecting execution to a controlled address in ring 0 โ full local privilege escalation to uid 0. This chain is not yet verified. - Required config or capabilities:
SYSCAP_NONET_NETGRAPHcapability, SMP DragonFly system, and a serial/tty device reachable from the user (real UART, USB-serial, or pty pair). - Reachability:
1. Open a serial tty or pty pair, set
BTUARTDISCldisc viaTIOCSETD(ng_h4_opencreates the h4 netgraph node). 2. Open a netgraph control/data socket pair (NgMkSockNode) and connect a peer node to the h4 node'shook. 3. Send a burst of mbufs via the data socket to fillsc->outq(ng_h4_rcvdataenqueues them at :822). 4. Immediately issue anngctl disconnecton the hook, triggeringng_h4_disconnectโIF_DRAIN. 5. If the tty layer happens to callng_h4_start(l_start) concurrently to drain the queue, theIF_DEQUEUE/IF_DRAINrace fires.
Proof of concept
PoC source: findings/poc/DF-0589/race.c (sketch โ full driver to be
materialized by the per-PoC verifier using ngctl(8) library calls or raw
ng_socket sendmsg).
Build & run
cc -O2 -lpthread -o race race.c ./race # as user with SYSCAP_NONET_NETGRAPH capability, SMP box
Expected output
Kernel panic: double-free detected by mbuf allocator INVARIANTS
...
backtrace:
m_free+0x...
ng_h4_start+0x... (sys/netgraph7/bluetooth/drivers/h4/ng_h4.c:610)
Or, on a non-INVARIANTS kernel, silent corruption of the mbuf slab leading to
a later panic from use-after-free in clist_btoq / m_freem.
For the escalation variant, the verifier would: (a) groom the mbuf slab between
the m_freem (CPU1) and the m_free (CPU0) by spraying mbuf clusters with a
fake m_ext containing a controlled ext_free (e.g. pointing at a ROP gadget
or kernel function that overwrites ucred); (b) when CPU0 calls m_free on
the reclaimed mbuf, ext_free fires in ring 0 โ code execution. The precise
slab-size and victim-object analysis belongs in the per-PoC VERDICT.md.
Impact
- Blast radius: any SMP DragonFly system that exposes the
BTUARTDISCline discipline to aSYSCAP_NONET_NETGRAPH-capable user (Bluetooth services, jail setups delegating netgraph capabilities, etc.). - Severity rationale: Medium. The race is real and the resulting memory
corruption is genuine (UAF/double-free), but exploitation is gated by a
capability, the race window is narrow (the
outqis only 12 mbufs deep and tty draining is typically bursty not continuous), and the escalation chain from "race wins" to "code exec" requires sophisticated heap grooming that is unverified. CVSS 3.1 base โ 6.8 (Medium). The AGENT.md rubric's "kernel memory corruption โ High" bar is tempered here by the high race complexity (AC:H) and capability gate. - Reliability: race is retryable indefinitely; minimum-case panic likely reproducible within seconds-to-minutes on SMP hardware; escalation reliability is unverified.
Recommended fix
The root cause is that NG_H4_LOCK is crit_enter (per-CPU, no cross-CPU
exclusion) and is not even held during the IF_DEQUEUE/IF_PREPEND in
ng_h4_start. The fix requires two changes:
- Upgrade
NG_H4_LOCKfromcrit_enter/crit_exitto a real cross-CPU spinlock so that alloutqoperations are mutually exclusive across CPUs. - Wrap the
IF_DEQUEUEandIF_PREPENDinng_h4_startwithNG_H4_LOCK/NG_H4_UNLOCK.
Lock-ordering is safe: the only nesting is tp->t_token โ sc->lock (in
ng_h4_start), which is a consistent order across all call sites.
ng_h4_disconnect/rcvmsg/shutdown acquire only sc->lock (no
tp->t_token), so no inversion.
diff --git a/sys/netgraph7/bluetooth/drivers/h4/ng_h4_var.h b/sys/netgraph7/bluetooth/drivers/h4/ng_h4_var.h
--- a/sys/netgraph7/bluetooth/drivers/h4/ng_h4_var.h
+++ b/sys/netgraph7/bluetooth/drivers/h4/ng_h4_var.h
@@ -83,8 +83,10 @@ typedef struct ng_h4_info {
struct ifqueue outq; /* Queue of outgoing mbuf's */
#define NG_H4_DEFAULTQLEN 12 /* XXX max number of mbuf's in outq */
-#define NG_H4_LOCK(sc) crit_enter();
-#define NG_H4_UNLOCK(sc) crit_exit();
+ struct spinlock sc_lock; /* Protects outq + parser state */
+#define NG_H4_LOCK(sc) spin_lock(&(sc)->sc_lock)
+#define NG_H4_UNLOCK(sc) spin_unlock(&(sc)->sc_lock)
#define NG_H4_IBUF_SIZE 1024 /* XXX must be big enough to hold full
frame */
diff --git a/sys/netgraph7/bluetooth/drivers/h4/ng_h4.c b/sys/netgraph7/bluetooth/drivers/h4/ng_h4.c
--- a/sys/netgraph7/bluetooth/drivers/h4/ng_h4.c
+++ b/sys/netgraph7/bluetooth/drivers/h4/ng_h4.c
@@ -174,6 +174,8 @@ ng_h4_open(struct cdev *dev, struct tty *tp)
sc->outq.ifq_maxlen = NG_H4_DEFAULTQLEN;
ng_callout_init(&sc->timo);
+ spin_init(&sc->sc_lock, "ng_h4");
+
NG_H4_LOCK(sc);
/* Setup netgraph node */
@@ -589,9 +591,13 @@ ng_h4_start(struct tty *tp)
#else
while (1) {
#endif
/* Remove first mbuf from queue */
+ NG_H4_LOCK(sc);
IF_DEQUEUE(&sc->outq, m);
+ NG_H4_UNLOCK(sc);
if (m == NULL)
break;
@@ -612,8 +618,11 @@ ng_h4_start(struct tty *tp)
/* Put remainder of mbuf chain (if any) back on queue */
if (m != NULL) {
+ NG_H4_LOCK(sc);
IF_PREPEND(&sc->outq, m);
+ NG_H4_UNLOCK(sc);
break;
}
Note: the same pattern (crit_enter as the only outq lock, unprotected
IF_DEQUEUE in the l_start method) exists in sys/netgraph7/tty/ng_tty.c
and should be fixed there as well โ out of scope for this audit but flagged
for the maintainer.
References
- DragonFlyBSD
crit_enter(9): blocks local-CPU preemption only, not other CPUs. - DragonFlyBSD
spinlock(9): cross-CPU mutual exclusion. sys/net/if_var.h:120-126(struct ifqueue) andIF_DEQUEUE/IF_DRAINmacros in sys/net/if_var.h โ non-atomic RMW on singly-linked list.- The same class of bug was historically present in ppp(4) over tty and fixed in the network-stack push to per-queue locks; ng_h4 was missed.
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).