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)
PoC verification
Evidence pack
findings/poc/DF-3038 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 equalspmp->pm_rootdirblkis silently remapped toMSDOSFSROOT, so the subsequentdeget()(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) isVROOT && !FAT32(pmp)β on FAT32 the guard does not fire, so rmdir of the alias truncates the root denode to length 0 andfreeclusterchain()(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 thescn == source->StartClustercomparison at :843 can match (source's StartCluster ispm_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
- In
lookup/deget, do not alias a non-root directory entry to the root denode: when a scanned entry hasscn == pmp->pm_rootdirblk(and it is not the actual root), return the entry as-is orEBADF(mirroring themsdosfs_lookup_checkercorruption handling), instead of rewritingscntoMSDOSFSROOT. - Make
detrunc()/deextend()rejectVROOTfor all FAT types (drop&& !FAT32(pmp)), matching the read-only root semantics used elsewhere. - In
doscheckpath(), compare against both root representations (MSDOSFSROOTandpmp->pm_rootdirblk) before thescn == MSDOSFSROOTearly-exit.
Fix verification
not_testableNo fix kernel built: Low severity, Phase V skipped by policy. Fix sketch (3 parts) in VERDICT.md/alias.c.
alias.c, VERDICT.md
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.
No comments yet.