# DF-3116 — msdosfs_rename() stale de_fndoffset TOCTOU (dirent slot overwrite)

## What

`msdosfs_rename()` saves the target-directory slot bookkeeping
(`to_diroffset`/`to_count`, from `de_fndoffset`/`de_fndcnt` recorded by
`msdosfs_lookup()`'s EJUSTRETURN path), then **drops the target
directory vnode lock** — across `doscheckpath()` (cross-parent
directory renames, `msdosfs_vnops.c:1045`) and across its source
`relookup()` (same-directory renames, `msdosfs_vnops.c:1127`) — and
afterwards **blindly restores the stale values**
(`msdosfs_vnops.c:1186-1187`) into `createde()`, which writes the new
dirent at that offset with no revalidation (`msdosfs_lookup.c:644-646`,
`DE_EXTERNALIZE` at `bptoep(pmp, bp, ddep->de_fndoffset)`).

A concurrent `create()` that slips into the unlock window lands its
dirent on the *same deterministic first-fit free slot* (both lookups
scan from offset 0), and the stale-slot `createde()` then **overwrites
the live dirent of the just-created file**: the victim file's name,
start-cluster and size in the directory are replaced by the renamed
entry's.  The victim's cluster is orphaned, the victim denode remains
keyed at the stolen `(dirclust, diroffset)`, and two metadata views
alias one dirent.

## PoC

`trigger.c` — three modes:

- `renamer <dir> <sec>`  — Window B racer: alternating same-dir file renames.
- `bouncer <p1> <p2> <sec>` — Window A racer: cross-parent **directory**
  renames (mkdir D<k> in p1; rename into p2). Every bounce runs
  `doscheckpath()` with tdvp unlocked.
- `creator <dir> <sec>`  — create X<i>, verify via **readdir()**
  (stat() is served from the namecache and would mask the theft),
  rotate old files; rotation-unlink ENOENT is a second detector.
- `sweep <dir> <manifest>` — post-run readdir-vs-manifest check.

`wedge.sh` — attribution-clean single-shot: fresh image, 30 s race,
verify racers exited, then umount under a 20 s watchdog.

Build on guest: `sh build.sh` (cc -O2 -Wall -o trigger trigger.c).
Run on guest as root: `sh run.sh` (mounts a 512 MB FAT32 image via
vnconfig; racers run as nobody).

## Expected result

- Race interference is provably reached: the kernel itself logs
  `kern_rename: retry due to race on: "D..." -> "D..."` during the runs.
- The precise dirent theft was **not** caught by the detectors in 10
  runs / ~500k window-opening renames (lock-handoff wakeup latency
  beats both window variants at buffer-cache-hot speeds) — see
  VERDICT.md.
- Racing this path reproducibly **wedges the system** (leaked mount
  refs → umount hangs → guest catatonic): that consequence is filed
  and verified as **DF-3117**.

## Fix

`fix.diff` — createde() revalidates that all slots in
`[fndoffset - fndcnt*32, fndoffset]` are still SLOT_EMPTY/SLOT_DELETED
before writing; returns EEXIST otherwise. Compile-verified against the
guest kernel module build (`msdos.ko`, -Werror) on 2026-09-06.
