# 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`:

```c
	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`:

```c
	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.
