pcbmap() accepts cluster numbers beyond pm_maxcluster (fatmask + reserved-range checks only) β crafted FAT chain yields cross-volume kernel-mediated read/write anywhere on the backing device beyond the mounted volume
| Field | Value |
|---|---|
| ID | DF-3091 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H |
| CWE | CWE-125 / CWE-787 / CWE-20 |
| File | sys/vfs/msdosfs/msdosfs_fat.c |
| Lines | 184-226 (fix.diff also touches msdosfs_vnops.c:728) |
| Area | vfs/msdosfs |
| Confidence | certain |
| Discovered | 2026-09-06 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | base:vfs |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
pcbmap()'s chain walk stops only on the reserved range ((cn | ~pm_fatmask) >= CLUST_RSRVD); a cluster number between pm_maxcluster and 0xfff6 passes both the fatmask and reserved checks and is translated by cntobn() to a device block beyond the volume claimed by the BPB. On a crafted image (32KiB FAT16 volume embedded in an 8MiB device, FAT[2]=7000, pm_maxcluster=5119), file reads return the out-of-volume bytes at device block 7020 and writes through the mount persist there β kernel-mediated cross-volume read/write on the backing device. VERIFIED (findings/poc/DF-3091/): TEST 1 read returned the planted marker DF3091-OUT-OF-VOLUME-MARKER from beyond the volume; TEST 2's 34-byte write changed the raw image sector to PWNED-BY-DF3091-CROSS-VOLUME-WRITE. Fix validated (run.log.patched): clamp cn > pm_maxcluster β hiteof in the walk and after FAT assembly, plus a bio_offset==NOOFFSET guard in msdosfs_write so unmappable blocks can't enter the delayed-write path. Crafted-media threat model (root-mount or vfs.usermount=1): attacker reads/writes other partitions' data (other OS partitions, other volumes) on media they control.
Timeline
- 2026-09-06 Discovered during pass-2 audit of msdosfs_fat.c (GLM 5.3); cross-volume R/W reproduced + fix validated. DF-0827/0828 re-verified, not re-reported.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3091 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.8 KB | β raw | |
| VERDICT.md | β | 4.7 KB | β raw | |
| craftfat16.c | β | 4.3 KB | view raw | |
| build.sh | β | 112 B | view raw | |
| build.log | β | 119 B | view raw | |
| run.sh | β | 109 B | view raw | |
| run.log | β | 2.0 KB | view raw | |
| run.2.log | β | 2.0 KB | view raw | |
| fix_run.log | β | 1.8 KB | view raw | |
| fix_build.log | β | 726 B | view raw | |
| run.log.patched | β | 1.9 KB | β download | |
| fix.diff | β | 1.1 KB | view raw | |
| env.txt | β | 943 B | view raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 3.8 KB | view raw |
DF-3091 β pcbmap() accepts cluster numbers beyond pm_maxcluster
Cross-volume kernel-mediated read AND write through a mounted FAT
filesystem, from a crafted FAT chain whose value lies between
pm_maxcluster and the reserved range.
Threat model
Crafted media: the attacker controls a disk image / USB stick that a
victim mounts (root, or any user with vfs.usermount=1). The crafted
FAT volume claims a small slice of the device; a file's FAT chain then
references cluster numbers that pass pcbmap()'s only guards
(pm_fatmask + reserved-range) but exceed pm_maxcluster, so
cntobn() maps them to device blocks beyond the volume β e.g. into
neighboring partitions on the same medium. Reads disclose those bytes;
writes persist attacker data there.
Build (in the DragonFly guest, as root)
cd /root/df3091 cc -O -o craftfat16 craftfat16.c # see build.sh / build.log
Run
sh run.sh # baseline: expect the two markers below
Success criteria (stock kernel):
- TEST 1 read:
dd if=/mnt/df3091/LEAK.TXT bs=512 skip=1 count=1returnsDF3091-OUT-OF-VOLUME-MARKERβ bytes from device block 7020, which lies beyond the 4 MiB claimed volume (blocks 0..8191) on the 8 MiB device. - TEST 2 write: writing
PWNED-BY-DF3091-CROSS-VOLUME-WRITEthrough the mount changes the raw image at device block 7020 (outside the volume).
Fixed kernel (fix.diff, single-fix rebuild): TEST 1 returns no data
(EIO), TEST 2 fails with Input/output error, the out-of-volume marker
sector is unchanged, and the in-volume control file still reads
correctly. See run.2.log (fresh baseline), fix_run.log (fresh
fixed-kernel run), and the pass-1 logs (run.log, run.log.patched).
Artifacts
See manifest.json for the full list.
DF-3091 VERDICT β pcbmap() maps out-of-range FAT chain values beyond the mounted volume (cross-volume read + write)
Status: REPRODUCED (fresh re-verification on stock kernel 2026-10-12) Fix VALIDATED on a single-fix rebuilt kernel (fixed 2026-10-12).
Root cause (line-accurate, current tree)
pcbmap() walks a file's FAT chain and only stops at the reserved
range β sys/vfs/msdosfs/msdosfs_fat.c:183-193:
for (; i < findcn; i++) {
/*
* Stop with all reserved clusters, not just with EOF.
*/
if ((cn | ~pmp->pm_fatmask) >= CLUST_RSRVD) /* :187 */
goto hiteof;
... cn = <next FAT entry>; cn &= pmp->pm_fatmask;
After the loop, any non-EOF value is mapped unconditionally β
sys/vfs/msdosfs/msdosfs_fat.c:225-230:
if (!MSDOSFSEOF(pmp, cn)) {
...
if (bnp)
*bnp = cntobn(pmp, cn); /* :230 */
There is no cn > pmp->pm_maxcluster check anywhere in pcbmap(),
although every other FAT function has one (fatentry :485,
clusteralloc :565/:632, chainalloc :759, chaincontig :823,
extendfile :897/:906/:950). pm_maxcluster itself is the lesser of
the BPB-derived cluster count and the FAT's entry capacity β clamped at
mount in sys/vfs/msdosfs/msdosfs_vfsops.c:486-490 (our image's mount
prints Warning: number of clusters (8172) exceeds FAT capacity (5120)
on the serial console and clamps to 5119). A FAT chain value between
pm_maxcluster+1 and CLUST_RSRVD-1 (FAT16: 0xfff5) therefore passes
every check in pcbmap and is translated by cntobn(cn) = firstcluster +
(cn-2)*SecPerClust into a device block far beyond the volume the mount
claimed β but still inside the backing device, so the bread/bwrite the
caller performs hits real, out-of-volume device sectors.
Reproduction (this run, fresh vm.sh reset with-src boot, stock kernel)
Crafted image (craftfat16.c): 8 MiB device, 32 KiB..4 MiB FAT16 volume
(HugeSectors=8192 β pm_maxcluster=5119 after the capacity clamp), file
LEAK.TXT with chain [2, 7000]: cluster index 1 = phantom cluster
7000 β device block 7020 (byte 3,594,240), 3.4 MiB beyond the volume
end. A marker is planted at that out-of-volume block.
- TEST 1 (read):
dd if=$MNT/LEAK.TXT bs=512 skip=1 count=1returnedDF3091-OUT-OF-VOLUME-MARKER:...β the kernel read device bytes from beyond the mounted volume into a file. Out-of-volume disclosure. - TEST 2 (write): 34 bytes written through the mount at file offset 512
changed the raw image at device block 7020 to
PWNED-BY-DF3091-CROSS-VOLUME-WRITE. Out-of-volume tampering that survives unmount (persisted to the medium). - Control:
CTRL.TXT(honest in-volume chain) reads its in-volume data β the bug does not disturb normal operation.
Logs: run.2.log (this run), run.log (pass-1, identical result).
Impact (honest)
This is a filesystem containment break, not kernel memory corruption:
the primitive reads/writes device sectors of the same backing device
that are outside the mounted volume β i.e. other partitions/slices of
attacker-supplied media (or, with vfs.usermount=1, of any device the
unprivileged mounter may access). Precondition: root mounts the crafted
image (default vfs.usermount=0) or usermount is enabled β matching
the finding's PR:H. Realistic ceiling: disclosure of other partitions'
data and silent tampering with it (integrity). Not a uid=0 chain by
itself on this guest; the audited severity (High, C:H/I:H) stands.
Fix validation (single-fix kernel, this run)
fix.diff (authored pass-1, re-verified): clamp cn > pm_maxcluster β
hiteof both inside the walk (msdosfs_fat.c:187) and after FAT
assembly (after :226), plus a bio_offset == NOOFFSET guard in
msdosfs_write (msdosfs_vnops.c:728) so unmappable clusters can't
enter the delayed-write path.
Applied to a clean-source snapshot, make nativekernel
KERNCONF=X86_64_GENERIC (~19 min), make installkernel, reboot
(DragonFly 6.5-DEVELOPMENT #1: Mon Oct 12 12:07:23 UTC 2026), exact
same run.sh:
- TEST 1: no marker bytes returned (read fails EIO β no out-of-volume data reaches userspace).
- TEST 2:
dd: /mnt/df3091/LEAK.TXT: Input/output error, 0 bytes transferred; the out-of-volume marker sector is byte-identical before/after. - In-volume control file unaffected; mount/umount clean.
Baseline reproduced / patched not reproduced β fix_status: fixed.
Pass-1 had already observed the same (run.log.patched).
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.
Fix verification
fixedSingle-fix kernel (fix.diff applied to clean /usr/src, nativekernel X86_64_GENERIC, installkernel, reboot): baseline had reproduced both out-of-volume read and write; on the patched kernel TEST1 returns no out-of-volume bytes (EIO), TEST2 fails EIO with 0 bytes transferred, the out-of-volume marker sector is byte-identical before/after, and the in-volume control file still reads correctly. Bad behavior gone; honest I/O unaffected.
fix_run.log (patched-kernel run: 'dd: Input/output error', empty TEST1 hexdump, unchanged AFTER marker); fix_build.log (build/install/uptime record)
Confirmed kernel references
Detail
Exploit chain
craft image (FAT[2]=7000, HugeSectors=8192, FATsecs=20 -> pm_maxcluster=5119) -> root mounts image (vfs.usermount=0 default; crafted-media model) -> open+read/write /mnt/LEAK.TXT -> pcbmap maps file cluster 1 to device block 7020 outside the volume -> kernel bread/bwrite hit other partitions' sectors: read = cross-volume disclosure, write = cross-volume tampering persisted to the medium
Evidence (decisive lines)
run.2.log lines 'TEST 1' (marker returned) and 'TEST 2' (PWNED-BY-DF3091 in the AFTER hexdump); fix_run.log (EIO both tests, marker unchanged); run.log/run.log.patched = identical pass-1 results; VERDICT.md has the full line-accurate trace
PoC changes
none vs pass-1 pack (same craftfat16.c/run.sh); this run added README/VERDICT/manifest/verdict.json/build.sh/build.log/fix_build.log and fresh baseline+fixed logs (run.2.log, fix_run.log)
Verified recommended fix
clamp cn > pm_maxcluster -> hiteof in the pcbmap walk and after FAT assembly, plus reject bio_offset==NOOFFSET buffers in msdosfs_write (see fix.diff)
Verdict
REPRODUCED on the stock kernel (fresh vm reset): a crafted FAT16 chain value (7000) between pm_maxcluster (5119) and the reserved range passes pcbmap()'s only guards (fatmask + reserved-range, msdosfs_fat.c:187) and is mapped by cntobn() (:230) to device block 7020 β 3.4MiB beyond the 4MiB claimed volume. Reading LEAK.TXT offset 512 returned the planted out-of-volume marker (disclosure); a 34-byte write through the mount persisted PWNED-BY-DF3091 at that raw device block (tampering). Fix validated on a single-fix rebuilt kernel: read fails EIO with no out-of-volume bytes, write fails EIO, marker sector unchanged, in-volume control file unaffected.
No comments yet.