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