โฌข DragonFlyBSD Kernel Audit
โ† dashboard
DF-0589

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

  1. NG_H4_LOCK is defined at sys/netgraph7/bluetooth/drivers/h4/ng_h4_var.h:88-89 as merely crit_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.

  2. The outq is a struct ifqueue (sys/net/if_var.h:120-126) โ€” a plain singly-linked list of mbufs threaded through m_nextpkt, manipulated by the non-atomic macros IF_DEQUEUE, IF_PREPEND, IF_ENQUEUE, IF_DRAIN. Each is a multi-instruction read-modify-write sequence with no atomicity.

  3. ng_h4_start (sys/netgraph7/bluetooth/drivers/h4/ng_h4.c:572) is registered as the tty l_start method. When the tty layer calls it, it acquires tp->t_token (:579) but does not acquire NG_H4_LOCK around the IF_DEQUEUE at :592 or the IF_PREPEND at :615. The only NG_H4_LOCK acquisitions in ng_h4_start are brief crit_enter/crit_exit pairs for stat counter updates (:601-603, :620-622, :638-644).

  4. ng_h4_disconnect (:716) acquires NG_H4_LOCK (:729, crit_enter) and calls IF_DRAIN(&sc->outq) at :735. It does not acquire tp->t_token. Since crit_enter is per-CPU, this provides no protection against ng_h4_start running concurrently on another CPU.

  5. The concrete interleaving that produces a double-dequeue: both CPUs execute IF_DEQUEUE simultaneously, both read ifq_head=mbuf_A. CPU0 then writes ifq_head=A->m_nextpkt and sets A->m_nextpkt=NULL. CPU1 reads A->m_nextpkt after CPU0 nulled it, sees NULL, sets ifq_head=NULL. Both CPUs now hold mbuf A. CPU1 (in IF_DRAIN) calls m_freem(A). CPU0 (in ng_h4_start) proceeds to use A: clist_btoq(mtod(A,...)) reads freed memory, and m_free(A) double-frees. The intermediate mbuf B (formerly A->m_nextpkt before CPU0 nulled it) is also leaked from the queue.

  6. The same race exists between ng_h4_start's IF_DEQUEUE and the IF_DRAIN in ng_h4_rcvmsg's NGM_H4_NODE_RESET handler (:886) and ng_h4_shutdown (:773, which drains without even holding NG_H4_LOCK).

Threat model & preconditions

  • Attacker position: local user holding the SYSCAP_NONET_NETGRAPH capability (required by ng_h4_open at sys/netgraph7/bluetooth/drivers/h4/ng_h4.c:157 to set the BTUARTDISC line 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_free function pointer, which is invoked by m_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_NETGRAPH capability, 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 BTUARTDISC ldisc via TIOCSETD (ng_h4_open creates the h4 netgraph node). 2. Open a netgraph control/data socket pair (NgMkSockNode) and connect a peer node to the h4 node's hook. 3. Send a burst of mbufs via the data socket to fill sc->outq (ng_h4_rcvdata enqueues them at :822). 4. Immediately issue an ngctl disconnect on the hook, triggering ng_h4_disconnect โ†’ IF_DRAIN. 5. If the tty layer happens to call ng_h4_start (l_start) concurrently to drain the queue, the IF_DEQUEUE/IF_DRAIN race 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 BTUARTDISC line discipline to a SYSCAP_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 outq is 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.

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:

  1. Upgrade NG_H4_LOCK from crit_enter/crit_exit to a real cross-CPU spinlock so that all outq operations are mutually exclusive across CPUs.
  2. Wrap the IF_DEQUEUE and IF_PREPEND in ng_h4_start with NG_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) and IF_DEQUEUE/IF_DRAIN macros 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).