# DF-3495 VERDICT — not_reproduced (root-caused), defect real

## Bottom line

The session-hook / hook-private use-after-free **exists in the code** — an
unreferenced `hook` + `hpriv` pair is cached from a lockless `LIST_FOREACH`
(ng_l2tp.c:769-781) and used ~13 µs later (861-882, measured) while any
concurrent rmhook frees both objects immediately (661-664 + ng_base.c:831-858).
On this DragonFly guest it could **not** be triggered: every practically
reachable (feeder, control-plane) pair of entry points executes serialized on
**cpu0's netisr thread**, so the free and the use never overlap. Verdict:
`not_reproduced`, impact `none` on this system via these paths.

## What was run (decisive final run, run.log)

* Runtime 90 s, 2 injector threads (L2TP-over-UDP via ng_ksocket, 64 KB
  packets, sid 0x0001), 3 teardown threads (rmhook/connect ping-pong).
* 797,646 UDP packets injected; 173,656 delivery-point executions (probe
  `deliv`); 2,309,380 session-hook frees (probe `frees`); 346,775
  offset-strip walks totalling 4.55 s of cumulative lookup→delivery window
  (avg **13.1 µs** per packet).
* **Zero** UAF observations: `stale_inv = 0`, `stale_realloc = 0`,
  `hpriv_bad = 0`; userspace oracle `delivered s3 = 0`.
* If the free and the use could overlap even 0.01% of the time, ~2.3M frees
  against 173K × 13 µs of exposure would have produced thousands of hits.
  Zero is conclusive for non-concurrency, not for absence of the window.

## Why it does not overlap (the root cause, all verified in-kernel)

The probes record the executing CPU of every delivery and every free:

```
net.graph.l2probe.deliv_cpu: 173656 0 0 0 0 0 ...
net.graph.l2probe.free_cpu: 2309380 0 0 0 0 0 ...
```

**100% of both paths run on cpu0.** On DragonFly:

1. `ng_socket` control sockets (ngctl-style rmhook/connect) execute their
   pru_send handlers on `so->so_port`, which `socreate()` assigns as
   `netisr_cpuport(0)` (sys/kern/uipc_socket.c:259). All netgraph control
   messages from userland therefore run on cpu0's netisr thread.
2. `ng_socket` *data* sockets likewise (same `socreate` path) — verified
   earlier in the session with a socket-fed lower hook: also 100% cpu0.
3. `ng_ksocket` (the real mpd L2TP-over-UDP deployment): the underlying UDP
   socket's `so_port` comes from `udp_initport()` → `inp_initport()` =
   **the CPU where the socket was created** (sys/netinet/ip_demux.c:462-476).
   The ksocket is created inside `ng_ksocket_newhook`, which runs while
   processing the mkpeer control message — on cpu0. Every inbound UDP packet
   is re-dispatched to that socket's port (cpu0) before the upcall delivers
   into the graph, so `recv_lower` runs on cpu0 as well (probes confirm).
4. Varying source ports/flows cannot change this: the pinning is per-socket
   at creation, not per-packet (32- and 64-socket pools were tried — still
   100% cpu0).
5. The one constructible off-cpu0 feeder — ng_eiface's transmit path
   (`ifq_dispatch` → hash-spread netisr CPUs → `ng_eiface_start` →
   `NG_SEND_DATA` into `l2tp:lower`) — **panics the guest at node creation**
   on INVARIANTS kernels: `panic: try holding ifnet lock in netisr`
   (ng_eiface_constructor → ether_ifattach_bpf → if_attach → ifnet_lock,
   KASSERT at sys/net/if.c:3787). This is the already-filed **DF-3486**
   (panicked guest recovered; boot.log excerpt in panic.txt).
   An ng_ether-fed "lower" hook (L2TP-over-Ethernet) would be the remaining
   genuinely-parallel configuration, but this SLIRP-only guest cannot inject
   raw host→guest Ethernet frames to drive it.

## Residual risk assessment

* The defect is a true lifetime bug of the classic v1-netgraph style; any
  future (or existing non-socket) feeder that runs `ng_send_data` into
  `l2tp:lower` from a non-cpu0 context (ng_ether-style link layers, other
  node types executing in their own contexts) makes it live: freed hook +
  12-byte hpriv chunks get recycled by the very next hook creation in the
  teardown loop (same-zone LIFO), yielding cross-session packet
  misdelivery (the s3 oracle) and kernel memory reads/writes through a
  dangling pointer.
* DF-0492 (pass 1, High) remains the practically-reachable race in this
  file: its rack/xack **timers** run in softclock context, which is NOT
  serialized with the cpu0 data path.
* The teardown+traffic machinery built here (2.3M frees / 90 s against a
  13 µs exposure window with in-kernel stale-detectors) is exactly the rig
  needed to re-verify DF-3495 the day an off-cpu0 feeder exists.

## Fix

`fix.diff` (defensive, authored against the read-only tree; not applied):
hold a hook reference across the cached lookup→delivery span in
`ng_l2tp_recv_lower()` and defer the hpriv free past the last hook
reference. Because the race did not reproduce on this system, fix
validation is honestly marked `not_testable` — a fixed kernel is
observationally identical here.

## Artefacts

* `run.log` — final decisive run (counters + probe sysctls)
* `env.txt` — guest uname/modules
* `probe.diff` — the diagnostic ng_l2tp module patch used for the counters
* `panic.txt` — DF-3486 panic tripped by the eiface variant (context)
* `l2tp_hook_uaf.c`, `l2tp_hook_uaf_eiface.c` — PoC sources

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