# DF-3169 — autofs `umount -f` vs in-flight trigger/mkdir: use-after-free

**Bug:** An in-flight autofs vop (`autofs_nresolve` trigger path /
`autofs_readdir` trigger path / `autofs_nmkdir`) holds only raw
`anp`/`amp` pointers across multi-second daemon-dispatch sleeps and the
1–2 s retry-gap sleeps of `autofs_trigger()`
(sys/vfs/autofs/autofs.c:427-455).  `autofs_unmount()`
(sys/vfs/autofs/autofs_vfsops.c:195-261) — reachable concurrently via
`umount -f` (MNT_FORCE) — force-closes even referenced vnodes
(sys/kern/vfs_mount.c:1165-1181), drains `sc_requests` with a 1 s-cadence
scan, and then frees **every** `autofs_node` and the `autofs_mount`.
Its "no new triggerings" assumption
(autofs_vfsops.c:209-212) is false: a trigger thread parked in a retry
gap inserts **new** requests — and dereferences/writes the freed node —
after the free.

**Sinks (all demonstrated or code-traced):**
- read-after-free: `anp->an_parent`, `anp->an_mount`, `amp->am_*` copied
  into the daemon-visible request (autofs.c:312-356) — leaked heap bytes
  of whichever kernel object reuses the 4288-byte `autofs_mount` block;
- write-after-free: `anp->an_retries++` (autofs.c:441-445),
  `anp->an_cached`/`anp->an_wildcards` (autofs.c:410-411),
  `callout_reset(&anp->an_callout, …)` arming a 600 s callout inside the
  freed 256-byte node block (autofs.c:412-413);
- post-free request insertion with dangling `ar_mount` (autofs.c:345,362);
- `autofs_nmkdir` NULL-deref: after force-vgone, `dvp->v_mount ==
  &dummymount` ⇒ `amp == dummymount.mnt_data == NULL` ⇒
  `mtx_lock_ex_quick(&amp->am_lock)` faults (autofs_vnops.c:251,263).

**Trigger model:** unprivileged churn (`stat`/`ls` under the automount —
the trigger side) racing root's `umount -f` (or `automount -u`) — i.e.
routine autofs admin operations on a box where users hit the automount.

## Reproduce (deterministic KLD harness — primary proof)

### Prerequisites (root, once)
```sh
kldload autofs
mkdir -p /df3169_mp
mount_autofs -f df3169 "" /df3169_mp     # run.sh sets the sysctls
```

### Build
```sh
./build.sh      # builds df3169_race.ko in-guest (needs /usr/src)
```

### Run (baseline, unpatched)
```sh
./baseline_run.sh   # == run.sh content; see run.log for the result
```
Expected (see `run.log`, reproduced verbatim on stock INVARIANTS kernel):
```
DF3169: dounmount returned 0 (ticks=15842) -- anp=... amp=... ARE FREED NOW
DF3169: reuse blocks: amp 0xfffff8008d4be800 ... SAME-BLOCK, node ... SAME-BLOCK
DF3169: DANGLING REQUEST ar=... id=1 ar_mount=0xfffff8008d4be800 == FREED amp
DF3169:   ar_from="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"...   <- read-after-free
DF3169 POLL: freed anp=... an_retries: N -> N+1 ...  <== WRITE-AFTER-FREE   (×10)
DF3169 W: trigger returned 60 (ticks=19042, dounmount_ticks=15842)
```
The harness re-allocates the freed blocks (marker 0x41 in the amp,
zeroed objcache node) right after `dounmount()` returns — same CPU, same
magazine — so the stale trigger's reads and writes become visible.

### Run (production-shaped, probabilistic)
```sh
cc -O2 -pthread -o fake_daemon fake_daemon.c   # + autofs_ioctl.h
./variantB.sh            # 40 × (mount_autofs → trigger+mkdir storm → umount -f)
```
On the unpatched kernel this exercises the real userspace paths (58
daemon requests handled in the recorded run); the µs-wide
`autofs_nmkdir` NULL-deref window did not hit within 40 cycles
(`run_variantB.log`) — the deterministic KLD run is the proof.

### Run (patched, fix.diff applied, kernel rebuilt)
Same `baseline_run.sh`; expected DIFFERENCE:
```
DF3169 W: trigger returned ... (ticks=..., dounmount_ticks=<same or later>)   # ordering flipped
DF3169: SUMMARY ... dangling=0                                                # no post-free request
# and NO "WRITE-AFTER-FREE" POLL lines
```

## Fix
`fix.diff` (v2) — per-mount lifetime references: `struct autofs_mount`
gains `am_dying`/`am_triggers`; `autofs_nresolve`/`autofs_readdir`/
`autofs_nmkdir` acquire a trigger reference for their whole body (ENXIO
if dying / force-reclaimed); `autofs_unmount()` marks the mount dying
before the request-drain loop and waits (bounded, EBUSY abort) for
in-flight triggers before freeing any node.  No `mnt_lock` is taken
inside the vops — a first fix revision that used `vfs_busy()` was
found to deadlock (ncp-lock → mnt_lock vs dounmount's mnt_lock →
cache_inval ncp-lock; see `FIX_NOTES.md` / `VERDICT.md`) and was
replaced.
