# DF-0725 — if_clone_destroy UAF / double-free-unit race

## Verdict
**REPRODUCED (panic / DoS).** The cited UAF race in `if_clone_destroy` is real
and deterministically triggers a kernel panic on the default GENERIC kernel
(`#0`, INVARIANTS ON). Fix validated: a single-fix kernel that holds
`ifnet_lock` across all `ifp` derefs eliminates the panic.

**Reachability is root-only**, so this is a **root→kernel race-condition DoS /
hardening gap**, not an unprivileged privesc.

## Mechanism (trigger → primitive → effect)

`if_clone_destroy()` (sys/net/if_clone.c:103-135) violates the MPSAFE contract
documented in `sys/net/if_var.h:894-896` ("ifunit() … ifnet lock must be held
… for the accessing of the ifp returned by this function"):

```
110:  ifnet_lock();
111:  ifp = ifunit(name);
112:  ifnet_unlock();                 <-- LOCK DROPPED
113:  if (ifp == NULL)
114:      return (ENXIO);
116:  if ((ifc = if_clone_lookup(ifp->if_dname)) == NULL)   <-- DEREF ifp w/o lock
119:  unit = ifp->if_dunit;                                  <-- DEREF ifp w/o lock
…
126:  ifnet_lock();                   <-- re-take
127:  if_clone_free_unit(ifc, unit);  <-- clears bitmap bit (under lock)
128:  error = ifc->ifc_destroy(ifp);  <-- may free ifp (gif_clone_destroy→if_detach→kfree)
132:  ifnet_unlock();
```

Two concurrent destroyers of the same interface both call `ifunit()` while the
ifp is still alive (before either enters the lock-protected section at 126),
so both obtain a live `ifp` pointer. They then serialize at line 126:

1. First destroyer: `if_clone_free_unit()` clears the unit bitmap bit, then
   `ifc->ifc_destroy(ifp)` → `if_detach()` removes ifp from `ifnetlist` and
   the driver frees the ifp memory.
2. Second destroyer: `if_clone_free_unit()` re-clears an **already-cleared**
   bitmap bit → `KKASSERT "bit is already cleared"` at
   `if_clone.c:367-368` → **panic** (INVARIANTS ON, default GENERIC).

With INVARIANTS OFF the second destroyer would instead proceed past the assert
to `ifc->ifc_destroy(ifp)` on already-freed ifp memory → genuine **UAF**
(silent heap corruption / secondary fault). The underlying primitive is thus a
use-after-free on the `struct ifnet` reached through the `if_clone_destroy`
framework path.

## Reproduction

Harness: `race_destroy.c` — forks N children, uses a pipe as a barrier; each
round the parent creates `gif666`, releases all children, and they race
`SIOCIFDESTROY gif666`.

On the **unpatched `#0` kernel** (GENERIC, INVARIANTS ON), `./race_destroy 12 5000`
panics within seconds:
```
panic: if_clone_free_unit: bit is already cleared
if_clone_free_unit.isra.1() at if_clone_free_unit.isra.1+0x49
if_clone_free_unit.isra.1() at if_clone_free_unit.isra.1+0x49
if_clone_destroy() at if_clone_destroy+0x7c
ifioctl() at ifioctl+0x243
mapped_ioctl() at mapped_ioctl+0x5fa
syscall2() at syscall2+0x11e
```
The trace names exactly the cited function (`if_clone_destroy` →
`if_clone_free_unit`); reproduced twice (initial + post-reset baseline).

## Reachability & threat model (why impact = panic/DoS, not uid0)

`if_clone_destroy()` is reachable from userspace **only** through:
- `SIOCIFDESTROY` ioctl → `sys/net/if.c:2012-2016`, gated by
  `caps_priv_check(cred, SYSCAP_RESTRICTEDROOT)` at `if.c:2013`.
- tun/tap **auto-destroy-on-close**: `sys/net/tun/if_tun.c:352`,
  `sys/net/tap/if_tap.c:460`. But opening the device is itself root-gated
  (`tunopen` at `if_tun.c:283`, `tapopen` at `if_tap.c:324` both require
  `SYSCAP_RESTRICTEDROOT`). Verified: `maxx` (uid 1001, not in wheel) gets
  `SIOCIFCREATE2: Operation not permitted`.
- wlan vap destroy (`sys/netproto/802_11/wlan/ieee80211_dragonfly.c:339`) —
  also root-gated.

There is **no unprivileged path** to `if_clone_destroy`. This is a valid Phase-6
hard blocker for escalation: the write/UAF is reachable only from an already-root
context, so there is no privilege boundary to cross (root→kernel is game-over by
definition). Impact is therefore **panic / DoS** from concurrent root operations
(a hardening gap), not a privilege escalation. Severity Medium is appropriate.

## Exploit chain

Not pursued beyond the panic/DoS characterization because reachability is
root-only (valid hard blocker per Phase 6: the primitive is reachable only from
an already-root context). The primitive is a `struct ifnet` UAF (the first
destroyer frees ifp; the second destroyer's `ifc->ifc_destroy(ifp)` operates on
freed memory when INVARIANTS is OFF), but since root already owns the kernel,
escalation is moot. `exploit_chain: "none (root-only reachability hard blocker)"`.

## Fix

`fix.diff` holds `ifnet_lock()` continuously from `ifunit()` through all `ifp`
derefs and the `ifc->ifc_destroy(ifp)` call (the destroy already ran under
`ifnet_lock` in the original code at line 128, and `ifnet_mtx` is recursive —
`sys/sys/mutex.h:47` — so the destroy callback's own `if_detach()`→`ifnet_lock()`
acquires recursively with no new lock-ordering constraint). Each error return
now drops the lock before returning. This matches the documented MPSAFE
contract and eliminates the race window: the second destroyer's `ifunit()` runs
under the same lock that the first destroyer holds during teardown, so it no
longer finds the interface and returns `ENXIO` cleanly.

## Fix validation (Phase 8)

- **Baseline (#0 unpatched):** `./race_destroy 12 5000` → panic
  `if_clone_free_unit: bit is already cleared` (captured in `boot.log` /
  `panic.txt`). `vm.sh status ⇒ down`.
- **Patched (#1 single-fix kernel, `6.5-DEVELOPMENT #1 Wed Jul 8 23:24:38`):**
  applied `fix.diff` to `/usr/src`, `make -j6 nativekernel KERNCONF=X86_64_GENERIC`
  (rc=0), `make installkernel`, reboot. Same harness → **`[+] completed 5000
  rounds with no panic.` RACE_EXIT=0**, guest stays up. Reproduced clean twice
  (12×5000 and 16×3000).
- **Before/after:** panic-on-baseline vs clean-completion-on-patched ⇒ fix
  closes the bug.

## PoC changes

The finding folder shipped with no PoC source (DF-0725 had no prior PoC). I
authored `race_destroy.c` (the concurrent-destroy race harness), `build.sh`,
`run.sh`, and `fix.diff` from scratch based on the line-by-line source trace of
`sys/net/if_clone.c`.

## Kernel references (confirmed during verification)

- `sys/net/if_clone.c:110-132` — the buggy `if_clone_destroy` lock-drop window.
- `sys/net/if_clone.c:367-368` — `KKASSERT "bit is already cleared"` that fires
  on the second concurrent destroyer (the observed panic site).
- `sys/net/if_var.h:894-896` — MPSAFE contract requiring ifnet_lock held for
  accessing the ifp returned by ifunit().
- `sys/net/if.c:1946` — `ifunit()` KASSERT `ifnet is not locked`.
- `sys/net/if.c:2012-2016` — `SIOCIFDESTROY` root-gate
  (`caps_priv_check(SYSCAP_RESTRICTEDROOT)`).
- `sys/net/if.c:949-991` — `if_detach()` removes ifp from ifnetlist under
  ifnet_lock; driver frees ifp after.
- `sys/net/gif/if_gif.c:176-203` — `gif_clone_destroy` → `if_detach` (called
  under ifnet_lock at if_clone.c:128).
- `sys/net/tun/if_tun.c:283`, `sys/net/tap/if_tap.c:324` — root-gate on the
  auto-destroy-on-close path.
- `sys/sys/mutex.h:47` — `mtx` is recursive (justifies holding ifnet_lock over
  the destroy callback).
