# DF-0886 — autofs_node_vn create race (lost-race KASSERT panic)

## Verdict: REPRODUCED (panic, then FIXED)

The race in `autofs_node_vn()` is real and deterministically panics the
default GENERIC kernel (INVARIANTS ON).  The fix — holding `an_vnode_lock`
across the sleepable `getnewvnode()` call — eliminates the race; validated
on a single-fix `autofs.ko` module with 3 consecutive race runs, no panic.

---

## The bug

**File:** `sys/vfs/autofs/autofs_vnops.c:564-602`
**Function:** `autofs_node_vn()`

```c
retry:
    KKASSERT(mtx_notlocked(&anp->an_mount->am_lock));
    mtx_lock_ex_quick(&anp->an_vnode_lock);          // 571  LOCK

    vp = anp->an_vnode;                               // 573  read
    if (vp != NULL) {
        vhold(vp);
        mtx_unlock_ex(&anp->an_vnode_lock);           // 576  fast path unlock
        error = vget(vp, flags | LK_RETRY);
        ...
        return (0);
    }

    mtx_unlock_ex(&anp->an_vnode_lock);               // 589  *** UNLOCK ***

    error = getnewvnode(VT_AUTOFS, mp, &vp,           // 591  SLEEPS
        VLKTIMEOUT, LK_CANRECURSE);
    if (error)
        return (error);
    vp->v_type = VDIR;
    vp->v_data = anp;

    KASSERT(anp->an_vnode == NULL, ("lost race"));    // 597  KASSERT
    anp->an_vnode = vp;                               // 598  assign (UNLOCKED)
```

The lock is **dropped at line 589** before `getnewvnode()` (line 591),
which can sleep arbitrarily long (vnode allocation, vnlru reclaim).
The assignment `anp->an_vnode = vp` at line 598 is done **without
re-taking the lock**.  The KASSERT at line 597 catches the lost race.

### Race scenario

1. Thread A: locks `an_vnode_lock` (571), reads `an_vnode == NULL` (573),
   drops lock (589), enters `getnewvnode()` (591) — sleeps.
2. Thread B: locks `an_vnode_lock` (571), reads `an_vnode == NULL` (573,
   because A hasn't assigned yet), drops lock (589), enters
   `getnewvnode()` (591) — sleeps.
3. Thread A wakes: KASSERT(597) passes (`an_vnode` still NULL), assigns
   `an_vnode = vp` (598), returns.
4. Thread B wakes: KASSERT(597) **fails** (`an_vnode != NULL`) →
   `panic: lost race`.

### Callers (reachability)

`autofs_node_vn()` is called from:
- `autofs_nresolve()` (`autofs_vnops.c:232`) — VOP_NRESOLVE, triggered by
  any `stat()`, `ls`, `open()`, or pathname resolution on the autofs
  mountpoint.  **Any unprivileged user** can trigger this.
- `autofs_nmkdir()` (`autofs_vnops.c:268`) — called by automountd.
- `autofs_root()` (`autofs_vfsops.c:271`) — root vnode access.

Both `nresolve` and `nmkdir` **release `am_lock` before calling
`autofs_node_vn`** (line 225 / 265), so concurrent callers can both find
the same `child` node and race in `autofs_node_vn()`.

### Preconditions

- `autofs` module loaded (`kldload autofs`) — root-only.
- An autofs mount exists (`mount_autofs`) — root-only setup.
- Once the admin has set up autofs, **any unprivileged user** triggers
  the race via `stat()`/`ls` of a path under the mountpoint.

---

## Reproduction

### Userspace stress (probabilistic)

`stress_autofs_race.c` forks N processes that all `stat()` the autofs
mountpoint root concurrently, in a mount/unmount loop.  The race window
is the duration of one `getnewvnode()` call (~us under no pressure), so
this approach is probabilistic and did not fire in 30 loops × 16 racers
on an idle guest.  Under vnode-table pressure (low `kern.maxvnodes`), the
window widens.

### Kernel-module harness (deterministic) — **USED FOR VALIDATION**

`df0886_race.c` is a kldload module that:
1. Finds the first mounted autofs filesystem via `mountlist`.
2. NULLs the root node's `an_vnode` (simulating a reclaim, as
   `autofs_vnops.c:425-428` does).
