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)
PoC verification
Evidence pack
findings/poc/DF-3553 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
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
- sys/netgraph7/tee/ng_tee.c:293-331 β
ng_tee_rcvdata: inβdest(+dup) forwarding; no cycle knowledge. T1: left2right-in β out left (via newhook-wired dest, ng_tee.c:186); left-in β out right; right-in β out left. - sys/netgraph7/tee/ng_tee.c:169-205 β
ng_tee_newhookprecalcutes dest/dup; with hook-add order left2right,right,left (T1) and right,left (T2) both tees end up leftβright bidirectional. - sys/netgraph7/netgraph/ng_base.c:1908-1965 β
ng_snd_item: forwarded items (refs>1) applied synchronously β unbounded recursion. - sys/netgraph7/netgraph/ng_base.c:1996-1999 β
ng_acquire_readβlwkt_gettoken_sharedβ td_toks[] overflow at 32 (sys/kern/lwkt_token.c:732).
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
fixedfixed
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.
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.
No comments yet.