# DF-3495 — v1 ng_l2tp session-hook / hook-private use-after-free (pass 2)

**File:** `sys/netgraph/l2tp/ng_l2tp.c`
**Type:** UAF / lifetime (CWE-416), Severity **Low** on DragonFly (see VERDICT)
**PoC verdict:** `not_reproduced` on the audit guest — root-caused; the code
defect is real but every practically-reachable trigger pair serializes on
cpu0's netisr on DragonFly (details in VERDICT.md).

## What the bug is

`ng_l2tp_recv_lower()` (ng_l2tp.c:769-781) looks up the session hook with a
lockless `LIST_FOREACH` over `node->hooks`, caching both `hook` and `hpriv`,
then dereferences `hpriv->conf` / `hpriv->nr` (861-872) and delivers through
the cached `hook` (882) with **no reference and no lock**. Concurrently,
`ng_l2tp_disconnect()` (661-664) `kfree()`s the `hpriv` the moment an rmhook
lands on the session hook, and the framework (`ng_disconnect_hook` →
`ng_unref_hook`, ng_base.c:831-858) frees the hook struct immediately after.
v1 netgraph delivers data synchronously in the feeder's context
(ng_base.c:1678-1999), so on an SMP system where the feeder and the control
plane run on different CPUs, the cached pointers can be used after free.

## How to build / run the PoC

On the DragonFly guest as root:

```sh
kldload ng_socket ng_ksocket ng_l2tp
cc -O2 -Wall -pthread -o /tmp/l2tp_hook_uaf /tmp/l2tp_hook_uaf.c
/tmp/l2tp_hook_uaf 90 2 3        # seconds, injectors, teardown threads
```

Graph built by the PoC (mirrors a real mpd-style L2TP-over-UDP deployment):

```
ng_socket(injN) --h_l2--> ng_l2tp(L):ctrl
L:lower <--inet/dgram/udp--> ng_ksocket bound to 127.0.0.1:<port>
injN:s1 <----> L:session_0001     (flooded with session-id 0x0001 packets)
injN:s3 <----> L:session_0003     (decoy; teardown ping-pongs the hooks)
```

* Injector threads: UDP sendto of L2TP data packets
  (`hdr 0x0a02 = SEQ|OFF|V2, tid 0, sid 0x0001, offset 64000`) into the
  ksocket → `ng_l2tp_recv_lower` with a **measured ~13 µs window** between
  hook-cache and delivery (the 64 KB offset-strip walks the mbuf chain
  after the lookup).
* Teardown threads: `rmhook session_XXXX` + `connect` ping-pong
  (2.3M hook frees in 90 s).
* **Oracle:** any packet received at the socket node from hook `s3` was
  delivered through a stale (freed-then-reallocated) cached hook —
  impossible on a correct kernel.

Expected result on a *vulnerable-and-concurrent* kernel: `delivered s3 > 0`
(cross-session misdelivery through a UAF hook) and/or kernel panic.
Observed on the audit guest: `delivered s3 = 0`, no stale accesses detected
by in-kernel probes (see VERDICT.md for why — CPU pinning serialization).

## In-kernel probes

`probe.diff` is a *diagnostic-only* patch (never applied to the repo `sys/`)
that adds `net.graph.l2probe.*` counters:

* `frees` / `free_cpu[]` — session-hook disconnects + which CPU ran them
* `deliv` / `deliv_cpu[]` — delivery-point executions + CPU
* `stale_inv` — delivery attempted through a freed (HK_INVALID) hook
* `stale_realloc` — delivery attempted through a freed-then-reallocated hook
  (the definitive in-kernel UAF detection)
* `hpriv_bad` — cached hpriv whose session_id no longer matches (stale hpriv)
* `walkn` / `walkns` — offset-strip executions and total ns (window size)

## Files

* `l2tp_hook_uaf.c` — main PoC (ksocket variant, the decisive run)
* `l2tp_hook_uaf_eiface.c` — variant that tries a genuinely parallel data
  path via ng_eiface; it trips the already-known DF-3486 panic
  (`if_attach` from netisr on INVARIANTS kernels) instead — included for
  completeness
* `probe.diff` — diagnostic kernel-module patch (guest-local only)
* `fix.diff` — proposed defensive fix for the lifetime defect
* `build.sh`, `run.sh`, `build.log`, `run.log`, `env.txt`
* `VERDICT.md`, `manifest.json`, `verdict.json`
