# DF-3102 — VERDICT

**Status: REPRODUCED (leak)** — unauthenticated-to-kernel heap disclosure via
`read()` on any mounted UDF filesystem, as an unprivileged user.

## Root cause

`sys/vfs/udf/udf_vnops.c:1045-1048`:

```c
	/* Adjust the size so that it is within range */
	if (*size == 0 || *size > max_size)
		*size = max_size;
	*size = min(*size, MAXBSIZE);
```

followed by `:1050` `udf_readlblks(udfmp, sector, *size, bp)` — which
(udf.h:96-100) bread()s only `roundup(*size, bsize)` bytes — and `:1057`:

```c
	*data = (uint8_t *)&bp1->b_data[offset % udfmp->bsize];
```

No clamp of `*size` by `bsize - (offset % bsize)` exists.  `udf_read()`
(:398-408) then does `uiomove(data, size, uio)`, which copies `size` bytes
starting at `b_data + offset%bsize` — i.e. up to `offset%bsize` bytes beyond
the buffer end — straight into the user's buffer.

## Reproduction

Crafted-but-fully-valid minimal UDF image (`craft_img.py`, extends the proven
DF-0831 skeleton): file `F1` with one short_ad extent `{len=4096, pos=4}`
(exactly two 2048-byte blocks), sector 4 = ASCII pattern, sector 5 = 0xAA.

* `pread(fd, buf, 2048, 1000)` → bmap lands on sector 4, `o = 1000`;
  `*size = min(2048, 4096, 65536) = 2048`; buffer = 2048 bytes;
  `uiomove(b_data + 1000, 2048)` touches `b_data[1000..3048)` — **1000 bytes
  past the 2048-byte allocation**.
* Reference `pread(fd, g, 3048, 0)` (in-bounds 4096-byte read) provides the
  ground truth.

Observed on the stock INVARIANTS kernel (run.log, uid=1001):

```
PREFIX-MATCH: first 1048 bytes correct
tail (bytes 1048..2048) differs from expected disk data in 1000/1000 bytes
tail identical across 3 runs: YES
```

The correct tail must be `0xAA`×1000 (disk sector 5); the kernel returned
non-disk kernel memory instead (zeros at this allocation's adjacency — i.e.
whatever follows the buf), stable across runs.  Deterministic, repeatable,
bounded only by `bsize-1` per call and freely repeatable (arbitrary volume).

Method note (attempts): the first run executed the reference read first,
which cached the same block in a 4096-byte buffer and masked the overread
(getblk returned the larger existing buffer).  Reordering the PoC —
unaligned preads FIRST, reference afterwards — reproduces on a cold cache,
which is also the real-world access pattern.  The final `run.log` reflects
the corrected order and the unprivileged (`su maxx`) execution.

## Impact

* Kernel heap info leak, up to `bsize-1` (typically 2047) bytes per call,
  unbounded aggregate; content includes stale buffer-cache pages
  (cross-file/cross-volume data) and allocator remnants.
* No write primitive — read-only filesystem, copy *from* kernel *to* user.
* Same class reachable through `udf_readdir()` (`ds->data[ds->off]` walk
  shares `udf_readatoffset`), though directory offsets are usually aligned.

## Fix validation (fix.diff)

`df31xx_combined_fix.diff` (this finding + DF-3103 + DF-3104) applied to the
guest's `/usr/src`, `udf.ko` rebuilt in `/usr/src/sys/vfs/udf`, module
swapped, image remounted, same PoC re-run (fix_run.log):

```
--- DF-3102 on patched kernel (expect tail == 0xAA, NOT_CONFIRMED) ---
tail (bytes 1048..2048) differs from expected disk data in 0/1000 bytes
DF3102_NOT_CONFIRMED: tail matched expected 0xAA disk data
```

Normal file reads still work (`od` shows the `DF3102-S0-DATA-` pattern).
The one-line clamp `*size = min(*size, udfmp->bsize - (offset % udfmp->bsize));`
makes the read complete correctly across two loop iterations (1048 + 1000
bytes) and eliminates the overread entirely.

## Upstream reference

FreeBSD sys/fs/udf/udf_vnops.c `udf_readatoffset()` carries the equivalent
fix ("Because we will read starting at block boundary, we need to adjust how
much we need to read so that all promised data is in").

## 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.
