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

ng7 ng_tee: two cross-connected tees plus a left2right entry hook form a constructible pure-topology forwarding cycle; one injected byte recurses through ng7's unbounded synchronous forwarding until the LWKT token array overflows β€” deterministic kernel panic (DF-3540 class, tee instance)

Summary

ng_tee is a bidirectional forwarding node. Cross-connecting two tees T1.left<->T2.left and T1.right<->T2.right closes a packet cycle, and - unlike a bare ng_echo pair whose hooks are both consumed by the loop - the tee's left2right/right2left hooks provide an entry edge: a socket node wired to T1.left2right injects straight into the cycle. ng7 forwards items synchronously, acquiring a per-node LWKT token per hop until td_toks[LWKT_MAXTOKENS=32] overflows. VERIFIED live on the stock INVARIANTS guest: panic 'assertion ref < &td->td_toks_end failed in lwkt_gettoken_shared' at lwkt_token.c:732 with ng_apply_item x31 recursive frames, captured on the serial console and live via DDB trace. Core hop-bound fix (el_hops vs NG_HOPS_MAX=16 -> ELOOP) rebuilt in-guest and validated: trigger survives 30s, RUN_RC=0. Root-gated; privileged DoS; no memory corruption before the always-on KKASSERT. DF-0727 not re-reported.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3553 Β· 15 files
FileTypeDescriptionSize
README.md β€” 3.8 KB ↓ raw
VERDICT.md β€” 5.0 KB ↓ raw
tee_cycle.c β€” 6.6 KB view raw
build.sh β€” 208 B view raw
run.sh β€” 254 B view raw
run.log β€” 568 B view raw
panic.txt β€” 675 B view raw
console_full.txt β€” 13.3 KB view raw
ddb_trace.txt β€” 2.1 KB view raw
fix.diff β€” 1.5 KB view raw
fix_build.log β€” 565 B view raw
fix1_cycle.log β€” 436 B view raw
env.txt β€” 387 B view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 3.3 KB view raw

DF-3553 β€” netgraph7 ng_tee: two cross-connected tees + left2right entry = constructible forwarding cycle β†’ deterministic kernel panic (DF-3540 class, tee instance)

ng_tee (sys/netgraph7/tee/ng_tee.c) is a bidirectional forwarding node: data in on left β†’ out right (+ dup left2right), data in on right β†’ out left (+ dup right2left), data in on left2right β†’ out left (ng_tee_rcvdata, ng_tee.c:293-331). Cross-connecting two tees (T1.left ↔ T2.left, T1.right ↔ T2.right) therefore closes a packet cycle, and β€” unlike a bare ng_echo pair, whose single hooks are both consumed by the cycle β€” the tee's left2right/right2left hooks provide an entry edge: one packet injected into T1.left2right is forwarded out T1.left straight into the loop.

ng7 forwards items synchronously (ng_snd_item() β†’ refs>1 β†’ ng_apply_item() direct call, netgraph/ng_base.c:1948-1961), acquiring the peer node's LWKT token each hop (ng_acquire_read β†’ lwkt_gettoken_shared pushes a tokref into td_toks[LWKT_MAXTOKENS=32]). The tee cycle has no exit, so one injected byte recurses until the token array overflows:

panic: assertion "ref < &td->td_toks_end" failed in lwkt_gettoken_shared
       at /usr/src/sys/kern/lwkt_token.c:732
lwkt_gettoken_shared() x2
ng_apply_item() x ~31 recursive
Debugger("panic")

