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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3554 Β· 14 files
FileTypeDescriptionSize
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 in Xcpustop / cpu_smp_stopped β€” i.e. a debugger/panic entry whose console could not be serviced;
  • all CPUs in cpu_idle yet the network stack catatonic (no TCP answers);
  • CPU0 in memmove (mbuf copy path) + CPU1 in an HPET DELAY loop, 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
VERDICT.md
↓ download raw

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+0x22c6 and ng_tee.ko+0x2e1 β€” the per-hop m_dup/m_freem churn and the two v1 forwarding functions. Other CPUs: cpu_idle/cpu_smp_stopped.
  • Terminal states varied: siocngetc+0x41 with RFL.IF=0 and Xcpustop frames (kernel debugger entry polling a console that can never answer); catatonic all-idle; memmove+HPET-DELAY cross-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 nm output):

    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-hop m_dup (line 311) for the (disconnected) dup hook, freed by the core's NULL-hook path; dest send recurses via NG_SEND_DATA (line 339).
  • sys/netgraph/netgraph/ng_base.c:1678-1698 β€” ng_send_data calls the peer's rcvdata directly, 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fixed

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.
↓ fix.diffper-fix-DF-3554

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).