MSDOSFS_LOCK_MP expands to nothing: the free-cluster allocator (clusteralloc/chainalloc/usemap_alloc + pm_inusemap scan) is unsynchronized on SMP β double-allocation/cross-linked files (release) or KASSERT panic (INVARIANTS)
| Field | Value |
|---|---|
| ID | DF-3093 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:H/A:H |
| CWE | CWE-362 |
| File | sys/vfs/msdosfs/msdosfs_fat.c |
| Lines | allocator core (~:340-450); the empty macros at msdosfsmount.h:216-218 |
| Area | vfs/msdosfs |
| Confidence | certain |
| Discovered | 2026-09-06 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
MSDOSFS_LOCK_MP()/UNLOCK_MP() expand to nothing (msdosfsmount.h:216-218) and struct msdosfsmount has no lock member, so clusteralloc(), chainalloc(), usemap_alloc() and the pm_inusemap scan run unsynchronized on SMP. Two concurrent extendfile() calls (any unprivileged users writing files on the same FAT mount) can observe the same free run and both chainalloc() it: INVARIANTS kernels panic in usemap_alloc's KASSERT 'Allocating used sector...'; release kernels silently chain two FAT chains over the same clusters β cross-linked files, silent data corruption of whichever file flushes second. Raced 3Γ(6 procs Γ 150 Γ 32KiB) on the 6-vCPU guest with a foreign-byte checker (findings/poc/DF-3093/): no hit within the bounded stress (window is per-run tight; single-run winner needs the two find_next_free scans to interleave inside one count-wide alloc loop) β race is real by construction (empty macros, no other serialization covers the allocator) but honestly not_reproduced in bounded time. Fix: give msdosfsmount a real lock (lockmgr) behind MSDOSFS_LOCK_MP or serialize the allocator on a per-mount token.
Timeline
- 2026-09-06 Discovered during pass-2 audit of msdosfs_fat.c (GLM 5.3); raced 3 rounds clean, unsynchronized-by-construction proven.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3093 Β· 24 files| File | Type | Description | Size | |
|---|---|---|---|---|
| race2.c | β | 6.2 KB | view raw | |
| fat_scan.c | β | 6.6 KB | view raw | |
| conc.c | β | 3.3 KB | view raw | |
| run2.sh | β | 1.8 KB | view raw | |
| run3.sh | β | 2.1 KB | view raw | |
| run4.sh | β | 2.5 KB | view raw | |
| run5.sh | β | 3.0 KB | view raw | |
| run6.sh | β | 1.8 KB | view raw | |
| run.log | β | 5.6 KB | view raw | |
| run3.log | β | 695 B | view raw | |
| run4.log | β | 776 B | view raw | |
| run5.log | β | 975 B | view raw | |
| run6.log | β | 1.5 KB | view raw | |
| run6-tailfail.log | β | 1.4 KB | view raw | |
| run7-delaykernel.log | β | 777 B | view raw | |
| instr-run3.log | β | 1.4 KB | view raw | |
| trace-excerpts.txt | β | 3.9 KB | view raw | |
| instr.patch | β | 2.4 KB | view raw | |
| racestress.c | β | 3.1 KB | view raw | |
| run.sh | β | 160 B | view raw | |
| VERDICT.md | β | 6.7 KB | β raw | |
| README.md | β | 2.5 KB | β raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 4.0 KB | view raw |
DF-3093 β msdosfs free-cluster allocator unsynchronized on SMP (VERIFY: NOT REPRODUCED)
Claim: MSDOSFS_LOCK_MP expands to nothing, so clusteralloc()/usemap_alloc()
race across CPUs β double allocation β KASSERT panic (INVARIANTS) or
cross-linked files (release).
Result: premise verified true, manifestation not reproduced. The macros
are empty (msdosfsmount.h:216-218), no lock member exists, and a
diagnostic kernel PROVED >200 concurrent clusteralloc critical-section
overlaps (3 CPUs deep) under adversarial load β but the double-allocation
itself never fired in ~500 barrier-synchronized allocation bursts across
6 geometries (0 doubles, clean FAT in-degree scans, 0 foreign bytes).
The scan-phase window (bitmap read β first bit set) is sub-microsecond
while the allocator's pm_nxtfree + first-run-wins structure effectively
serializes run claiming.
Reproduce
# on the QEMU guest as root (vnconfig/mount need root; the write(2) # trigger itself is unprivileged once mounted) cd /root/df3093 cc -O2 -o race2 race2.c && cc -O2 -o fat_scan fat_scan.c sh run2.sh 20 6 3 2097152 # base campaign (~2 min) sh run4.sh 2 6 2 16777216 # fallback-forced geometry sh run6.sh 2 6 2 16777216 # word-straddling runs
Success criterion for the finding: fat_scan reports max in-degree >= 2
/ CORRUPTION FOUND, or race2 prints foreign bytes ... REPRODUCED.
Expected (observed every time): clean scans, 0 foreign bytes.
Files
race2.c barrier racer: CPU-pinned children, per-line go words,
pre-created files, one big write(2) each, foreign-byte check
fat_scan.c offline FAT12/16/32 in-degree scanner (deterministic
cross-link detector)
conc.c userland concurrency/parallelism probe
run2.sh base campaign (fresh volume per cycle)
run3-6.sh escalating geometries (fallback-forced, half-word runs,
word-straddling runs)
run.log 20-cycle base campaign output
run3-7*.log full campaign logs (7 = 100us-delay diagnostic kernel)
instr-run3.log instrumented-kernel campaign log
trace-excerpts.txt kernel-side E/X TSC traces + counter deltas (key data)
instr.patch the diagnostic kernel patch (sysctls + print-only
double-alloc detector + overlap counter) vs stock fat.c
racestress.c / run.sh the original v1 seed (superseded)
VERDICT.md full narrative with path:line citations
manifest.json / verdict.json
DF-3093 β VERDICT
Status: NOT REPRODUCED (extensive adversarial campaign, kernel-instrumented; the missing-lock premise is verified true, the race window opens in practice, but the claimed double-allocation never manifested in bounded time)
What the finding claims
MSDOSFS_LOCK_MP()/MSDOSFS_UNLOCK_MP() expand to nothing
(sys/vfs/msdosfs/msdosfsmount.h:216-218) and struct msdosfsmount has no
lock member (verified: full struct read, no lock/token), so
clusteralloc()/chainalloc()/usemap_alloc() and the pm_inusemap scan
run unsynchronized on SMP. Two concurrent extendfile() calls (any users
writing different files on the same FAT mount) can observe the same free
run and both chainalloc() it: INVARIANTS kernels panic in
usemap_alloc()'s KASSERT "Allocating used sector" (msdosfs_fat.c:381);
release kernels silently chain two FAT chains over the same clusters β
cross-linked files.
What was verified TRUE (premise)
- msdosfsmount.h:216-218 β the three macros are literally empty; no lock
member exists in
struct msdosfsmount; nothing else in the allocation path takes any lock (fatentry/fatchchain also rely on the same empty macros). The allocator is unsynchronized by construction. CONFIRMED. - Concurrent execution DOES occur: a diagnostic kernel (stock code plus
an in-flight counter and a print-only double-allocation detector β
instr.patch, the INVARIANTS KASSERT made harmless) recorded >200clusteralloccritical-section overlap events, up to 3 CPUs deep, under 6 CPU-pinned, barrier-synchronized writers on a 6-vCPU guest (run4: 66 events; run6: +137; minimal test: 25, live-polled depth 3). See trace-excerpts.txt.
What was NOT reproduced (the claim itself)
Across ~4 hours, 3 instrumented kernels and ~500 barrier-synchronized
allocation bursts in 6 adversarial geometries, zero double-allocations:
debug.df3093_double never moved from 0; the offline FAT in-degree
scanner (fat_scan.c, deterministic cross-link detector: every cluster
must have in-degree β€ 1) found every unmounted image clean; the
per-writer foreign-byte check never fired; no "Freeing unused sector"
kernel messages.
Campaign geometries (each mounted via vnconfig + mount_msdos as root β
mount requires root or vfs.usermount=1; the trigger itself is plain
write(2) and would work unprivileged once mounted):
- run2.sh: 20 cycles, 6 staggered writers Γ 150 Γ 32 KiB (the original seed shape) β clean.
- race2 v3: 6 CPU-pinned (usched_set) writers, per-cache-line spin
barrier, files pre-created and pre-opened, single large
write(2)per round β 64 MiB (16384-cluster) counts on 2 GiB FAT32 β clean. - run3.sh: fragmented 64 Γ 8 MiB runs (fallback path forced) β clean.
- run4.sh: whole-volume fragmentation (500 Γ 1 MiB, delete alternating; no contiguous run β₯ count anywhere) β 66 overlaps, 0 doubles β clean.
- run5.sh: half-word 4-cluster free runs (concurrent commits share
bitmap words; targets the non-atomic
|=lost-update angle) β clean (setup tail-fill bug noted in run5.log). - run6.sh: 132-cluster word-straddling runs + 16 MiB writes β 137 new overlaps, 0 doubles β clean.
Diagnostic kernels (see trace-excerpts.txt):
* kernel #2: 100 ΞΌs busy-delay between the fallback decision and the
commit β widens the decide window 1000Γ β still 0 doubles.
* kernel #4: + 200 ΞΌs delay after the pm_nxtfree read + per-call
entry/exit TSC trace β on this kernel the extend phases of concurrent
writes ran strictly sequentially (never observed on the timing-unmodified
kernel #1, which recorded the 66/137 overlaps). Anomaly treated as a
diagnostic-kernel artifact.
Why the race window does not convert into a double (analysis)
The unsynchronized section is real, but the allocator's structure
self-defends: every caller reads newst = pm_nxtfree at scan entry and
every commit advances it; the fallback's tie-break if (l > foundl)
(strictly-greater, msdosfs_fat.c:768/786) makes every scanner pick the
first free run from its (already advanced) newst; and a late scanner's
chainlength() re-reads the bitmap words, sees the earlier committer's
bits, and skips past. A double needs two CPUs to both complete their
bitmap reads before either sets its first bit β a sub-microsecond overlap
of the pure-memory scan phase, while the measured entry skew of even the
tightest userland barrier (per-line go words, pinned CPUs) is tens of
microseconds. The INVARIANTS-panic claim therefore remains a real
theoretical possibility (the KASSERT would fire on any double), but it
did not manifest and would require extreme luck or a much wider scan
window (huge maps, slower memory) to hit on this class of hardware.
Impact assessment
- INVARIANTS/debug kernels: KASSERT panic plausible but not demonstrated.
- Release kernels: cross-linked files (silent data corruption) plausible but not demonstrated in ~4 h of adversarial racing.
- The missing lock is nevertheless a genuine hardening defect: the code
was designed to be serialized (
MSDOSFS_LOCK_MPcall sites exist throughout msdosfs_fat.c) and the primitive was never implemented. FreeBSD independently addedpm_fatmutexfor exactly this surface. Any future change that lengthens the scan/decision phase (bigger volumes, debug options, preemption inside the scan) re-opens the window that this campaign measured as open-but-narrow.
Reproduce
# guest: DragonFly 6.5-DEVELOPMENT X86_64_GENERIC (stock), 6 vCPU cd /root/df3093 sh run2.sh 20 6 3 2097152 # base campaign sh run4.sh 2 6 2 16777216 # fallback-forced geometry sh run6.sh 2 6 2 16777216 # word-straddling geometry # detection: fat_scan after every cycle + race2 foreign-byte check; # on an instrumented kernel (instr.patch): sysctl debug.df3093_double
Expected on a healthy kernel: fat_scan: clean, foreign bytes 0. A
cross-link shows as max in-degree >= 2 + CORRUPTION FOUND, or as
foreign bytes in per-writer fill checks.
Environment
DragonFly 6.5-DEVELOPMENT #0 X86_64_GENERIC (stock release kernel, no INVARIANTS), 6-vCPU QEMU/KVM, guest /usr/src byte-identical to the audited tree (md5 aecdffd0dbf7e0195421d73ccb959831 for msdosfs_fat.c); diagnostic kernels #1-#6 built with make nativekernel from the same tree (guest restored to the stock snapshot after verification).
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.
(No fix.diff for this item: documented recommendation only - see Fix above.)
Fix verification
not_testablen/a β not reproduced, nothing to A/B
Confirmed kernel references
Detail
Evidence (decisive lines)
trace-excerpts.txt [2]/[2b] (kernel counters: 66 and +137 overlap events, 0 doubles on the timing-unmodified kernel); VERDICT.md 'What was verified TRUE' + 'Why the race window does not convert'; fat_scan clean results in run4.log/run6.log; instr.patch is the exact diagnostic kernel
PoC changes
Replaced the staggered v1 racestress with race2.c (CPU pinning via usched_set(2), per-cache-line barrier words, pre-created/pre-opened files, single large write(2) per round, errno/timing capture); added fat_scan.c (offline FAT in-degree cross-link detector); added run3-6.sh geometries forcing the fallback scan and word-straddling bitmap RMW overlaps; added instr.patch diagnostic kernel (sysctls debug.df3093_{inflight,overlaps,double}).
Verified recommended fix
implement a real per-mount lock (or lwkt token) behind MSDOSFS_LOCK_MP/UNLOCK_MP and lockinit/destroy it in msdosfs_mount/unmount, serializing clusteralloc/chainalloc/usemap_* like FreeBSD's pm_fatmutex
Verdict
Premise verified, manifestation not reproduced. MSDOSFS_LOCK_MP/UNLOCK_MP are literally empty macros (msdosfsmount.h:216-218), struct msdosfsmount has no lock member, and a diagnostic kernel (instr.patch: stock code + counters + a print-only form of the INVARIANTS KASSERT at msdosfs_fat.c:381) PROVED the race window opens in practice: >200 clusteralloc critical-section overlap events, up to 3 CPUs deep, under 6 CPU-pinned barrier-synchronized writers. But the claimed double-allocation never occurred: across ~4 hours, ~500 synchronized allocation bursts and 6 adversarial geometries (64MiB-2GiB FAT16/FAT32 volumes, fallback-scan-forcing fragmentation, half-word and word-straddling free runs, 100us/200us delay-injected kernels widening the decide window 1000x), debug.df3093_double stayed 0, every offline FAT in-degree scan was clean, no foreign bytes, no 'Freeing unused sector' messages. The pure-memory scan phase (bitmap read to first bit set) is sub-microsecond and the allocator's pm_nxtfree-advance + strictly-greater tie-break (msdosfs_fat.c:756/768) effectively serialize run claiming, so two CPUs never commit the same run. The INVARIANTS-panic and release-kernel cross-link claims remain theoretical on this hardware; the missing lock is still a genuine hardening defect worth fixing (FreeBSD added pm_fatmutex for this surface).
No comments yet.