# DF-3103 — VERDICT

**Status: REPRODUCED (leak / wild-pointer read)** — crafted UDF image mounted
by root gives any unprivileged user an out-of-bounds kernel-heap read (and a
provably wild pointer dereference) via plain `open()`+`read()`.

## Root cause chain

`sys/vfs/udf/udf_vfsops.c:527-530`:

```c
	size = UDF_FENTRY_SIZE + fe->l_ea + fe->l_ad;      /* wraps in u32/int */
	unode->fentry = kmalloc(size, M_UDFFENTRY, M_WAITOK | M_ZERO);
	bcopy(bp->b_data, unode->fentry, size);
```

Crafted FE (`icbtag.flags & 7 == 3`, embedded data): `l_ea = 0x1000`,
`l_ad = 0xFFFFF150` → sum `0x100000200` → `size = 0x200` → a **512-byte**
allocation that still carries the huge raw fields.

`sys/vfs/udf/udf_vnops.c:1037-1039` (embedded path of udf_readatoffset):

```c
		fentry = node->fentry;
		*data = &fentry->data[fentry->l_ea];   /* fentry + 176 + 4096 */
		*size = fentry->l_ad;                  /* (int)0xFFFFF150 = -5936 */
```

No validation of `l_ea`/`l_ad` against the allocation, no offset handling,
no negative-size guard.  `udf_read()` (:401-403) passes both to
`uiomove(data, size, uio)`; the negative int length sign-extends to a huge
`size_t`, so `uiomove` moves `uio_resid` (user-chosen) bytes from
`fentry + 4272` — ~3.7 KB past the 512-byte `M_UDFFENTRY` object.

## Reproduction (run.log)

Crafted image: `EMB` FE at partition-sector 3 with the fields above,
`inf_len = 0x10000`; `EMB2` FE with `l_ea = 0x40000000`,
`l_ad = 0xC0000140` (same 512-byte wrap).  Runs as `su maxx` (uid 1001):

* `read(EMB, 96)` ×3: **96 bytes returned each time** from kernel heap past
  the allocation (`DF3103_LEAK_CONFIRMED` ×3, deterministic).
* `read(EMB, 1024)` and `read(EMB, 4096)`: same, no fault — the 512-byte
  malloc zone is backed by large slabs, so the overread stays mapped.
* `read(EMB2, 96)`: **`EFAULT` (Bad address)** — `uiomove` faulted reading
  `fentry->data[0x40000000]` (1 GB past the object); DF's copy-fault
  handling converted the kernel-mode wild read into EFAULT instead of a
  panic.  This is direct proof the kernel dereferenced the attacker-chosen
  wild offset.

A "correct" kernel would have rejected this file entry outright (an FE is
one block; `l_ea + l_ad` cannot legitimately reach 2^32) — see fix.

## Impact

* Crafted-media heap OOB read with attacker-chosen offset (`l_ea` fully
  controlled: any offset up to 4 GB past a small heap object) and
  attacker-chosen length (read count).  Deterministic leak of kernel heap;
  an unmapped choice yields EFAULT (or, on kernels without the copy-fault
  guard, a kernel page fault/panic → DoS).
* Read primitive only — no write, so no direct uid=0 chain from this bug
  alone.  Combined with DF-0831-style write primitives on the same
  read-only-image attack surface, it provides the info-leak half of an
  exploit pair (heap layout disclosure).

## Fix validation (fix.diff)

* `udf_vget()`: reject `l_ea`/`l_ad` that don't fit in one logical block
  (`bsize - UDF_FENTRY_SIZE`), which kills the wrap and the DF-0880
  bcopy over-read at once.
* `udf_readatoffset()` embedded path: FreeBSD-style offset handling
  (`*data += offset; *size -= offset;` / `*size = 0` past EOF).
* `udf_read()`: `break` on `size == 0` so a short embedded file cannot spin
  the loop.

With the patched module (fix_run.log, same image, same PoC):

```
--- DF-3103 on patched kernel (expect open EMB to fail) ---
open /mnt/EMB: Invalid argument
--- DF-3103 EMB2 on patched kernel (expect open to fail) ---
open /mnt/EMB2: Invalid argument
```

No allocation, no read, clean EINVAL — behavior eliminated at the root.

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