# DF-3116 VERDICT — stale de_fndoffset TOCTOU in msdosfs_rename → createde overwrite

**Status: not_reproduced (the dirent-theft interleaving itself); the code
defect is proven by dataflow; the race family demonstrably fires (kernel
logged rename races) and its adjacent consequence (system wedge) is
reproduced and filed as DF-3117.**

## The defect (code-certain)

1. `sys/kern/vfs_default.c:1054` — `vop_compat_nrename` runs
   `vop_old_lookup(tdvp, &tvp, &tcnp)` with `cn_nameiop = NAMEI_RENAME`;
   for a non-existent target `msdosfs_lookup` returns EJUSTRETURN with
   tdvp locked and `dp->de_fndoffset` = first-fit free slot S
   (`msdosfs_lookup.c:367-400`).
2. `msdosfs_vnops.c:1009-1010` — `to_diroffset = dp->de_fndoffset`
   saved while tdvp is locked.
3. `msdosfs_vnops.c:1045` (Window A: doingdirectory && newparent) or
   `msdosfs_vnops.c:1127` (Window B: newparent == 0) — **tdvp unlocked**;
   a concurrent `create()` in the same directory deterministically picks
   the same first-fit slot S (`vop_compat_ncreate`,
   `vfs_default.c:349` → `msdosfs_lookup` CREATE → `msdosfs_create` →
   `createde` writes X's dirent at S).
4. `msdosfs_vnops.c:1186-1187` — after relocking, `dp->de_fndoffset =
   to_diroffset` **restores the stale slot**; `relookup`'s fresh value is
   discarded.
5. `msdosfs_lookup.c:644-646` — `createde()` does
   `ndep = bptoep(pmp, bp, ddep->de_fndoffset); DE_EXTERNALIZE(ndep, dep);`
   with **zero revalidation** → X's live dirent is overwritten by the
   renamed entry.  Victim's data cluster orphaned; victim denode stays
   keyed at the stolen slot (cross-file metadata confusion for as long
   as it is cached).

FreeBSD identified this same stale-offset family in its pre-2019
msdosfs_rename and resolved it by rewriting the function to re-derive
the slot after every relock rather than restoring saved values.

## Verification attempts (10 runs on the QEMU guest)

| run | window | iterations | dirent theft | notes |
|-----|--------|-----------|--------------|-------|
| run.3.log  | B (same-dir file rename)      | 431k renames / 163k creates | 0 | stat()-based detector was namecache-blind |
| run.4.log  | B | 431k/163k | 0 | switched to readdir() detector |
| run.5.log  | A (shallow p2)                | 242k dir renames / 272k creates | 0 | window ≈ one cached bread (~2 µs) |
| run.7/8    | A (path-buffer PoC bug fixed) | – | – | my 512-byte path buffers truncated the 655-char deep path; kernel was fine |
| run.9.log  | A (100-deep p2, slow walk)    | 70k dir renames / 192k creates | 0 | **umount hung; guest went catatonic** → DF-3117 |
| run.10.wedge.log | A (clean guest, attribution-clean) | 38.6k dir renames / 100k creates | 0 | **wedge reproduced**: racers exited, umount watchdog never completed, ssh died |

Detection was NOT the limiting factor after run.4: every created file
is verified via readdir() (dirents read through the buffer cache, no
namecache) and again via its rotation unlink 200 creates later
(ENOENT ⇒ theft).  Across all runs **zero** rotation-unlink failures
occurred — the create-inside-the-window interleaving never landed.

Why it never landed (measured): Window B's unlock→relock gap is
~100 ns of instructions on the same CPU; wakeup latency of the blocked
creator (~5-50 µs) loses the handoff every time.  Window A was widened
to the whole doscheckpath walk by placing the target parent at the
bottom of a 100-deep ".." chain (each hop = bread + deget incl. a
`pcbmap(0xffff)` chain walk + vput), giving ~200 µs windows at ~4k
renames/s — still no landing.  The kernel's own
`kern_rename: retry due to race on: "D68634" -> "D68634" (12,0)`
(sys/kern/vfs_syscalls.c:4323, seen on the console) proves concurrent
modification *during* rename occurs, but the exact
createde-runs-inside-the-window ordering did not happen in ~1e6
opportunities on this 6-vCPU KVM guest.

Classification: real code defect (blind trust of cached slot state
across an explicit lock drop — CWE-367), exploitability demonstrated
only down to "window provably opens under unprivileged racing"; the
corruption itself not observed.  Downgraded to confidence=likely,
severity Medium.  The verified DoS consequence of racing this same
path is filed separately as DF-3117 (reproduced).

## Fix validation

`fix.diff` adds a revalidation pass to `createde()` (all slots in
[fndoffset − fndcnt·32, fndoffset] must still read SLOT_EMPTY or
SLOT_DELETED, else EEXIST).  Compile-verified: applied to the guest's
/usr/src copy, `make` in sys/vfs/msdosfs built `msdos.ko` clean with
-Werror; source restored afterward.  Runtime A/B not meaningful
(baseline theft not reproduced); fix_status=not_testable.

## Artifacts

- `trigger.c` — racer (renamer/bouncer/creator/sweep), final
- `run.sh` — full run: fresh FAT32 image, baseline + race + sweeps + fsck
- `wedge.sh` — attribution-clean single-shot wedge probe (see DF-3117)
- `run.3.log`, `run.5.log`, `run.9.log`, `run.10.wedge.log` — runs
- `build.log`, `fix.diff`

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