β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0636

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_NETGRAPH who 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_rcvdata on CPU 0) while the tty device driver drains its output queue and calls l_startβ†’ngt_start on another CPU.
  • Impact ranges from:
  • kernel panic (corrupted list β†’ NULL/invalid pointer deref on next traversal, or ifq_len underflow 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): sets ifq_head=X, [preempted before ifq_tail=X]; (3) CPU1 ngt_start IF_DEQUEUE: dequeues X, calls m_free(X); (4) CPU0 resumes: sets ifq_tail=X (freed); (5) next IF_ENQUEUE writes to freed_X->m_nextpkt β€” a write to freed slab memory that an attacker can groom to overwrite a victim object.

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

Timeline

  • 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
  • 2026-07-02 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0636 Β· 11 files
FileTypeDescriptionSize
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
VERDICT.md verdict full narrative analysis
↓ download 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

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.