v1 ng_tee: same constructible tee/tee cycle with left2right entry edge; v1's unbounded synchronous ng_send_data recursion (plus per-hop m_dup churn) deterministically hard-wedges the whole machine β unrecoverable DoS with no panic banner or dump (DF-3546 class, tee instance)
Summary
v1 twin of DF-3553: two tees cross-wired left<->left / right<->right with the socket tap on T1.left2right. v1 ng_send_data calls the peer rcvdata directly in the caller's thread with no hop bound. One root-injected 1-byte sendto never returns and the machine wedges 4/4: ssh/ICMP dead, console silent, no banner/dump, only a QEMU reset recovers. Terminal states varied (kernel-debugger entry polling an unservicable console with IF=0 and Xcpustop frames; all-CPU catatonia; cross-CPU memmove/DELAY deadlock). QEMU-monitor kstack walk proved the ngd_send -> ng_send_data -> ngt_rcvdata -> m_dup/m_free chain live with the 'tap' injection sockaddr residue. Depth-bound fix in v1 ng_send_data (atomic counter, NG_SEND_DEPTH_MAX=16 -> ELOOP) validated live: sendto errno=62, machine survives. No FLOW_COOKIE in v1 (DF-0727 has no v1 twin); m_dup/meta failure paths and lifecycle verified clean.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3554 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.8 KB | β raw | |
| VERDICT.md | β | 4.8 KB | β raw | |
| tee_cycle.c | β | 6.6 KB | view raw | |
| build.sh | β | 109 B | view raw | |
| run.sh | β | 156 B | view raw | |
| run.log | β | 279 B | view raw | |
| boot_tail.txt | β | 273 B | view raw | |
| wedge_forensics.txt | β | 1.6 KB | view raw | |
| fix.diff | β | 1.4 KB | view raw | |
| fix_build.log | β | 358 B | view raw | |
| fix1_cycle.log | β | 429 B | view raw | |
| env.txt | β | 362 B | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 3.1 KB | view raw |
DF-3554 β netgraph (v1) ng_tee: two cross-connected tees + left2right entry = constructible forwarding cycle β unbounded synchronous ng_send_data recursion β hard machine wedge (DF-3546 class, tee instance)
v1 twin of DF-3553 (ng7). ngt_rcvdata (sys/netgraph/tee/ng_tee.c:277-341)
is positional (no hook-order table): in left β out right (+dup
left2right), in right β out left (+dup right2left), in left2right β out
left. Cross-connecting two tees (T1.left β T2.left,
T1.right β T2.right) closes a packet cycle; the socket node's tap hook
wired to T1.left2right is the entry edge. v1 netgraph forwards
synchronously β ng_send_data() (sys/netgraph/netgraph/ng_base.c:1678)
calls the peer's rcvdata directly in the caller's thread, with no hop
bound and no cycle detection β and the tee additionally performs an
m_dup() + m_freem() per hop for the disconnected dup hook.
Observed failure (stock v1 modules, 4/4 runs)
One 1-byte sendto() never returns and the machine dies a hard system
wedge: ssh (and ICMP) stop being answered within seconds; serial console
silent; no panic banner, no dump; only a QEMU reset recovers. Terminal
states varied across runs:
- one CPU spinning at 100% with interrupts disabled in
siocngetc+0x41(kernel debugger entry waiting for console input) while the other CPUs sat stopped inXcpustop/cpu_smp_stoppedβ i.e. a debugger/panic entry whose console could not be serviced; - all CPUs in
cpu_idleyet the network stack catatonic (no TCP answers); - CPU0 in
memmove(mbuf copy path) + CPU1 in an HPETDELAYloop, others idle β cross-CPU deadlock live-lock.
In-kernel stack walk of the wedging thread (QEMU monitor virtual-memory
dump, symbols from the guest's own nm):
... m_free+0x1ce / m_freem+0x15 (per-hop m_dup churn) 0xffffffff826022f0 netgraph.ko+0x22f0 (ng_send_data) 0xffffffff82656405 ng_tee.ko+0x405 (ngt_rcvdata) 0xffffffff82602277 netgraph.ko+0x2277 (ng_send_data) 0xffffffff8263e34f ng_socket.ko+0x34f (ngd_send β sendto entry) 0xfffff80000706174 "tap\0" (injection sockaddr residue)
i.e. the sendto β ngd_send β ng_send_data β ngt_rcvdata β ng_send_data β m_dup/m_free forwarding recursion live on the wedged machine.
Trigger privilege
ROOT ONLY (verified for the identical v1 gate in DF-3546's
unpriv_check.txt: ngc_attach caps_priv_check, uid 1001 β errno 43).
Severity Low (privileged DoS), same class as DF-3540/DF-3546/DF-3553.
Build / Run (in-guest, root, stock modules)
sh build.sh # cc -O2 -o /root/poc/tee_cycle_v1 tee_cycle.c sh run.sh # kldload netgraph ng_socket ng_tee (v1 stock) + trigger # EXPECTED (stock): sendto never returns; machine wedges (see above); # ssh dies; vm.sh status -> down; only reset recovers.
Fix validation (fix.diff β v1 core depth bound in ng_send_data)
| build | tee-cycle trigger |
|---|---|
| stock v1 netgraph.ko | hard wedge x4 (ssh dead, unrecoverable) |
| patched netgraph.ko (depth bound 16, -Werror clean) | STEP sendto errno=62 (ELOOP), RESULT no-panic survived 30s, RUN_RC=0, guest healthy (fix1_cycle.log) |
fix_status: fixed.
Files
tee_cycle.c trigger (one source; no -DNG7 = v1 ABI β this finding) build.sh run.sh exact commands run.log stock run trigger output (graph built, injection hung) wedge_forensics.txt RIP/RSP samples, CPU states, stack walk, symbol map boot_tail.txt serial console tail at wedge time (no panic output) fix1_cycle.log patched-core run: sendto ELOOP, survives fix_build.log patched v1 netgraph.ko build (clean) fix.diff v1 core NG_SEND_DEPTH_MAX bound in ng_send_data env.txt guest uname / module state
DF-3554 β VERDICT
status: reproduced / impact: dos / confidence: certain fix_status: fixed (baseline wedge reproduced 4x; patched core survives)
What was verified, step by step
Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, stock INVARIANTS),
QEMU/KVM single-tenant; fresh vm.sh reset with-src between phases.
1. Graph constructible, trigger deterministic (4/4)
tee_cycle: uid=0 core=v1 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 ... [sendto never returns; ssh dies with the machine]
All four stock runs (plus one accidental first run) killed the machine
within seconds of the 1-byte injection: ssh stops answering entirely,
serial console prints nothing further, vm.sh status -> down; recovery
requires a QEMU reset.
2. Wedge forensics (why this is the forwarding recursion, not something else)
Because the machine is catatonic, standard in-guest evidence is unavailable; forensics were taken through the QEMU monitor:
- CPU state sampling: exactly one vCPU busy (~94% of one host core),
RIPs cycling through kernel mbuf code (
m_free+0x1c/0x3c/0x5c,m_tag_copy_chain+0x20,m_dup_pkthdr),netgraph.ko+0x22c6andng_tee.ko+0x2e1β the per-hopm_dup/m_freemchurn and the two v1 forwarding functions. Other CPUs:cpu_idle/cpu_smp_stopped. - Terminal states varied:
siocngetc+0x41with RFL.IF=0 andXcpustopframes (kernel debugger entry polling a console that can never answer); catatonic all-idle;memmove+HPET-DELAYcross-CPU deadlock. In every case the machine was unrecoverable and network-dead. -
Direct stack walk of the wedging thread (monitor virtual-memory dump of its kstack, symbolized against the guest's own
nmoutput):m_free+0x1ce / m_freem+0x15 per-hop dup churn netgraph.ko+0x22f0 ng_send_data (2nd frame) ng_tee.ko+0x405 ngt_rcvdata netgraph.ko+0x2277 ng_send_data ng_socket.ko+0x34f ngd_send (sendto entry) 0xfffff80000706174 = "tap\0" injection sockaddr residue
This is the v1 tee forwarding chain live on the syscall thread.
3. Source trace
- sys/netgraph/tee/ng_tee.c:277-341 β
ngt_rcvdata: positional leftβright / rightβleft / left2rightβleft forwarding; per-hopm_dup(line 311) for the (disconnected) dup hook, freed by the core's NULL-hook path; dest send recurses viaNG_SEND_DATA(line 339). - sys/netgraph/netgraph/ng_base.c:1678-1698 β
ng_send_datacalls the peer'srcvdatadirectly, same thread, no hop bound (the v1 twin of ng7's synchronous ng_snd_item path). - No exit exists in the T1.leftβT2.leftβT2.rightβT1.rightβT1.left loop.
4. Why v1 wedges instead of double-faulting like the v1 ng_bpf cycle (DF-3546)
The tee performs an m_dup + NG_SEND_DATA(NULL-hook) + m_freem on
every hop (dup hook present but unconnected), so the recursion is
interleaved with mbuf allocator churn; observed terminal states were a
debugger-entry-with-unserviced-console, all-CPU catatonia, and a
cross-CPU memmove/DELAY deadlock rather than DF-3546's clean guard-page
double fault. The security property is the same or worse: deterministic,
unrecoverable whole-machine loss from one root-injected byte (no banner,
no dump, no autop-reboot).
5. Fix validation (fix.diff)
fix.diff adds NG_SEND_DEPTH_MAX 16 nesting bound to v1
ng_send_data() (atomic depth counter; ELOOP + NG_FREE_DATA at the
bound). Applied to the guest's /usr/src copy only; v1 netgraph.ko rebuilt
-Werror clean; loaded with stock ng_socket/ng_tee:
| run | stock netgraph.ko | patched netgraph.ko |
|---|---|---|
| tee cycle + 1-byte inject | hard wedge x4 (ssh dead, reset required) | STEP sendto errno=62 (ELOOP), RESULT no-panic survived 30s, RUN_RC=0, guest healthy |
v1 control/data messages are synchronous, so the ELOOP surfaces directly as the sendto errno β the cycle unwinds at depth 16 and the packet is dropped.
Exploit ceiling
None beyond denial of service: root-gated trigger (v1 ngc_attach is caps_priv_check'd; DF-3546 unpriv_check.txt: uid 1001 -> errno 43). No memory corruption demonstrated; the observed failure modes are recursion/deadlock pathologies. Same ceiling as DF-3540/DF-3546/DF-3553.
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 adds NG_SEND_DEPTH_MAX=16 nesting bound (atomic counter, ELOOP + data free) to v1 ng_send_data; netgraph.ko rebuilt -Werror clean in guest /usr/src, loaded with stock ng_socket/ng_tee: same graph + injection -> 'STEP sendto errno=62 (ELOOP)', 'RESULT no-panic survived 30s', RUN_RC=0, guest healthy (fix1_cycle.log). Stock wedges 4/4.
Confirmed kernel references
Detail
Exploit chain
root builds graph with 3 control messages + 1-byte sendto -> unbounded synchronous v1 forwarding recursion interleaved with mbuf churn -> unrecoverable whole-machine deadlock (worse than a clean panic: no dump, no auto-reboot). No uid=0 path: no memory corruption; trigger requires root.
Evidence (decisive lines)
run.log (graph built, injection hangs); wedge_forensics.txt (RIP samples symbolized to m_free/m_dup_pkthdr/netgraph.ko/ng_tee.ko, CPU terminal states, the symbolized kstack walk ending at ngd_send + 'tap' sockaddr residue); boot_tail.txt (no panic output); fix1_cycle.log (patched: sendto errno=62 ELOOP, survives)
PoC changes
Authored fresh (no seed); same source as DF-3553 without -DNG7 (v1 NG_VERSION 2 ABI).
Verified recommended fix
Depth-bound nested synchronous forwarding in v1 ng_send_data (fix.diff); upstream may prefer queueing instead of recursion.
Verdict
v1 ng_tee admits the same pure-topology forwarding cycle as its ng7 twin (DF-3553): two tees cross-wired T1.left<->T2.left / T1.right<->T2.right with the socket's tap hook on T1.left2right as entry edge. v1 ng_send_data forwards synchronously with no hop bound, and ngt_rcvdata adds a per-hop m_dup/m_freem; one root-injected 1-byte sendto deterministically (4/4) wedges the whole machine: ssh/ICMP dead, console silent, no banner or dump, terminal states observed = kernel-debugger-entry polling an unservicable console (siocngetc, IF=0, Xcpustop on other CPUs), all-CPU catatonia, and a cross-CPU memmove/DELAY deadlock. QEMU-monitor stack walk proves the sendto->ngd_send->ng_send_data->ngt_rcvdata->m_dup chain live on the wedged thread. Root-gated (v1 ngc_attach caps_priv_check).
No comments yet.