# DF-3557 — VERDICT

**Status: reproduced. Impact: panic (kernel DoS, root-gated). Confidence: certain.**

## What was verified

Guest: DragonFly 6.5-DEVELOPMENT #0 (stock INVARIANTS kernel, 2026-07-02
build), QEMU/KVM single-tenant, fresh `vm.sh reset with-src` before the
decisive runs. ng7 stack (netgraph.ko, ng_socket.ko, ng_UI.ko) built from
the guest's clean /usr/src with `-Werror` (build via build.sh).

### Baseline (stock) — deterministic panic

Trigger `ui_cycle.c` (control socket only, no data socket needed):

1. control socket node named "uicyc" (NGM_NAME);
2. `mkpeer` UI → S.a ↔ U0.downstream; name U0 "u0";
3. `mkpeer` UI from U0 → U0.upstream ↔ U1.downstream; name U1 "u1";
4. `rmhook` uicyc:a — U0 survives with one hook (a UI node only
   self-destructs at zero hooks, ng_UI.c:241-244);
5. `connect` (addressed to "u1:", path "downstream" resolves to U0
   through the wire): U1.upstream ↔ U0.downstream — **closed 2-cycle**;
6. inject one `NGM_FLOW_COOKIE` (851672669) message addressed to
   `"u0:downstream"`.

Result: `sendto()` never returns; the ssh session dies with the guest.
Serial console (panic.txt, console_full.txt):

    panic: assertion "ref < &td->td_toks_end" failed in lwkt_gettoken
           at /usr/src/sys/kern/lwkt_token.c:661
    lwkt_gettoken() x2, ng_apply_item() x3+ recursive
    Debugger("panic")

### Why it happens (source trace)

- `ng_UI_rcvmsg()` — sys/netgraph7/UI/ng_UI.c:145-157 — relays any
  NGM_FLOW_COOKIE message that arrives with `lasthook` set:
  downlink→uplink, uplink→downlink, via `NG_FWD_ITEM_HOOK` (ng_UI.c:148,
  153). No hop/nesting bound; cmd field is never examined.
- Injection channel: `ngc_send()` (socket/ng_socket.c:301-306) uses
  `ng_address_path()`, which for a hook-terminated path sets the item's
  el_hook (netgraph/ng_base.c:3100-3103). Hook-addressed delivery does
  **not** consume the wire, so it reaches nodes whose every hook is
  already part of the loop. `ng_path2noderef()` walks *through* hooks and
  returns `lasthook = peer of the final hook` (ng_base.c:1804-1806), so
  "u0:downstream" is delivered to the node across the wire — inside the
  ring either way.
- Each relay re-addresses via `ng_address_hook()` (ng_base.c:3051-3080)
  to the peer of the target hook = the other node's cycle hook;
  `ng_snd_item()` applies forwarded items synchronously (refs>1 →
  `ng_apply_item`, ng_base.c:1948-1961), and `ng_acquire_read`/
  writer pushes one LWKT tokref per hop into `td_toks_array[32]`
  (sys/sys/thread.h; KKASSERT at lwkt_token.c:661/732 always compiled).
- Topology is constructible because neither `ng_con_nodes()`
  (ng_base.c:1442-1485) nor the v1 twin `ng_con_nodes()` (v1
  ng_base.c:975-988) rejects same-graph cycles, and `ng_UI_newhook()`
  grants both hooks unconditionally (ng_UI.c:117-132).
- The **data** path cannot be the entry: the data socket only addresses
  hooks of its own node (socket/ng_socket.c:466), and a closed UI cycle
  has no spare hook. The control relay is the enabling ng_UI code —
  that is what makes this a finding *on this file* (DF-3540 class,
  ng_UI instance).

This is the same framework property as DF-3540 (ng_bpf) / DF-3553
(ng_tee), but a distinct constructible instance: no bpf programs, no
tee, two stock UI nodes and one FLOW message.

### Unprivileged reachability

uid 1001 running the same trigger: `socket(AF_NETGRAPH, …, NG_CONTROL)`
fails with errno 1 (EPERM) — ng7 `ngc_attach` gates socket creation via
caps_priv_check (unpriv_check.txt). Root-only trigger; severity Low
(privileged DoS), consistent with DF-3540/3553.

## Exploit chain

None beyond denial of service: root-gated configuration, deterministic
panic. No memory-safety violation — the tripwire is an always-on
KKASSERT; no controlled overwrite occurs before it. No path to uid=0
(and the trigger itself requires root).

## Fix validation (fix.diff, applied to the guest's /usr/src copy only)

`fix.diff` bounds nested synchronous FLOW relaying in `ng_UI_rcvmsg()`
with an atomic nesting counter (`NG_UI_FLOW_HOPS_MAX = 8`): pure cycles
stay nested for the whole relay, so the counter trips deterministically;
every exit path decrements. Rebuilt ng_UI.ko alone (`-Werror` clean),
swapped in over the stock module:

| build | FLOW injection into closed ring |
|---|---|
| stock ng_UI.ko | panic (token-array KKASSERT), guest down (run.log) |
| patched ng_UI.ko | `sendto` returns errno 62 (ELOOP); guest survives; repeated injections with fresh node names trip ELOOP again and the guest stays up (fix_run.log) |

fix_status: **fixed** (baseline reproduced; patched reproduces neither
the panic nor the hang; the ELOOP return exercises 8 full relay hops).

## v1 twin — why there is NO v1 finding (negative result)

`sys/netgraph/UI/ng_UI.c` cannot participate in a self-contained cycle:

- v1 `ng_UI_rcvmsg()` (v1 ng_UI.c:149-154) frees every message and
  returns EINVAL — no relay, so no control-message cycle.
- v1 control delivery carries no hook (v1 ngc_send discards it; the v1
  rcvmsg signature has no hook parameter).
- v1 data entry needs a free hook on a cycle node; a closed UI cycle has
  none (data socket only addresses its own node's hooks, v1
  socket/ng_socket.c:391-397).
- A v1 cycle containing a UI node therefore requires a third node type
  with an injection port (bpf's ifMatch redirect — DF-3546; tee's
  left/right+connect — DF-3554); the UI is an interchangeable passenger,
  not the enabler. Not filed against this file.

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