This is the same framework defect as DF-3540 (there made reachable through ng_bpf's per-hook ifMatch/ifNotMatch forwarding destinations); this finding demonstrates the pure-topology instance through ng_tee β€” no node-specific configuration at all, only mkpeer/mkpeer/connect.

Trigger privilege

ROOT ONLY: ng7 control sockets are caps_priv_check-gated (same as v1 / DF-3546's unpriv_check). Severity Low (privileged DoS), consistent with DF-3540/DF-3546.

Build / Run (in-guest, root, stock 6.5-DEVELOPMENT #0 INVARIANTS kernel)

sh build.sh            # builds ng7 netgraph.ko ng_socket.ko ng_tee.ko from
                       # clean /usr/src + compiles tee_cycle (ng7 ABI)
sh run.sh              # loads the ng7 stack, fires the trigger
# EXPECTED (stock): instant panic (see panic.txt / console_full.txt /
#                   ddb_trace.txt); guest sits in DDB, ssh dead.

Fix validation (fix.diff β€” ng7 core synchronous-forward hop bound)

fix.diff is the core portion of DF-3540's validated fix (identical defect class): a per-item el_hops counter bounds synchronous forwarding at 16 hops; cycles unwind with ELOOP and the packet is dropped. No ng_tee node-level change is possible for this instance (a pure-topology cycle cannot be rejected at newhook without graph knowledge).

build tee-cycle trigger
stock ng7 modules panic: token-array KKASSERT; DDB trace = ng_apply_item x31 (console_full.txt)
patched core (fix.diff, -Werror clean) no panic; STEP sendto ok; RESULT no-panic survived 30s; guest healthy (fix1_cycle.log)

fix_status: fixed (baseline reproduced; patched reproduces neither wedge nor panic; normal single-hop forwarding unaffected β€” the graph build itself exercised mkpeer/connect through the patched core).

Files

tee_cycle.c     trigger (one source; -DNG7 selects ng7 ABI β€” this finding)
build.sh run.sh exact commands
run.log         stock run: ssh-side output (guest died before the
                background log could be read back; console is decisive)
panic.txt       panic banner from the serial console
console_full.txt full serial console (boot + panic + DDB)
ddb_trace.txt   DDB `trace` captured live through a chardev-change
                serial socket: panic→lwkt_gettoken_shared→ng_apply_item x31
fix1_cycle.log  patched-core run: sendto returns, survives 30s
fix.diff        ng7 core hop bound (el_hops / NG_HOPS_MAX)
env.txt         guest uname / module state
VERDICT.md
↓ download raw

DF-3553 β€” VERDICT

status: reproduced / impact: panic / confidence: certain fix_status: fixed (baseline panic reproduced; patched core survives)

What was verified, step by step

Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, stock INVARIANTS, 2026-07-02 build), QEMU/KVM single-tenant, fresh vm.sh reset with-src before each phase.

1. The graph is constructible (pure topology)

./tee_cycle7 (root), output captured over ssh before the machine died:

tee_cycle: uid=0 core=ng7
STEP mkpeer1 ok        (C.tap <-> T1.left2right   β€” ENTRY EDGE)
STEP mkpeer2 ok        (T1.right  <-> T2.right)
STEP connect ok        (T1.left   <-> T2.left     β€” CYCLE)
STEP graph built (tee/tee cycle + tap entry), injecting 1 byte ...

ng7 hook-add order on T1 is left2right, right, left (and right, left on T2), which resolves left.dest=&right / right.dest=&left on both nodes β€” i.e. the canonical bidirectional tee. One sendto() of a single byte is the whole trigger. No node-specific control message is used (contrast DF-3540, which needed three ng_bpf SET_PROGRAMs).

2. Stock kernel: deterministic panic

The guest died during launch; the serial console (console_full.txt, panic.txt) shows:

panic: assertion "ref < &td->td_toks_end" failed in lwkt_gettoken_shared
       at /usr/src/sys/kern/lwkt_token.c:732
cpuid = 0
Trace beginning at frame 0xfffff80117ee87f0
lwkt_gettoken_shared() at lwkt_gettoken_shared+0x34c
lwkt_gettoken_shared() at lwkt_gettoken_shared+0x34c
ng_apply_item() at ng_apply_item+0xa9
ng_apply_item() at ng_apply_item+0x105   (x ~30 more)
Debugger("panic")
CPU0 stopping CPUs: 0x0000003e
db>

Captured live via the QEMU monitor as well (chardev-change of the serial to a socket, then DDB trace over that socket β€” ddb_trace.txt):

Debugger() at Debugger+0x7c
panic() at panic+0x24e
lwkt_gettoken_shared() at lwkt_gettoken_shared+0x34c
ng_apply_item() at ng_apply_item+0xa9
ng_apply_item() at ng_apply_item+0x105    x 31
ngthread() at ngthread+0x18

Identical signature to DF-3540 (ng_bpf instance): each synchronous forward pushes a tokref; LWKT_MAXTOKENS=32; KKASSERT trips. The recursion rides the cpu0 ngthread (ng_snd_item queues first-entry items to ng_cpuport(0), ng_base.c:1948-1956).

3. Source trace

4. Fix validation (fix.diff)

fix.diff = the ng7 CORE portion of DF-3540's validated two-layer fix (an el_hops counter on the item + NG_HOPS_MAX 16 bound in ng_snd_item), because the correct layer for a pure-topology cycle is the core: ng_tee itself cannot detect the cycle at newhook/connect time without graph-wide knowledge.

Applied to the guest's /usr/src copy only; ng7 modules rebuilt with -Werror (clean); never applied to the audit tree.

run stock patched core
tee cycle + 1-byte inject panic (token KKASSERT), DDB, guest dead STEP sendto ok, RESULT no-panic survived 30s, RUN_RC=0, guest healthy (fix1_cycle.log)

The patched run also re-executed all three graph-construction control messages through the patched core (mkpeer/mkpeer/connect all OK), so normal message handling is intact; only the unbounded recursion is gone (cycle unwinds at depth 16, item dropped with ELOOP on the ngthread).

Exploit ceiling

None beyond denial of service: root-gated configuration, deterministic panic behind an always-on KKASSERT tripwire; no memory-safety violation and no controlled overwrite occurs before the panic. Same ceiling as DF-3540/DF-3546. (netgraph socket creation is caps_priv_check-gated; uid 1001 cannot even open the control socket β€” verified for the identical gate in DF-3546's unpriv_check.txt.)

Notes

  • The stock-run trigger's own stdout log was lost with the guest (death was immediate); the ssh-side transcript (kldstat + LAUNCH + step output shown above) and the console capture are the decisive records.
  • Terminal state of the ng7 instance is a clean panic + DDB (unlike the v1 twin DF-3554, whose terminal state varies β€” see that pack).

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fixed

fix.diff (ng7 core el_hops + NG_HOPS_MAX=16, the validated core layer of DF-3540's fix) applied to guest /usr/src, modules rebuilt -Werror clean: same graph + injection -> sendto returns, RESULT no-panic survived 30s, RUN_RC=0, guest healthy (fix1_cycle.log); stock panics identically (console_full.txt). Pure-topology cycles cannot be rejected at the ng_tee node level, so the core bound is the correct layer.
↓ fix.diffper-fix-DF-3553

Confirmed kernel references

Detail

Exploit chain

root builds graph with 3 control messages (mkpeer tee tap/left2right; mkpeer tee right/right; connect T1.left<->T2.left) + 1-byte sendto -> unbounded synchronous forwarding recursion -> LWKT token-array KKASSERT -> panic; machine dead in DDB. No uid=0 path: no memory corruption occurs before the always-on tripwire; trigger itself requires root.

Evidence (decisive lines)

panic.txt + console_full.txt (serial console: panic banner, CPU0 stopping CPUs, db>); ddb_trace.txt (live DDB 'trace' via chardev-change serial socket: panic<-lwkt_gettoken_shared<-ng_apply_item x31<-ngthread); fix1_cycle.log (patched core survives 30s, RUN_RC=0)

PoC changes

Authored fresh (no seed): one C source covering both ABIs (-DNG7 for this finding); graph = 2 mkpeers + 1 connect; entry via left2right; ng7 module stack built from clean /usr/src per DF-3540's harness pattern.

Verified recommended fix

Core hop bound on synchronous forwarding (el_hops/NG_HOPS_MAX in ng_snd_item) as in fix.diff; upstream may prefer queueing over recursion instead.

Verdict

ng7 ng_tee admits a pure-topology forwarding cycle: two tees cross-wired T1.left<->T2.left / T1.right<->T2.right, entered via T1.left2right (the entry edge a bare ng_echo pair lacks). One 1-byte sendto recurses through ng7's synchronous ng_snd_item/ng_apply_item forwarding, acquiring a per-node LWKT token per hop until td_toks[32] overflows: panic 'assertion "ref < &td->td_toks_end" failed in lwkt_gettoken_shared' with ng_apply_item x31 in the DDB trace. Same framework defect as DF-3540 (ng_bpf instance); this is the constructible pure-topology instance. Root-gated (control sockets are caps_priv_check'd), deterministic, verified on the stock INVARIANTS guest.