# DF-5788 — kern_getcwd walks the mount topology with no lock and no mount reference (UAF vs umount -f / chdir)

File: `sys/kern/vfs_cache.c` — `kern_getcwd()` (`:5060-5160`), reached
unprivileged from `__getcwd(2)` on every path-resolution system.

## What the bug is

`kern_getcwd()`:

* reads `fdp->fd_ncdir` **completely unlocked** (`:5076-5079`) while
  `kern_chdir()` swaps that handle under `fdp->fd_spin` and drops the old
  reference immediately after (`sys/kern/vfs_syscalls.c:1945-1952`);
* walks up the namecache topology **holding only ncp references** and
  dereferences `nch.mount` (`:5094`, `:5095`) — a `struct mount *` for which
  this function never took a reference.

Sibling code does it right: `sysctl_kern_proc_cwd` snapshots the handle with
`cache_copy()` under `p_token` (`sys/kern/kern_proc.c:2056-2059`), and
`cache_fullpath()` documents + requires a referenced `nchp`.

Two manifestations:

* **(A) initial-hold TOCTOU** — between `nch = fdp->fd_ncdir` and
  `_cache_hold(ncp)` a sibling thread's `chdir()` can `cache_drop()` the old
  handle.  When cwd sits on a mount root (`mnt_ncmountpt.ncp`, created by
  `cache_allocroot` → *never hash-linked*), after `umount -f` the only
  remaining ref is the fd's, so chdir's drop frees the ncp → `_cache_hold`
  increments `nc_refs` on freed/reused memory (UAF write).
* **(B) walk-phase UAF** — every loop iteration derefs
  `nch.mount->mnt_ncmountpt.ncp` (`:5094`) and, at a mount root, follows
  `nch.mount->mnt_ncmounton` (`:5095`) — two pointers read out of the mount
  and then `_cache_hold`d.  A concurrent `umount -f` of the mount being
  walked frees the `struct mount` (`dounmount` → `mount_drop` →
  `kfree(mp, M_MOUNT)`, `sys/kern/vfs_syscalls.c:1107-1117` +
  `sys/kern/vfs_mount.c:399-403`) while the walk still uses it.  Non-forced
  unmount is blocked by held ncps (`:916-918`); **forced unmount is not**.

Threat: fully unprivileged when `vfs.usermount=1` (tmpfs user-mountable —
`get_fscap()` `SYSCAP_NOMOUNT_TMPFS`, `sys/kern/vfs_syscalls.c:5392`; mount
owner may unmount with arbitrary flags incl. `MNT_FORCE`, `:660-670,701`);
otherwise the free side is a privileged unmount racing (automounter /
removable media / admin `umount -f`) — both sides raceable by a local user
with the first config.

## Contents

* `df5788.c`, `df5788_v2.c` — riggers (walker + chdir-flapper + mount churn)
* `instrumentation.diff` — guest-only detection rig: poisons
  `mnt_ncmountpt.ncp`/`mnt_ncmounton.ncp` with magic values in `mount_drop()`
  and panics in `kern_getcwd()` if the walk ever reads them (i.e. ever
  dereferences a freed mount)
* `fix.diff` — the real fix (hold a mount reference for the whole walk;
  snapshot the initial nchandle under `fd_spin`; read the transition
  `mnt_ncmounton` under `mnt_token`)
* `build.sh`, `run.sh` — exact commands
* `build.log`, `run_baseline.log`, `run_rig.log`, `panic.txt`, `env.txt` —
  evidence
* `VERDICT.md`, `manifest.json`, `verdict.json` — results

## Reproduce

Guest with the rig kernel installed (see VERDICT.md):

```
cc -O2 -pthread -o /tmp/df5788v2 /tmp/df5788_v2.c
sysctl -w vfs.usermount=1
/tmp/df5788v2 /tmp/df5788 600        # expected: DF5788 panic on rig kernel
```
