β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3038

Crafted FAT32 root-alias directory entry (startCluster == pm_rootdirblk): rmdir() frees the live mount's root-directory cluster chain; rename() clobbers root entry 1 and creates a namespace cycle

Field Value
ID DF-3038
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:L
CWE CWE-20 / CWE-668
File sys/vfs/msdosfs/msdosfs_vnops.c
Lines 1230-1257, 1469 (aliasing: lookup.c:411-418; guard hole: denode.c:502)
Area vfs/msdosfs
Confidence likely
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket base:vfs
Reported pending
Known CVE none
CVE match novel

Summary

lookup.c silently remaps a scanned directory entry whose start cluster equals pmp->pm_rootdirblk to MSDOSFSROOT, so deget returns THE VROOT root denode for an ordinary subdirectory entry β€” a crafted 'rootalias' entry aliases the mount root. (a) rmdir() of the alias: dosdirempty returns 1 on a crafted-empty root, removede drops a ref on the root denode, and detrunc's root guard is 'VROOT && !FAT32' β€” on FAT32 it proceeds and freeclusterchain frees the ROOT DIRECTORY CHAIN of the still-live mount; freed clusters return to the allocator and can be handed out as file data while still being walked as the root directory. (b) rename() of the alias into a subdirectory: doscheckpath's blind spot (top-level FAT32 '..' entries report cluster 0, which early-exits the walk BEFORE the source comparison can match) lets the move of the root into its own descendant succeed; the '..' update then overwrites entry 1 of the root directory, clobbering a live entry and installing a live namespace cycle. The FAT12/16 sibling (startCluster 0) is blocked for both operations. Requires a crafted FAT32 image mounted read-write + one rmdir/rename; effects confined to the attacker's own crafted mount β€” no kernel memory-safety violation, no cross-volume effect. Not executed (Low; Phase V skipped β€” untested; manual recipe in pack). Fix: three-part hardening (don't alias scanned entries to the root denode; make detrunc/deextend reject VROOT for all FAT types; compare both root representations in doscheckpath).

Timeline

  • 2026-09-02 Discovered during pass-2 audit of msdosfs_vnops.c (GLM 5.3).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3038 Β· 4 files
FileTypeDescriptionSize
alias.c β€” 2.0 KB view raw
VERDICT.md β€” 2.9 KB ↓ raw
verdict.json β€” 2.8 KB view raw
manifest.json β€” 759 B view raw
VERDICT.md
↓ download raw

DF-3038 β€” crafted FAT32 "root-alias" directory entry: rmdir frees the live root chain; rename corrupts root entry 1

Where

  • sys/vfs/msdosfs/msdosfs_lookup.c:411-418 β€” a directory entry whose start cluster equals pmp->pm_rootdirblk is silently remapped to MSDOSFSROOT, so the subsequent deget() (denode.c:327-347) returns the VROOT root denode instead of a fresh denode: a subdirectory entry anywhere on the volume aliases the mount root.
  • sys/vfs/msdosfs/msdosfs_vnops.c:1469 (msdosfs_rmdir β†’ detrunc): detrunc()'s root guard (denode.c:502) is VROOT && !FAT32(pmp) β€” on FAT32 the guard does not fire, so rmdir of the alias truncates the root denode to length 0 and freeclusterchain() (denode.c:602) frees the root directory's cluster chain of the still-live mount.
  • sys/vfs/msdosfs/msdosfs_vnops.c:1230-1257 (msdosfs_rename ".." update): doscheckpath() fails to recognize the aliased root as an ancestor because FAT32 ".." entries of top-level dirs report cluster 0 (MSDOSFSROOT), which ends the walk at lookup.c:847 before the scn == source->StartCluster comparison at :843 can match (source's StartCluster is pm_rootdirblk, not 0). The rename then overwrites entry 1 of the root directory with a ".." pointing at the new parent β€” clobbering a live root entry and creating a namespace cycle (the mount root now lives inside its own subtree).

Preconditions / trigger

Crafted FAT32 image (root directory crafted empty, one subdirectory entry with deStartCluster|deHighClust == pm_rootdirblk), mounted read-write (root or vfs.usermount=1 owner). Trigger: rmdir("/mnt/S/rootalias") or rename("/mnt/S/rootalias", "/mnt/T/x").

Impact

Filesystem-structure corruption and namespace confusion confined to the attacker's own crafted volume (the freed root clusters are handed to the allocator and can subsequently be allocated as file data while still being walked as the root directory). No kernel memory-safety violation, no cross-volume effect β‡’ Low. Phase V guest run skipped (Low severity); the full path is traced above and the intermediate claims (empty-root dosdirempty == 1, deget root special-case, detrunc FAT32 guard hole, doscheckpath 0-vs-rootdirblk blind spot) are line-verified.

Fix sketch

  1. In lookup/deget, do not alias a non-root directory entry to the root denode: when a scanned entry has scn == pmp->pm_rootdirblk (and it is not the actual root), return the entry as-is or EBADF (mirroring the msdosfs_lookup_checker corruption handling), instead of rewriting scn to MSDOSFSROOT.
  2. Make detrunc()/deextend() reject VROOT for all FAT types (drop && !FAT32(pmp)), matching the read-only root semantics used elsewhere.
  3. In doscheckpath(), compare against both root representations (MSDOSFSROOT and pmp->pm_rootdirblk) before the scn == MSDOSFSROOT early-exit.

Fix verification

not_testable

No fix kernel built: Low severity, Phase V skipped by policy. Fix sketch (3 parts) in VERDICT.md/alias.c.

alias.c, VERDICT.md
per-fix-DF-3038

Confirmed kernel references

Detail

Evidence (decisive lines)

['alias.c: crafted-image recipe + full code-path trace with path:line', 'VERDICT.md: root-cause narrative and 3-part fix sketch']

PoC changes

n/a (analysis-only)

Verified recommended fix

Stop aliasing non-root dir entries to the root denode (lookup.c:411-418), make detrunc/deextend reject VROOT on all FAT types, and compare both root representations in doscheckpath.

Verdict

Not executed on the guest (Low severity; Phase V skipped per audit policy). Line-traced: a crafted FAT32 directory entry with deStartCluster|deHighClust == pm_rootdirblk is remapped to MSDOSFSROOT by lookup.c:411-418, so lookup returns THE VROOT root denode (deget denode.c:327-347) for an ordinary subdirectory entry. rmdir() of that alias passes dosdirempty() on a crafted-empty root, and detrunc()'s root guard (denode.c:502) is 'VROOT && !FAT32(pmp)' so on FAT32 it proceeds to freeclusterchain() the live mount's ROOT directory chain (denode.c:523-524,602 via vnops.c:1469). rename() of the alias dodges doscheckpath's ancestor check because top-level FAT32 '..' entries report cluster 0, which early-exits the walk (lookup.c:847) before the source comparison (:843, source StartCluster is rootdirblk not 0), and the rename then overwrites root-directory entry 1 with a '..' to the new parent (vnops.c:1238-1251), clobbering a live entry and creating a namespace cycle. All effects are confined to the attacker's own crafted volume: no kernel memory-safety violation, hence Low.