# DF-3407 — VERDICT

**Status: REPRODUCED (panic, 3 distinct kernel-panic signatures across the
lifetime race family); fix validated after 3 iterations (v3 clean, 2×120s
stress runs).**

## Baseline (stock kernel #0, INVARIANTS)

Guest: DragonFly 6.5-DEVELOPMENT `#0` X86_64_GENERIC, `vm.sh reset
with-src` fresh boot. PoC `race_tun.c`: parent loops
`open("/dev/tun")`/`close()` (autoclone open creates a unit via
`tunclone()` → `if_clone_create()`, close auto-destroys via `tunclose()`
→ `if_clone_destroy()`, sys/net/tun/if_tun.c:352); a forked child
hammers `SIOCIFDESTROY` on tun0..tun32 through an AF_ROUTE socket.

**Run 1 (≤60s): panic** (panic.txt):

```
panic: if_clone_free_unit: bit is already cleared
if_clone_destroy() at if_clone_destroy+0x7c
tunclose() at tunclose+0x2ba
dev_dclose() ...
```

## Root cause (three unsynchronized lifetime domains + one framework TOCTOU)

The cdev side of tun(4) takes **no lock at all**, while the whole clone
create/destroy side runs under the ifnet lock (sys/net/if_clone.c:74-95).
The race family:

1. **destroy-vs-close**: `tunclose()` clears `TUN_OPEN` at if_tun.c:317
   and runs a long unlocked body (:320-347: purge, if_down,
   if_purgeaddrs_nolink, EVENTHANDLER, rt_ifannouncemsg, funsetown,
   KNOTE) before its own `if_clone_destroy()` at :352. From the moment
   `TUN_OPEN` clears, a concurrent `SIOCIFDESTROY` passes the gate in
   `tun_clone_destroy()` (:419) and `tundestroy()` frees the softc
   (kfree, :270) *while tunclose still dereferences it* (UAF).
2. **open NULL-resurrection**: `tunopen()` (:286-287) resurrects
   (`tuncreate`) when `dev->si_drv1 == NULL` — which, post-destroy,
   means the cdev is already in `destroy_dev()` putaway.
3. **if_clone_destroy lookup/act TOCTOU** (sys/net/if_clone.c:110-132):
   the framework looks the ifnet up under the ifnet lock, **drops the
   lock**, then re-acquires and runs `if_clone_free_unit()` /
   `ifc->ifc_destroy(ifp)` on the stale `ifp`. Two concurrent destroyers
   of the same unit both pass the lookup and then double-run the
   teardown: KKASSERT "bit is already cleared" on INVARIANTS builds
   (observed), and — without INVARIANTS — a **double bit-clear plus
   `tun_clone_destroy()` on a freed ifnet (UAF)**.

Domain 1+3 together produced the baseline panic; the exact interleave is
tunclose and SIOCIFDESTROY both carrying the same unit through
`if_clone_destroy()`'s unlocked gap.

## Fix validation — 3 iterations (this is the interesting part)

* **v1** (if_tun.c only: ifnet_lock around tunopen/tunclose critical
  sections): race re-run → **panic moved to domain 2**
  (panic.2.txt): `tunopen → tuncreate → reference_dev → sysref_get:
  assertion "(sr->flags & SRF_PUTAWAY) == 0"` — proof that the
  NULL-resurrection window is independently live.
* **v2** (v1 + no-resurrect: `sc == NULL` → ENXIO under the lock): race
  re-run → **panic back at domain 1+3 signature**
  (`tunclose+0x24f → if_clone_destroy → if_clone_free_unit`), exposing
  the framework lookup/act TOCTOU: serialization of the *tun* paths
  cannot close a window inside `if_clone_destroy` itself.
* **v3** (v2 + if_clone.c: hold the ifnet lock across
  lookup→free_unit→destroy): race re-run **2×120s** (56521 and 59259
  open/close cycles against the destroyer child): clean exit both times,
  guest up, no panic (run.fix.log, run.fix.2.log). `ifc_destroy`
  (tundestroy) was already invoked under this lock by the original code
  path, so the lock condition is unchanged for every cloner.

Baseline: panic in ≤60s (1/1 runs). Patched v3: no panic in 240s of
racing (2/2 runs).

## Exploitability / escalation ceiling

Every entry point in the race is root-gated: `tunopen()` requires
`caps_priv_check(SYSCAP_RESTRICTEDROOT)` (if_tun.c:283 — refused in
jails), `SIOCIFCREATE`/`SIOCIFDESTROY` are root-gated
(sys/net/if.c:2008-2017). No unprivileged reachability exists, so a
uid=0 chain is impossible by construction; the ceiling is a
root-triggered kernel panic / UAF of `tun_softc`/`ifnet`. (Note: the
if_clone.c TOCTOU half is triggerable by *any* two concurrent
`ifconfig <clonable-if> destroy` callers — still root-only — and is
filed as the framework component DF-3414.)

## Artifacts

panic.txt, panic.2.txt (resurrection panic on v1), run.log (baseline),
run.fix.log + run.fix.2.log (v3 clean), fix.diff (v3, two files),
fix_kernel_info.txt (kernel #1 of 09:05:29, KERNEL_RC=0/INSTALL_RC=0),
env.txt, build.log.

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