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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3091 Β· 15 files
FileTypeDescriptionSize
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):

  1. TEST 1 read: dd if=/mnt/df3091/LEAK.TXT bs=512 skip=1 count=1 returns DF3091-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.
  2. TEST 2 write: writing PWNED-BY-DF3091-CROSS-VOLUME-WRITE through 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.

VERDICT.md
↓ download raw

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=1 returned DF3091-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

fixed
baseline reproduced→ patch + rebuild →patched clean

Single-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)
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Mon Oct 12 12:07:23 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC

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.