# DF-3037 — msdosfs `doscheckpath()` unbounded ".." walk → rename(2) kernel livelock (system-wide DoS)

## Where

* Call site: `sys/vfs/msdosfs/msdosfs_vnops.c:1045` — `msdosfs_rename()`:
  ```c
  vref(tdvp);
  error = doscheckpath(ip, dp);      /* walkingdirectory && newparent */
  ```
* Root cause: `sys/vfs/msdosfs/msdosfs_lookup.c:822-863` — `doscheckpath()`:

```c
for (;;) {                                        /* <-- NO BOUND */
    ...
    scn = dep->de_StartCluster;
    error = bread(pmp->pm_devvp, de_bn2doff(pmp, cntobn(pmp, scn)),
                  pmp->pm_bpcluster, &bp);        /* cached after 1st pass */
    ...
    ep = (struct direntry *) bp->b_data + 1;      /* the ".." entry */
    ...
    scn = getushort(ep->deStartCluster);
    ...
    if (scn == source->de_StartCluster) { error = EINVAL; break; }
    if (scn == MSDOSFSROOT) break;                /* only clean exits */
    ...
    vput(DETOV(dep));
    if ((error = deget(pmp, scn, 0, &dep)) != 0)  /* hash hit forever */
        break;
}
```

The loop walks the destination-parent's ancestor chain via the on-disk `".."`
entries and terminates **only** when it reaches the root cluster or the source.
Every FAT directory entry on the volume is attacker-controlled when a crafted
image is mounted (the project's standing crafted-media threat model, see
DF-2902/DF-3015). If the `".."` chain contains a **cycle** that never passes
through the root cluster or the source cluster, the loop never terminates:
after the first pass every `bread()` is a buffer-cache hit and every `deget()`
is a denode-hash hit, so the loop never sleeps again — the rename(2) syscall
never returns, the thread cannot be killed (`kill -9` is never delivered), and
the token churn (denode hash / vnode / buffer tokens re-acquired in a tight
spin) starves the whole machine: on the 6-CPU test guest, sshd stopped
completing connections ("Connection timed out during banner exchange") within
seconds of the trigger. No panic, no core — a hard livelock requiring a reboot.

Current FreeBSD (`sys/fs/msdosfs/msdosfs_lookup.c`, doscheckpath) has the same
unbounded `for (;;)` — this is an upstream-live hardening gap, not DFly-only
regression.

## Preconditions

* A FAT12/16/32 filesystem whose `".."` chain (from any directory reachable as
  a rename *destination parent*) contains a cycle not passing through cluster 0
  / the FAT32 root cluster. fsck_msdosfs rejects such images, but the kernel
  mounts them without validation.
* The filesystem must be mounted read-write (rename) — by root, or by an
  unprivileged user with `vfs.usermount=1` and ownership of the backing
  device/image.
* Trigger: `rename("/mnt/A/moveme", "/mnt/B/m2")` where `B`'s ancestor chain
  is cyclic. Any local user with write access to the mount.

## Impact

Local denial of service, kernel-wide: one unkillable kernel thread, one CPU
pinned, and (observed) total loss of interactive service on the test guest
(sshd unable to complete new sessions) until reboot. Crafted-image-mount
precondition → Medium.

## Reproduce (guest, root)

See `run.sh`. Summary:

1. `dd` 16 MB image, `newfs_msdos -F 16`, mount, `mkdir A A/moveme B D`.
2. Unmount; `dirpatch img` maps each `.` entry → (offset, own cluster).
3. **Control:** mount, `trigger /mnt/A/moveme /mnt/B/m2` → returns instantly
   (`rename returned success`).
4. Re-create image; patch `B/..`→cluster(D), `D/..`→cluster(B) (16-bit
   `deStartCluster` of the `..` entries).
5. Mount, run `trigger /mnt/A/moveme /mnt/B/m2` under a 15 s watchdog.

Expected (vulnerable kernel): trigger still running after 15 s; `kill -9`
ineffective; guest stops servicing ssh (banner timeout); serial console shows
no panic. Observed exactly this on stock `X86_64_GENERIC` `#0` — see `run.log`,
`run.2.log`.

## Fix

`fix.diff` — bound the walk (`depth > 4096 → EINVAL`). A legitimate ancestor
chain can never exceed namei-reachable depth (PATH_MAX bounds the number of
components of *any* path that can name the rename target), so the cap cannot
reject a valid rename.

## Fix validation

* Baseline (stock `#0`): hang + unkillable + guest wedged (`run.log`,
  `run.2.log`).
* Patched (`#1 Sat Sep 5 15:02:31 UTC 2026` with `fix.diff`,
  `patch -p1` clean, `make nativekernel KERNCONF=X86_64_GENERIC` BUILD_RC=0,
  `make installkernel` INSTALL_RC=0): cycle-rename returns `EINVAL` instantly
  (`rename: Invalid argument`, trigger exits rc=1 in <1 s), guest fully
  responsive (uptime/load normal, `run.sh` completes RUN_RC=0), control
  rename still succeeds in 0 s (`run.fixed.log` / `fix_run.log`).
  fix_status: **fixed**.

Note: the full 14 897-line build log lived on the guest and was lost with the
post-validation `vm.sh reset with-src` (required to clear the baseline wedge
dirt); `fix_build.log` preserves the decisive build/install tails
(BUILD_RC=0 / INSTALL_RC=0). Guest was left booted on the clean `with-src`
snapshot (stock `#0` kernel, no PoC residue).
