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).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0589 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| race.c | trigger-source | netgraph7-format race harness (data flood + RESET vs pty/tty outq) | 9.6 KB | view raw |
| README.md | readme | original PoC description and build/run instructions | 2.5 KB | β raw |
| VERDICT.md | verdict | full analysis: race confirmed at code level, not triggerable on this guest | 9.1 KB | β raw |
| fix.diff | suggested-fix | git-apply-able fix: upgrade NG_H4_LOCK to spinlock + wrap IF_DEQUEUE/IF_PREPEND | 1.7 KB | view raw |
| build.sh | build-script | build netgraph7 modules + PoC | 554 B | view raw |
| run.sh | run-script | load modules + run race PoC | 789 B | view raw |
| fix_build.log | build-log | fixed module compilation output | 623 B | view raw |
| run.log | run-log | PoC run output (30s, no panic on pty) | 366 B | view raw |
| panic.txt | panic-signature | UNRELATED panic in socket receive path (chunk_mark_allocated), NOT the DF-0589 race | 607 B | view raw |
| env.txt | environment | uname, cc version, kern.version | 247 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0589 β PoC: race sc->outq IF_DEQUEUE (ng_h4_start) vs IF_DRAIN (ng_h4_disconnect)
Privileged-capability local race. The per-node mbuf output queue sc->outq is
mutated from two execution contexts (tty l_start callback vs netgraph
disconnect/reset/shutdown) that hold no common cross-CPU lock: NG_H4_LOCK
is just crit_enter() (per-CPU), and ng_h4_start doesn't even take that
around its IF_DEQUEUE/IF_PREPEND. On SMP, concurrent dequeue and drain can
both return the same mbuf β UAF / double-free.
Files
race.cβ sketch driver (flood outq via ng data socket, race disconnect against ttyl_start).- (added by per-PoC verifier) full
race.crewrite with proper ng_socket / ngctl plumbing,build.sh,run.sh,build.log,run.log,VERDICT.md,manifest.json, and β if escalation is developed βexploit.cplus the mbuf-slab grooming recipe.
Build & run
cc -O2 -lpthread -o race race.c ./race /dev/cuaU0 # user with SYSCAP_NONET_NETGRAPH capability, SMP
Expected first outcome
Kernel panic from double-free (mbuf allocator INVARIANTS, or freed-mbuf
KASSERT), with backtrace pinning the fault inside m_free called from
ng_h4_start (sys/netgraph7/bluetooth/drivers/h4/ng_h4.c:610):
Kernel panic: mbuf double-free / use-after-free
m_free+0x...
ng_h4_start+0x...
l_start ...
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.
Notes for the per-PoC verifier
- The race window is narrow: the
outqis only 12 mbufs deep, and tty draining is typically bursty not continuous. Plan for many flood+disconnect iterations (thousands-to-millions) on SMP hardware. AdjustFLOOD_COUNTand the inter-disconnect pause to maximize overlap with the ttyl_startcallback. - The escalation variant (mbuf-slab grooming β controlled
m_ext.ext_freeβ ring-0 code execution) requires asizeof(struct mbuf)and victim-object analysis; document the chosen victim inVERDICT.md. If the heap layout does not admit a stable primitive, the verdict should reflectDoS_confirmed / escalation_unverifiedand the finding stays at Medium. - Verify the fix with
git apply findings/poc/DF-0589/fix.diff(upgradingNG_H4_LOCKto a spinlock and wrappingIF_DEQUEUE/IF_PREPENDinng_h4_start); after the fix the race should no longer fire. - The same pattern exists in
sys/netgraph7/tty/ng_tty.c(out of this file's scope but flagged in the finding markdown for the maintainer).
DF-0589 β VERDICT
Verdict: RACE CONFIRMED (code-level); NOT TRIGGERABLE ON THIS GUEST (sio-console-gated)
The race condition described in the finding is real at the code level β all
the structural preconditions hold. However, triggering it live requires a real
serial port with the sio driver (NOT a pty), and the only sio port on this
guest is the kernel console (sio0), which cannot be freed for BTUARTDISC.
The race is also gated behind SYSCAP_NONET_NETGRAPH (not a default
unprivileged capability) and requires the netgraph7 module stack to be loaded
by root (netgraph7 is compiled OUT of the default X86_64_GENERIC kernel).
What was confirmed
1. NG_H4_LOCK is per-CPU only (the root cause)
sys/netgraph7/bluetooth/drivers/h4/ng_h4_var.h:88-89:
#define NG_H4_LOCK(sc) crit_enter();
#define NG_H4_UNLOCK(sc) crit_exit();
DragonFly crit_enter(9) blocks preemption and defers IPIs on the current
CPU only β it provides zero cross-CPU exclusion. Two CPUs can both be
inside NG_H4_LOCK simultaneously.
2. ng_h4_start does IF_DEQUEUE without NG_H4_LOCK
sys/netgraph7/bluetooth/drivers/h4/ng_h4.c:572-648:
- Line 579: lwkt_gettoken(&tp->t_token); β acquires the tty token only.
- Line 592: IF_DEQUEUE(&sc->outq, m); β no NG_H4_LOCK held.
- Line 615: IF_PREPEND(&sc->outq, m); β no NG_H4_LOCK held.
- Lines 601/620/638: NG_H4_LOCK is acquired only briefly for stat counter updates.
3. ng_h4_disconnect / rcvmsg / shutdown do IF_DRAIN with crit_enter only
ng_h4_disconnect(line 729-743):NG_H4_LOCK(crit_enter) βIF_DRAIN(line 735) β unlock. Does NOT acquiretp->t_token.ng_h4_rcvmsgNGM_H4_NODE_RESET (line 886):IF_DRAINinsideNG_H4_LOCK.ng_h4_shutdown(line 773):IF_DRAINwithout evenNG_H4_LOCK.
4. The ONLY concurrent execution path: sio siopoll β l_start
The finding claims ng_h4_start runs from the tty l_start line-discipline
callback concurrently with netgraph methods. This is correct β but with a
critical DragonFly-specific nuance:
Exhaustive search of the entire sys/ tree reveals that linesw[].l_start is
called from exactly one place in the kernel:
sys/dev/serial/sio/sio.c:2241 (*linesw[tp->t_line].l_start)(tp);
This is inside siopoll() (line 2176), which is registered as a Software
Interrupt (SWI) handler:
sys/dev/serial/sio/sio.c:1209 register_swi_mp(SWI_TTY, siopoll, ...);
The general tty layer's ttstart() (sys/kern/tty.c:1549) does NOT call
l_start β it only calls tp->t_oproc. On DragonFly, pty-based triggering
does not work because l_start is never invoked for pty devices.
So the race fires ONLY on systems with a real sio serial port where:
1. siopoll (SWI context, CPU A) calls l_start β ng_h4_start β IF_DEQUEUE
2. The netgraph writer thread (CPU B) processes ng_h4_disconnect/RESET β IF_DRAIN
3. Both operate on sc->outq with no common cross-CPU lock β double-free / UAF
5. FORCE_WRITER serializes netgraph-internal calls
NG_NODE_FORCE_WRITER(sc->node) at line 215 ensures all netgraph methods
(rcvdata, rcvmsg, disconnect, shutdown) are serialized. The callout path
(ng_h4_process_timeout β ng_h4_start) also goes through ng_callout which
sets NGQF_WRITER (sys/netgraph7/netgraph/ng_base.c:~3260), so it is also
serialized. Only the sio siopoll β l_start path bypasses FORCE_WRITER.
Why it was NOT triggered live on this guest
-
pty doesn't call l_start: The DragonFly tty layer's
ttstart()callstp->t_oproc, notlinesw[].l_start. A pty-based PoC cannot exercise the race. (Confirmed: 80 seconds Γ 4M RESET + 4M data packets on pty β no panic.) -
sio0 is the console: The guest has one sio port (
sio0at 0x3f8, IRQ4) which is the kernel console. Opening it forBTUARTDISCreturnsEBUSYbecause the console subsystem holds it. Getty was killed but the kernel console driver retains the reference. -
Netgraph7 compiled out of GENERIC:
NNETGRAPH7_BLUETOOTH_H4 = 0in the default kernel config. Requires rootkldloadof netgraph7 core + ng_socket + ng_h4 modules. -
Capability gate:
ng_h4_open(line 157) requiresSYSCAP_NONET_NETGRAPHβ not a default unprivileged capability. Verified:maxx(uid 1001) getsEPERMonTIOCSETD BTUARTDISC.
Impact assessment
| Factor | Assessment |
|---|---|
| Race real? | YES β confirmed by exhaustive source analysis |
| Triggerable via pty? | NO β DragonFly tty layer doesn't call l_start for ptys |
| Triggerable via sio? | YES β siopoll SWI calls l_start; requires real serial port |
| Reachable unprivileged? | NO β needs SYSCAP_NONET_NETGRAPH + netgraph7 loaded (root) |
| Default kernel? | NO β netgraph7 compiled out of X86_64_GENERIC |
| Exploitation chain? | DoS/panic via mbuf double-free (INVARIANTS catches it) |
| Privilege escalation? | NOT assessed β not reachable from unprivileged on default kernel |
| Severity | Medium is appropriate (HW + capability + module-loading gated) |
Exploit chain (Phase 6 assessment)
This is a memory-corruption class (potential mbuf double-free / UAF), so Phase 6 escalation assessment is required. However:
Valid hard blocker β root-only reachability: The bug's write primitive
(IF_DEQUEUE / IF_DRAIN on a shared mbuf queue) is reachable ONLY when:
1. An admin has loaded the netgraph7 module stack (root kldload)
2. The user holds SYSCAP_NONET_NETGRAPH (delegated capability)
3. A real sio serial port is available and not the console
On the default GENERIC kernel, netgraph7 is not compiled in and the code path is dead. There is no unprivileged path to the write without root module loading. This meets the Phase 6 valid hard blocker criterion: "The write is reachable only from an already-root context (kldload / wheel-only ioctl)".
Even with root-loaded modules + delegated capability, the race is narrow (2-3 instruction window in IF_DEQUEUE vs IF_DRAIN) and the realistic outcome is a DoS panic (INVARIANTS mbuf double-free detection), not reliable code execution. The speculative m_ext.ext_free hijack chain described in the finding is unverified.
No uid0 escalation was attempted because the bug is not reachable from an unprivileged user on the default kernel. Documented as a rootβkernel hardening gap with DoS impact.
PoC changes
Rewrote race.c from the non-compiling sketch into a working harness that:
- Constructs netgraph7-format ng_mesg manually (NG_VERSION=8, 32-bit arglen)
since the system's libnetgraph/ngctl use the incompatible old-netgraph ABI
- Creates pty + BTUARTDISC + ng socket node + hook connection correctly
- Races data flooding + NGM_H4_NODE_RESET (IF_DRAIN) for 60-80 seconds
- Also created race_sio.c variant targeting real sio ports (not usable on this
guest due to console conflict, but correct for systems with a free serial port)
The pty-based PoC confirmed 4M+ RESET + 4M+ data iterations with no panic,
which is expected β the pty path doesn't exercise l_start. The PoC is kept
as a functional harness; on a system with a free sio port and the right
capabilities, it would exercise the actual race via race_sio.
Recommended fix
Upgrade NG_H4_LOCK from crit_enter/crit_exit to a real spinlock, and wrap
the IF_DEQUEUE/IF_PREPEND in ng_h4_start with it. This matches the
finding's recommendation. See fix.diff.
The same pattern (crit_enter as the only outq lock, unprotected IF_DEQUEUE in
the l_start method) also exists in sys/netgraph7/tty/ng_tty.c and should be
fixed there as well (flagged for the maintainer, out of scope for this finding).
Fix validation
The fix (fix.diff) was applied to the in-guest source and the ng_h4.ko module
was rebuilt successfully. All three netgraph7 modules (netgraph core, ng_socket,
ng_h4) loaded correctly. The PoC ran for 30 seconds (1.37M data packets + 1.36M
RESET messages) on the fixed module with no panic and the guest stayed up.
Note: Since the pty-based PoC cannot exercise the actual race path (l_start is only called by siopoll for real serial ports), this validates that the fix compiles, loads, and does not introduce regressions β but cannot directly demonstrate the fix preventing the race (which requires a free sio serial port).
fix_status: not_testable β the race cannot be triggered on this guest (no
free sio port, pty doesn't call l_start), so the fix's effectiveness against
the specific race cannot be verified live. The fix is compile-validated,
load-validated, and correct by code inspection.
Unrelated panic observed
During heavy PoC operations (1M+ ng_socket control messages), an unrelated panic occurred in the socket receive path (NOT in ng_h4):
panic: memory chunk 0xfffff8004f0a407f is already allocated! chunk_mark_allocated β _kmalloc β dup_sockaddr β soreceive β kern_recvmsg
This is a separate issue (likely a double-allocation or UAF in the ng_socket/
netgraph7 message handling under heavy load), not the DF-0589 outq race.
Documented in panic.txt for reference.
Fix verification
not_testablenot_testable: the DF-0589 race cannot be triggered on this guest (pty doesn't call l_start; sio0 is the console), so the fix's effectiveness against the specific race cannot be verified live. The fix.diff was applied to in-guest source, the ng_h4.ko module compiled successfully (22424 bytes, was 19432), all three netgraph7 modules loaded correctly, and the PoC ran 30s (1.37M data + 1.36M RESETs) on the fixed module without panic or regression. The fix is compile-validated, load-validated, and correct by code inspection (spinlock provides cross-CPU exclusion that crit_enter lacked).
Fixed module build: cc ... -c ng_h4.c -> ng_h4.ko (22424 bytes) BUILD=0. Module load: kldload ng_h4.ko -> H4=0, kldstat shows 3000 size. PoC on fixed module: 30s run, data=1369497 resets=1363264, RACE_EXIT=0, guest up (no regression). Baseline: same PoC on unpatched module also exits 0 (pty doesn't exercise the race path on either).
Confirmed kernel references
- sys/netgraph7/bluetooth/drivers/h4/ng_h4_var.h:88
- sys/netgraph7/bluetooth/drivers/h4/ng_h4_var.h:89
- sys/netgraph7/bluetooth/drivers/h4/ng_h4.c:579
- sys/netgraph7/bluetooth/drivers/h4/ng_h4.c:592
- sys/netgraph7/bluetooth/drivers/h4/ng_h4.c:615
- sys/netgraph7/bluetooth/drivers/h4/ng_h4.c:735
- sys/netgraph7/bluetooth/drivers/h4/ng_h4.c:773
- sys/netgraph7/bluetooth/drivers/h4/ng_h4.c:886
- sys/dev/serial/sio/sio.c:2241
- sys/dev/serial/sio/sio.c:1209
- sys/kern/tty.c:1549
Detail
Exploit chain
none -- valid hard blocker: root-only reachability. The write primitive (IF_DEQUEUE/IF_DRAIN on shared mbuf outq) is reachable ONLY when: (1) root has kldloaded the netgraph7 module stack (compiled out of default GENERIC), (2) the user holds SYSCAP_NONET_NETGRAPH (not a default unprivileged capability), and (3) a real sio serial port is available. There is no unprivileged path to the write without root module loading -- meets Phase 6 valid hard blocker criterion ('reachable only from already-root context via kldload'). Even with root setup, the race is narrow (2-3 instruction IF_DEQUEUE vs IF_DRAIN window) and only the sio siopoll->l_start path bypasses FORCE_WRITER. Realistic outcome is DoS panic (INVARIANTS mbuf double-free detection), not reliable code execution. No uid0 escalation assessed because bug is unreachable from unprivileged on default kernel.
Evidence (decisive lines)
pty PoC (80s, 4M+ iters): data=4097982 resets=4053430, RACE_EXIT=0, no panic. Guest stayed up. Unprivileged test: TIOCSETD BTUARTDISC -> EPERM (SYSCAP_NONET_NETGRAPH gate). sio0 console conflict: open /dev/cuaa0 -> EBUSY. Unrelated panic: 'panic: memory chunk is already allocated!' in chunk_mark_allocated->_kmalloc->dup_sockaddr->soreceive->kern_recvmsg (NOT ng_h4). Fixed module: compiles OK (22424 bytes), loads OK (kldstat 3000 size), PoC runs 30s without regression.
PoC changes
Rewrote race.c from the non-compiling sketch into a working harness: (1) constructs netgraph7-format ng_mesg manually (NG_VERSION=8, 32-bit arglen) since system libnetgraph/ngctl use the incompatible old-netgraph ABI; (2) correctly creates pty + BTUARTDISC ldisc + ng socket node (bind csock, connect dsock to same node) + NGM_CONNECT hook; (3) races data flooding + NGM_H4_NODE_RESET (IF_DRAIN) with ng_h4_start (IF_DEQUEUE). Added build.sh, run.sh, VERDICT.md, fix.diff, manifest.json, and all logs.
Verified recommended fix
Upgrade NG_H4_LOCK from crit_enter/crit_exit to a real spinlock (struct spinlock sc_lock) in ng_h4_var.h, init it with spin_init in ng_h4_open, and wrap the IF_DEQUEUE and IF_PREPEND in ng_h4_start with NG_H4_LOCK/NG_H4_UNLOCK. This matches the finding's proposal. Lock order is safe: tp->t_token -> sc_lock in ng_h4_start; sc_lock alone in disconnect/rcvmsg/shutdown. Full git-apply-able diff in fix.diff (includes sys/spinlock.h + sys/spinlock2.h for the spinlock API).
Verdict
RACE CONFIRMED at code level but NOT TRIGGERABLE on this guest. The race IS real: NG_H4_LOCK is crit_enter (per-CPU only, ng_h4_var.h:88-89), ng_h4_start does IF_DEQUEUE without it (ng_h4.c:592), and ng_h4_disconnect/rcvmsg/shutdown do IF_DRAIN with only crit_enter (ng_h4.c:735/886/773). HOWEVER, exhaustive source analysis revealed that linesw[].l_start is called ONLY from sys/dev/serial/sio/sio.c:2241 (inside siopoll, a SWI handler) -- the DragonFly tty layer's ttstart() calls t_oproc, NOT l_start. This means pty-based triggering does NOT work (confirmed: 80s, 4M+ data + 4M+ RESET iterations on pty -> no panic). The ONLY concurrent execution path that bypasses FORCE_WRITER is sio's siopoll -> l_start -> ng_h4_start on a real serial port. The guest's sio0 is the kernel console and cannot be freed for BTUARTDISC. Additionally: netgraph7 is compiled OUT of the default X86_64_GENERIC kernel (NNETGRAPH7_BLUETOOTH_H4=0), requiring root kldload; and ng_h4_open requires SYSCAP_NONET_NETGRAPH capability (maxx uid 1001 gets EPERM). An unrelated panic occurred in the socket receive path (chunk_mark_allocated->kmalloc->dup_sockaddr->soreceive) during heavy ng_socket operations -- this is NOT the DF-0589 outq race.
No comments yet.