msdosfs doscheckpath() unbounded ".." ancestor walk β rename(2) kernel livelock wedging the whole system (crafted FAT ".." cycle)
| Field | Value |
|---|---|
| ID | DF-3037 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:N/I:N/A:C |
| CWE | CWE-835 / CWE-400 |
| File | sys/vfs/msdosfs/msdosfs_vnops.c |
| Lines | 1045 (loop: msdosfs_lookup.c:822-863) |
| Area | vfs/msdosfs |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | base:vfs |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
msdosfs_rename() calls doscheckpath(ip, dp) when moving a directory to a different parent. doscheckpath walks the destination parent's ancestor chain via on-disk ".." entries and only terminates at the root cluster or the source cluster β no iteration bound. A crafted (or fsck-failing) FAT image whose ".." chain cycles without passing through either makes the loop run forever: after the first pass every bread() is a buffer-cache hit and every deget() a denode-hash hit, so the loop never sleeps again, rename(2) never returns, SIGKILL is never delivered, and the tight token churn starves the machine. VERIFIED on the stock INVARIANTS guest: control rename returns in 0s; with a two-directory BβD ".." cycle, rename hung, kill -9 ineffective, and the 6-CPU guest stopped completing ssh connections (banner-exchange timeout) with no panic β system-wide DoS until reboot. Any local user with write access to a read-write msdosfs mount containing a cyclic ".." chain (crafted image mounted by root, or self-mounted with vfs.usermount=1). Current FreeBSD has the identical unbounded for(;;) β upstream-live hardening gap. Fix validated in-guest (depth cap 4096 β EINVAL): cycle rename now fails instantly, guest fully responsive, control rename unaffected.
Timeline
- 2026-09-02 Discovered during pass-2 audit of msdosfs_vnops.c (GLM 5.3); system-wide livelock reproduced + fix validated.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3037 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| dirpatch.c | β | 2.9 KB | view raw | |
| trigger.c | β | 585 B | view raw | |
| build.sh | β | 213 B | view raw | |
| run.sh | β | 2.7 KB | view raw | |
| run.log | β | 1.7 KB | view raw | |
| run.2.log | β | 1.3 KB | view raw | |
| run.fixed.log | β | 2.0 KB | view raw | |
| fix_run.log | β | 2.0 KB | view raw | |
| fix_build.log | β | 15.0 KB | view raw | |
| fix_build_summary.txt | β | 15.0 KB | view raw | |
| fix.diff | β | 824 B | view raw | |
| env.txt | β | 205 B | view raw | |
| VERDICT.md | β | 4.9 KB | β raw | |
| README.md | β | 1.8 KB | β raw | |
| manifest.json | β | 1017 B | view raw | |
| verdict.json | β | 4.6 KB | view raw |
DF-3037 β PoC evidence pack
msdosfs doscheckpath() unbounded ".." ancestor walk β rename(2) kernel
livelock / system-wide DoS via crafted FAT image.
- File audited:
sys/vfs/msdosfs/msdosfs_vnops.c(call sitemsdosfs_renameline 1045); root cause insys/vfs/msdosfs/msdosfs_lookup.c:822-863. - Severity: Medium (local DoS, crafted-image-mount precondition).
- Verdict: reproduced (baseline: hang + unkillable process + guest-wide
ssh outage; fix validated β see
fix.diff,run.fixed.log).
Contents
| file | what |
|---|---|
dirpatch.c |
image tool: lists ./.. directory entries (offset + own cluster); patches a .. entry's deStartCluster |
trigger.c |
plain rename(2) caller |
build.sh |
compiles both (guest) |
run.sh |
full flow: base image β control rename β install BβD .. cycle β exploit rename under watchdog |
run.log |
baseline run on stock kernel: control OK; cycle rename hangs; kill -9 ineffective; capture session lost when guest wedged |
run.2.log |
post-trigger probe: ssh "Connection timed out during banner exchange" (guest-wide outage, no panic on serial) |
run.fixed.log |
same run.sh on patched kernel #1: cycle rename returns EINVAL in <1 s, guest stays responsive |
fix.diff |
bound the walk (depth cap 4096 β EINVAL) |
verdict.json / manifest.json |
machine verdict |
Reproduce
# guest (root), stock kernel:
sh /root/df3037/build.sh && sh /root/df3037/run.sh # β hang + wedge (expect to lose the guest)
# guest (root), patched kernel:
sh /root/df3037/run.sh # β "rename: Invalid argument", guest fine
Success criterion (vulnerable): TRIGGER STILL RUNNING AFTER 15s,
KILL -9 INEFFECTIVE, subsequent ssh sessions time out during banner
exchange, serial console shows no panic.
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():
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=1and ownership of the backing device/image. - Trigger:
rename("/mnt/A/moveme", "/mnt/B/m2")whereB'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:
dd16 MB image,newfs_msdos -F 16, mount,mkdir A A/moveme B D.- Unmount;
dirpatch imgmaps each.entry β (offset, own cluster). - Control: mount,
trigger /mnt/A/moveme /mnt/B/m2β returns instantly (rename returned success). - Re-create image; patch
B/..βcluster(D),D/..βcluster(B) (16-bitdeStartClusterof the..entries). - Mount, run
trigger /mnt/A/moveme /mnt/B/m2under 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 2026withfix.diff,patch -p1clean,make nativekernel KERNCONF=X86_64_GENERICBUILD_RC=0,make installkernelINSTALL_RC=0): cycle-rename returnsEINVALinstantly (rename: Invalid argument, trigger exits rc=1 in <1 s), guest fully responsive (uptime/load normal,run.shcompletes 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).
Fix verification
fixedRebuilt X86_64_GENERIC with fix.diff (doscheckpath depth cap 4096 -> EINVAL; patch -p1 applied cleanly, make nativekernel BUILD_RC=0, installkernel INSTALL_RC=0, kernel #1 booted). Baseline kernel #0 (same PoC, run.log): cycle-rename hangs forever, kill -9 ineffective, guest-wide ssh outage. Patched kernel #1 (run.fixed.log): identical crafted image + identical rename returns 'rename: Invalid argument' (EINVAL) in <1s, trigger exits rc=1, guest fully responsive (uptime/load normal, run.sh completes RUN_RC=0); control rename on unpatched image still succeeds in 0s. Bad behavior gone.
run.fixed.log (control OK + cycle rename EINVAL + responsive guest); fix_build.log (build/install completion tails, BUILD_RC=0/INSTALL_RC=0); fix.diff applies cleanly (patch -p1 verified in-guest and out-of-guest)
Confirmed kernel references
Detail
Exploit chain
craft FAT16 image (16MB, newfs_msdos) with dirs A/A/moveme/B/D -> patch the 16-bit deStartCluster of B's and D's '..' entries to point at each other (dirpatch) -> root mounts rw (vnconfig+mount_msdos; or user with vfs.usermount=1 owning the image) -> any user with write access runs rename(/mnt/A/moveme, /mnt/B/m2) -> kernel spins forever in doscheckpath: unkillable thread + token-churn livelock -> guest-wide DoS until reboot
Evidence (decisive lines)
["run.log: control rename 'rename returned success ... elapsed=0s'; after cycle patch 'TRIGGER STILL RUNNING AFTER 15s', 'KILL -9 INEFFECTIVE (stuck in kernel) -- DoS CONFIRMED'", "run.2.log: post-trigger ssh probe exits 255 'Connection timed out during banner exchange' (guest no longer services logins); boot.log serial console shows no panic", "run.fixed.log: patched kernel - 'rename: Invalid argument' in <1s, process exits, guest responsive afterwards", 'fix.diff: depth cap 4096 in doscheckpath, applies cleanly (patch --dry-run verified) and builds (nativekernel BUILD_RC=0)']
PoC changes
none vs seed (pack authored fresh in this run; no seed existed)
Verified recommended fix
Bound the doscheckpath '..' ancestor walk (depth cap 4096 -> EINVAL); see fix.diff.
Verdict
msdosfs_rename() calls doscheckpath() (vnops.c:1045) whose ancestor walk (lookup.c:822-863) has no iteration bound; on a crafted FAT image whose rename-destination '..' chain cycles without passing through the root or source cluster, the walk never terminates (after the first pass every bread() is a cache hit and every deget() a hash hit, so it never sleeps). Verified on the stock INVARIANTS guest: control rename returns in 0s; with a two-directory B<->D '..' cycle installed, rename(2) never returned, kill -9 was never delivered, and the whole 6-CPU guest stopped servicing ssh (banner-exchange timeouts) with no panic on serial β a system-wide livelock requiring reboot. Bound-the-walk fix.diff validated: patched kernel #1 returns EINVAL for the cycle rename in <1s and the guest stays fully responsive; control rename unaffected. Current FreeBSD has the same unbounded loop, so this is an upstream-live hardening gap.
No comments yet.