3. Spawns 4 kthreads on **different CPUs** (`kthread_create_cpu`) that
   all spin-wait on a barrier, then simultaneously call the REAL
   `autofs_node_vn()` on the same node.

Because `getnewvnode()` takes several microseconds, overlapping calls
from different CPUs guarantee the race fires.  Module-only (root
`kldload`); the bug IS userspace-reachable via `stat()`, just too
tight to fire reliably from userspace without pressure.

### Panic signature (unpatched `#0` kernel, original `autofs.ko`)

```
DF-0886: releasing 4 racers -- expect panic
panic: lost race
Trace beginning at frame 0xfffff80117c0fa20
autofs_node_vn() at autofs_node_vn+0x2c4 0xffffffff82603cd4
autofs_node_vn() at autofs_node_vn+0x2c4 0xffffffff82603cd4
racer_thread() at racer_thread+0x46 0xffffffff82659046
Debugger("panic")
Stopped at Debugger+0x7c: movb $0,0xbdaf09(%rip)
db>
```

The double `autofs_node_vn` in the stack trace is the smoking gun: two
threads are inside `autofs_node_vn` concurrently; the one deeper in the
stack hit the KASSERT after the shallower one won the assignment.

---

## Impact

- **Default GENERIC (INVARIANTS ON):** `panic: lost race` → kernel DoS.
  Any unprivileged user on an autofs-configured box can crash the kernel
  by racing lookups of a path under the mountpoint.
- **Without INVARIANTS:** the KASSERT is compiled out; both threads assign
  `anp->an_vnode`, leaking one vnode and corrupting reclaim state (the
  lost vnode's `v_data` still points at the autofs node, leading to
  use-after-free on unmount).
- **No escalation path:** this is a pure race-condition DoS / memory
  corruption.  The write (`anp->an_vnode = vp`) overwrites a vnode
  pointer with another vnode pointer — both are valid kernel objects, so
  there is no attacker-controlled content primitive.  No path to `uid=0`.

**CVSS:** `CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:H` (Medium)

---

## Fix

**`fix.diff`** — hold `an_vnode_lock` across `getnewvnode()`.

`mtx_t` in DragonFly is a **sleeping mutex** (`_mtx_lock_ex` blocks via
`tsleep`, confirmed in `sys/sys/mutex2.h:153`), so holding it across the
sleepable `getnewvnode()` is legal.  The fix:

1. Removes the `mtx_unlock_ex` at line 589 (the unlock before getnewvnode).
2. Adds `mtx_unlock_ex` on the error return path of getnewvnode.
3. Adds `mtx_unlock_ex` after `anp->an_vnode = vp` (line 598).
4. Removes the now-dead `KASSERT(anp->an_vnode == NULL, ...)` at 597.

The second concurrent caller blocks on `mtx_lock_ex_quick` at line 571
until the first finishes and sets `an_vnode != NULL`; it then takes the
fast path (line 574).  No double-create, no KASSERT, no vnode leak.

### Fix validation (single-fix `autofs.ko`)

Built the fixed `autofs.ko` module from `/usr/src/sys/vfs/autofs/` with
the fix applied, installed it at `/boot/kernel/autofs.ko`, loaded it,
mounted autofs, and ran the race harness **3 times**:

| Run | autofs.ko | Result |
|-----|-----------|--------|
| baseline | original (bc43bb…) | **panic: lost race** at autofs_node_vn+0x2c4 |
| fix-1 | fixed (68eec6…) | 4/4 racers rc=0, same vp, no panic |
| fix-2 | fixed (68eec6…) | 4/4 racers rc=0, same vp, no panic |
| fix-3 | fixed (68eec6…) | 4/4 racers rc=0, same vp, no panic |

The fix is deterministic: all 4 racers always observe the same `vp`
(serialized by the lock), and no panic occurs.  INVARIANTS is ON
(confirmed: `grep -c INVARIANTS sys/config/X86_64_GENERIC` = 1).

---

## PoC changes

- `df0886_race.c` + `Makefile` — new deterministic kernel-module harness
  (not in the original PoC folder, which didn't exist).  Calls the real
  `autofs_node_vn()` from 4 CPU-pinned kthreads.
- `stress_autofs_race.c` — userspace concurrent-stat stress (probabilistic;
  documents the userspace reachability path but the window is too tight
  to fire reliably without vnode pressure).
- `fix.diff` — the verified fix (hold `an_vnode_lock` across getnewvnode).
