# DF-3405 — VERDICT

**Status: `reproduced` (baseline, stock INVARIANTS kernel #0).**
**Fix: `fixed` (patched kernel #1 built in-guest; the UAF crash is gone).**
**Impact ceiling: kernel use-after-free (privileged-trigger local DoS / memory
corruption); no unprivileged trigger found (open + destroy both require
root/cap).**

## Root cause (path:line, all against `sys/net/tap/if_tap.c`)

The tap softc's lifetime is governed by three unsynchronized entry paths:

- `tapopen()` (:315) holds the **mplock** and dereferences
  `dev->si_drv1` (:332) for its entire body; it publishes `TAP_OPEN`
  at :347 — but nothing forces a racing destroy to wait for that.
- `tapclose()` (:395) holds the **mplock** and (auto-destroy path,
  :458-461) calls `if_clone_destroy()`.
- **`tap_clone_destroy()` (:503)** — the `ifconfig tapN destroy` /
  SIOCIFDESTROY path — holds **no mplock at all**: `if_clone_destroy()`
  (sys/net/if_clone.c:104) only takes `ifnet_lock()`, which `tapopen()`
  never acquires.  It checks `TAP_OPEN` (:507) and then runs
  `tapdestroy()` (:471) which does `ether_ifdetach`, `dev->si_drv1 =
  NULL` (:487), `destroy_dev`, and **`kfree(sc)`** (:495).
- `tapcreate()` (:214) also publishes `dev->si_drv1 = sc` at :223,
  *before* the interface is initialized (:236-250), so a racing
  `open("/dev/tapN")` can also catch a half-built softc (this is what
  the captured `flags = 0x41` line proves happened).

Any interleave where `tap_clone_destroy()`'s `TAP_OPEN` check passes
while a `tapopen()` (or the SIOCIFCREATE-side `tapcreate()`) is inside
the window leaves the open path — and everything reachable from the
returned fd (`tapwrite`, `tapioctl`, `tapclose`) — operating on a
`kfree`d `tap_softc`.

## How it was reproduced (baseline)

`taprace.c`: 8 threads loop `open("/dev/tap0")` → `write(64B)` →
`close()` while the main thread loops `SIOCIFCREATE tap0` → 20-60ms →
`SIOCIFDESTROY tap0`.  Built and run as root on the stock INVARIANTS
kernel #0 (`DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026`).

- **Run 1** (90 s): kernel fault `bcmp+0x3c` (wild pointer read),
  guest dead in ddb.
- **Run 2** (150 s, `debug.if_tap_debug=1`,
  `debug.debugger_on_panic=0`): **panic at 43 s uptime** — full trace
  and crash dump captured (`panic.txt`, `core.txt.0`):

```
tap0: opened, minor = 0. Module refcnt = 1
tap0: writing, minor = 0
tap0: not ready, minor = 0, flags = 0x41      <- TAP_OPEN|TAP_MANUALMAKE
                                                 with TAP_INITED clear:
                                                 torn/freed softc state
                                                 (a live ifconfig-created
                                                 tap reads 0x53)
Fatal user address access from kernel mode from taprace at ffffffff82600cb8
Fatal trap 12: page fault while in kernel mode
fault virtual address    = 0x84
fault code               = supervisor write data, page not present
current process          = 872 (taprace)
panic: page fault
tapifstop() at tapifstop+0x88        <- if_tap.c:710 IF_DRAIN(&sc->tap_devq)
tapclose()  at tapclose+0x1b5        <- if_tap.c:422
dev_dclose / devfs_spec_close / ... / kern_close(fd=4)
```

kgdb on the dump (`core.txt.0`): `tapifstop (sc=0xfffff801179a9e00,
clear_flags=1) at if_tap.c:710` — the closer thread is writing through
subqueue pointers read out of the **freed** softc (fault VA 0x84; the
dump's process table is also corrupt: `Cannot access memory at
0x2b00001665`).  2/2 completed baseline runs panicked within ≤90 s.

## Why the crash is the UAF (not e.g. a half-created tap)

Both interleavings are the same missing-lock defect:

1. open vs **destroy**: `tapopen` fetched `si_drv1` (or
   `tap_clone_destroy` checked `TAP_OPEN`) just before `kfree(sc)`;
   the open side then sets `TAP_OPEN`/`taprefcnt` in freed memory and
   hands the fd to userland; the eventual `close()` runs `tapclose →
   tapifstop` on the freed softc → fault writing through garbage
   `ifq_sq` pointers.
2. open vs **create**: `si_drv1` is published at :223 before
   `TAP_INITED` (:250) — a racing open proceeds on a half-built sc
   (the captured `flags = 0x41` line) and/or races the concurrent
   destroy of that same unit.

## Exploit chain (as far as the class permits)

Root-triggerable kernel UAF write.  The freed `tap_softc` (~256 B,
`M_TAP` kmalloc zone) is immediately re-usable by any same-size
allocation; a reallocation with attacker-influenced content gives
control of the `ifq_sq`/`if_snd` pointers that `tapifstop`/`tapifstart`
write through, and of `sc->tap_dev`/`sc->arpcom` used by every later
fd operation.  On this guest no SMEP/SMAP/KASLR is present, so a
patient root attacker could plausibly convert this to RIP control —
but the trigger already requires root, so escalation adds nothing;
the finding is filed at Low severity as privileged local DoS /
memory corruption.  No unprivileged trigger exists: `/dev/tapN` is
0600 root:wheel and `tapuopen` defaults to 0
(SYSCAP_RESTRICTEDROOT gate at :323-327); SIOCIFCREATE/SIOCIFDESTROY
are SYSCAP_RESTRICTEDROOT (net/if.c:2012).

## Fix validation

`fix.diff` (3 hunks, all in if_tap.c):
1. `tapcreate`: publish `dev->si_drv1` only after the softc is fully
   initialized (`TAP_INITED` set);
2. `tapopen`: `si_drv1 == NULL` → `ENXIO` (never resurrect a
   destroyed/pending device);
3. `tap_clone_destroy`: hold the mplock across the `TAP_OPEN` check
   and `tapdestroy()` — matching the lock discipline `tapopen`/
   `tapclose` already use.

Built in-guest (`make -j4 nativekernel KERNCONF=X86_64_GENERIC`,
`make installkernel`, kernel #1 Sat Sep 12 06:59:23 UTC 2026, both
DF-3405+DF-3406 fixes).  Re-ran the identical race three times
(~480 s total hammering, same 8-thread pressure): **zero panics,
guest stable** (stock crashed 2/2 within 90 s).  Post-fix
create/open/write/destroy cycles work (`CYCLE_OK`).

### Residual, honestly noted (NOT covered by this fix)

On the patched kernel a `destroy_dev()` racing an in-flight `open()`
can still lose the *fd* — the cdev is destroyed mid-open, the fd is
never delivered, `tapopen`'s `taprefcnt++`/`TAP_OPEN` are left set
forever (zombie tap unit; `kldunload if_tap` blocked until reboot).
No memory unsafety was observed in this state (the UAF is gone);
fully closing it needs `destroy_dev()`-side synchronization (devfs
drain of in-flight opens), which is outside this file.  Reported in
`notes`/`recommended_fix`.

## Artifacts

- `taprace.c` — trigger (8 openers vs SIOCIFCREATE/SIOCIFDESTROY)
- `run.sh` — baseline procedure (sysctls + signature scan)
- `panic.txt` — serial-console capture of the panic (run 2)
- `core.txt.0` — savecore output incl. kgdb backtrace of the dump
- `run.log` — baseline narrative; `fix_validation.log` — patched runs
- `fix.diff` — the validated fix

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