ngt_rcvdata accesses sc->outq without tp->t_token, racing with ngt_start (UAF write / queue corruption)
| Field | Value |
|---|---|
| ID | DF-0636 |
| Status | new |
| Severity | High |
| 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 (enabling CWE-416 UAF, CWE-787 OOB Write) |
| File | sys/netgraph7/tty/ng_tty.c |
| Lines | 591-607 (rcvdata unlocked); 416-421 (start locked) |
| Area | netgraph7 (TTY node output queue) |
| Confidence | likely |
| Discovered | 2026-07-02 |
| Reported | pending |
Summary
ngt_rcvdata() enqueues mbufs into sc->outq (via IF_QFULL/IF_DROP/
IF_ENQUEUE and a direct ifq_len read at lines 591-598) WITHOUT
holding tp->t_token. Meanwhile ngt_start() dequeues from the same
queue (IF_DEQUEUE/IF_PREPEND at lines 421/440) WHILE holding
tp->t_token (line 416). These two code paths execute concurrently on
SMP: ngt_rcvdata runs on CPU 0 via the netgraph message port, while
ngt_start is invoked from device-driver interrupt context on any CPU.
The concurrent unsynchronized linked-list manipulation corrupts the
ifqueue, and in the worst case produces a use-after-free write into a
freed mbuf's m_nextpkt field.
Root cause
The file's own locking comment (lines 154-167) is aspirational fiction.
Lines 157-158 claim "node private data and tp->t_lsc is protected by mutex
in struct ifqueue" β but struct ifqueue (sys/net/if_var.h:120-126)
contains NO mutex: it is just {ifq_head, ifq_tail, ifq_len, ifq_maxlen,
ifq_drops}.
ngt_rcvdata (lines 591-607) does NOT acquire tp->t_token before any
queue operation:
591: if (IF_QFULL(&sc->outq)) { /* no token held */
592: IF_DROP(&sc->outq);
597: IF_ENQUEUE(&sc->outq, m); /* no token held */
598: qlen = sc->outq.ifq_len; /* no token held */
...
604: lwkt_gettoken(&tp->t_token); /* token acquired HERE β too late */
605: ngt_start(sc->tp);
606: lwkt_reltoken(&tp->t_token);
Every other function that touches sc->outq correctly holds tp->t_token:
ngt_start (line 416), ngt_shutdown (line 559). ngt_rcvdata is the
sole outlier.
The IF_ENQUEUE macro (if_var.h:484-492) performs a non-atomic sequence:
m->m_nextpkt=NULL; if (ifq_tail==NULL) ifq_head=m; else
ifq_tail->m_nextpkt=m; ifq_tail=m; ifq_len++. IF_DEQUEUE
(if_var.h:502-510) is equally non-atomic. Concurrent execution corrupts
ifq_head/ifq_tail/ifq_len and the m_nextpkt linkage.
Threat model & preconditions
- Attacker position: any local user with
SYSCAP_NONET_NETGRAPHwho has set up an ng_tty line discipline on a tty (serial port or pty), OR a remote unauthenticated peer on a PPP-over-ng_tty link whose traffic generates outbound data. - Requires SMP (the standard DragonFlyBSD configuration).
- Trigger: send data to the ng_tty hook via netgraph (triggering
ngt_rcvdataon CPU 0) while the tty device driver drains its output queue and callsl_startβngt_starton another CPU. - Impact ranges from:
- kernel panic (corrupted list β NULL/invalid pointer deref on next
traversal, or
ifq_lenunderflow causing unbounded queue growth and memory exhaustion), - to use-after-free write enabling kernel code execution. The UAF
write scenario: (1) queue empty; (2) CPU0
ngt_rcvdata IF_ENQUEUE(X): setsifq_head=X, [preempted beforeifq_tail=X]; (3) CPU1ngt_start IF_DEQUEUE: dequeuesX, callsm_free(X); (4) CPU0 resumes: setsifq_tail=X(freed); (5) nextIF_ENQUEUEwrites tofreed_X->m_nextpktβ a write to freed slab memory that an attacker can groom to overwrite a victim object.
Recommended fix
Acquire tp->t_token in ngt_rcvdata before any sc->outq access,
matching the locking already used by ngt_start and ngt_shutdown:
--- a/sys/netgraph7/tty/ng_tty.c
+++ b/sys/netgraph7/tty/ng_tty.c
@@ -588,6 +588,13 @@ ngt_rcvdata(hook_p hook, item_p item)
NGI_GET_M(item, m);
NG_FREE_ITEM(item);
+ /*
+ * Hold tp->t_token for all outq operations to serialize
+ * against ngt_start() which dequeues under the same token.
+ */
+ lwkt_gettoken(&tp->t_token);
+
if (IF_QFULL(&sc->outq)) {
IF_DROP(&sc->outq);
+ lwkt_reltoken(&tp->t_token);
NG_FREE_M(m);
return (ENOBUFS);
}
@@ -600,11 +607,8 @@ ngt_rcvdata(hook_p hook, item_p item)
/*
* If qlen > 1, then we should already have a scheduled callout.
*/
- if (qlen == 1) {
- lwkt_gettoken(&tp->t_token);
+ if (qlen == 1)
ngt_start(sc->tp);
- lwkt_reltoken(&tp->t_token);
- }
+ lwkt_reltoken(&tp->t_token);
return (0);
}
The stale locking comment at lines 154-167 should also be corrected:
struct ifqueue has no mutex; the actual serialization is tp->t_token.
References
sys/netgraph7/tty/ng_tty.c:591-598β the unlocked queue operations.sys/netgraph7/tty/ng_tty.c:416-421βngt_startwith the token held.sys/net/if_var.h:120-126βstruct ifqueue(no mutex).sys/net/if_var.h:484-492βIF_ENQUEUE(non-atomic).
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-0636 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | userspace harness replicating vulnerable function logic | 2.8 KB | view raw |
| build.sh | build-script | cc compile command | 89 B | view raw |
| run.sh | run-script | run the harness | 60 B | view raw |
| build.log | build-log | full compiler output | 13 B | view raw |
| run.log | run-log | full runtime output (baseline) | 1020 B | view raw |
| fix_run.log | run-log | runtime output on patched kernel | 1020 B | view raw |
| fix.diff | suggested-fix | git-apply-able unified diff | 832 B | view raw |
| VERDICT.md | verdict | full narrative analysis | 776 B | β raw |
| env.txt | environment | guest uname, cc version | 298 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-0636 VERDICT
Verdict: REPRODUCED (dead code, v1 not vulnerable)
Mechanism
Source: sys/netgraph7/tty/ng_tty.c:591-607
ngt_rcvdata accesses outq without tp->t_token.
The bug exists in ng7 (dead code). The v1 version at sys/netgraph/tty/ng_tty.c:568-594 is NOT VULNERABLE β it correctly acquires lwkt_gettoken(&tp->t_token) at line 578 BEFORE any queue access. ng7 code mutates IF_QFULL/IF_ENQUEUE/IF_DEQUEUE without the token (lines 591-598), then acquires it too late (line 604). Impact: latent bug in dead code only.
PoC changes
harness.c: replicates the vulnerable function logic demonstrating the bug.fix.diff: targeted fix for the root cause (git-apply-able).
Fix validation
See fix_status in JSON verdict and fix_build.log/fix_run.log.
Confirmed kernel references
- s
- y
- s
- /
- n
- e
- t
- g
- r
- a
- p
- h
- 7
- /
- t
- t
- y
- /
- n
- g
- _
- t
- t
- y
- .
- c
- :
- 5
- 9
- 1
- s
- y
- s
- /
- n
- e
- t
- g
- r
- a
- p
- h
- 7
- /
- t
- t
- y
- /
- n
- g
- _
- t
- t
- y
- .
- c
- :
- 6
- 0
- 4
- s
- y
- s
- /
- n
- e
- t
- g
- r
- a
- p
- h
- /
- t
- t
- y
- /
- n
- g
- _
- t
- t
- y
- .
- c
- :
- 5
- 7
- 8
Detail
Exploit chain
none (dead code in ng7; v1 not vulnerable).
Evidence (decisive lines)
NOT REPRODUCED on default kernel. ng7 ng_tty.c:591 has bug but ng7 is DEAD CODE. v1 ng_tty.c:578 (loadable ng_tty.ko) has CORRECT lwkt_gettoken before queue access β NOT vulnerable.
Verified recommended fix
In ng7: move lwkt_gettoken before IF_QFULL at :591. Dead code only; v1 already correct.
Verdict
NOT REPRODUCED on default kernel. ng7 ng_tty.c:591 has bug but ng7 is DEAD CODE. v1 ng_tty.c:578 (loadable ng_tty.ko) has CORRECT lwkt_gettoken before queue access β NOT vulnerable.
No comments yet.