# DF-0689 — encaptab list UAF race (sys/netinet/ip_encap.c)

## Verdict: NOT REPRODUCED (race is structurally real but root-gated on the writer side)

## Mechanism

`encaptab` is a global `LIST_HEAD` (ip_encap.c:102) walked without a lock
by `encap4_input` (line 174) and `encap6_input` (line 268). Mutators
(`encap_attach` :329, `encap_attach_func` :386, `encap_detach` :412) only
hold `crit_enter()` — per-CPU interrupt-deferral, which does **not**
serialize other CPUs. `encap_detach` does:

```c
LIST_REMOVE(p, chain);
kfree(p, M_IPENCAP);    /* XXX */                /* line 419-420 */
```

with no synchronize-with-reader. A concurrent `encap4_input` / `encap6_input`
that has already loaded `ep` (or `match->psw` at line 219/295, or
`match->arg` via `encap_fillarg` at line 221/297) and is about to
dereference `(*psw->pr_input)(...)` at line 223/298 will read freed memory
and call a stale function pointer → panic at A:H, or with slab grooming a
controlled indirect call.

## Why it doesn't reproduce on this guest

The race window opens only when **both** of these are concurrent:

1. **Reader side** — a remote or local packet that walks `encaptab`. This
   requires a tunnel consumer (`gif` / `stf` / `gre`) to be configured so
   that `encap_attach` has populated the list. **Remotely reachable in
   principle.**

2. **Writer side** — `encap_detach` fires. Detach happens when the tunnel
   is torn down via `SIOCDIFPHYADDR`, which is gated by:
   ```c
   case SIOCDIFPHYADDR:                            /* sys/net/if.c:2336 */
       error = caps_priv_check(cred, SYSCAP_RESTRICTEDROOT);   /* :2343 */
   ```
   i.e. **root only**. An unprivileged local user cannot trigger the
   writer side at all, so they cannot open the race window.

Per the audit's realistic-threat test, a race that needs root on one side
is a root→kernel hardening gap (root can already do anything), **not** an
unprivileged escalation. The finding's `likely` confidence is reasonable
for the structural claim, but its exploitability for a non-root user is
nil without a separate root bug.

## Reachability attempt

I did not attempt a multi-CPU race demonstration because:
- The writer side requires `caps_priv_check(SYSCAP_RESTRICTEDROOT)`.
- A successful hit demonstrates root→kernel (game-over by definition).
- A non-root trigger does not exist in the cited path.

The bug is therefore documented as a hardening gap with a fix.diff that
adds proper mutual exclusion.

## Fix

`fix.diff` introduces a static `lwkt_token` (`encaptab_token`) and:

- acquires it in `encap4_input` / `encap6_input` around the LIST walk and
  the `(*psw->pr_input)(...)` dispatch (released on every return path);
- replaces `crit_enter()`/`crit_exit()` in `encap_attach`,
  `encap_attach_func`, and `encap_detach` with the same token (released
  before `kfree` in detach so we don't hold it during the allocator).

`lwkt_token` is the DragonFly idiom for serializing a shared data
structure across CPUs without the cost of a mutex; it is the correct
synchronization primitive here. The diff applies cleanly
(`patch -p1 --dry-run` succeeds).

## Files

- `README.md` — root-only race recipe (gif + flood + destroy)
- `fix.diff` — lwkt_token serialization of encaptab
- `build.sh` / `run.sh` — no binary PoC (race gated by privileged op)
