# DF-0881 — Heap OOB read in sparing-table scan: `rt_l` unbounded by `st_size`

## Verdict: REPRODUCED (panic on unpatched kernel) + FIX VALIDATED

**Severity:** Medium (heap OOB read / info-leak / mount-time DoS via crafted UDF image)
**Confidence:** certain
**Status:** reproduced → fix authored → fix built → fix validated (before/after on single-fix kernel)

---

## Mechanism

The bug is in `udf_find_partmaps()` (`sys/vfs/udf/udf_vfsops.c`), called during
UDF mount when the Logical Volume Descriptor contains a Type 2 Sparable Partition
Map. The flow:

1. **`udf_vfsops.c:662`** — `udfmp->s_table = kmalloc(pms->st_size, M_UDFMOUNT, M_WAITOK | M_ZERO);`
   Allocates `st_size` bytes for the sparing table. `st_size` comes from the
   on-disk partition map (`struct part_map_spare.st_size`, a `uint32_t`).

2. **`udf_vfsops.c:681`** — `bcopy(bp->b_data, udfmp->s_table, pms->st_size);`
   Copies `st_size` bytes from the disk buffer into the allocation. This copy is
   correctly bounded by `st_size`.

3. **`udf_vfsops.c:692`** — **THE BUG:**
   ```c
   for (i = 0; i < udfmp->s_table->rt_l; i++) {
       udfmp->s_table_entries = i;
       if (udfmp->s_table->entries[i].org >= 0xfffffff0)
           break;
   }
   ```
   The loop iterates `rt_l` times (from `struct udf_sparing_table.rt_l`, a
   `uint16_t` read from the on-disk sparing table). **`rt_l` is NOT validated
   against `st_size`.** Since `entries[]` starts at offset 56
   (`offsetof(struct udf_sparing_table, entries)` = tag(16)+regid(32)+rt_l(2)+
   reserved(2)+seq_num(4) = 56) and each entry is 8 bytes
   (`sizeof(struct spare_map_entry)` = org(4)+map(4)), the loop reads:
   - `entries[0]` at offset 56 (valid if `st_size >= 64`)
   - `entries[k]` at offset `56 + 8*k` — OOB when `56 + 8*k >= st_size`

   With `st_size=64` and `rt_l=65535`, `entries[1]` (offset 64) is already OOB,
   and the loop reads up to offset 524,328 — **~512 KB past a 64-byte allocation**.

4. **Downstream impact** — `udfmp->s_table_entries = i` is set from the OOB-derived
   loop counter, then used in `udf_translate()` (`udf_vnops.c:1167`) to scan the
   same `entries[]` array again for sector remapping, compounding the OOB.

## Reproduction

### PoC: crafted UDF image (`evil.udf`)

A Python script (`craft_evil_udf.py`) generates a minimal UDF image with:
- Anchor VDP at sector 256 → VDS at sector 0
- Partition Descriptor (TAGID_PARTITION=5) at sector 0
- Logical Volume Descriptor (TAGID_LOGVOL=6) at sector 1 with a Type 2 Sparable
  Partition Map: `st_size=32768`, `st_loc[0]=34`, `packet_len=2048`
- Sparing Table at sector 34: tag id=0, `rt_l=65535`, 1 entry (org=0)

`st_size=32768` forces the allocation through the kmem page-zone (allocations >
`ZALLOC_ZONE_LIMIT`=16384 get dedicated pages), so the OOB read quickly hits an
unmapped page → deterministic panic.

### Before (unpatched kernel `#0`):

```
vnconfig -c vn0 evil.udf
mount -t udf -o rdonly /dev/vn0 /mnt/udf
```
→ **Kernel panic:**
```
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xfffff80118632000
fault code = supervisor read data, page not present
current process = 952 (mount)
Stopped at udf_mount.part.2+0x8c9: cmpl $-0x11,0x38(%rsi,%rdx,8)
```

The disassembly `cmpl $-0x11,0x38(%rsi,%rdx,8)` is exactly `entries[i].org >=
0xfffffff0`: offset 0x38=56, `%rsi`=s_table, `%rdx`=i, `$-0x11`=0xFFFFFFEF
(the `>=` compiles to `> 0xFFFFFFEF`). `udf_mount.part.2` is GCC's name for
`udf_find_partmaps()` partially inlined into `udf_mount()`.

### After (patched kernel `#1`):

Same PoC → `mount_udf: /dev/vn0: Invalid argument` (EINVAL), **guest alive**, no
panic. The EINVAL is from the intentionally-missing File Set Descriptor, not from
the bug. The sparing-table scan completed within bounds (no OOB read).

## Exploit chain

**Class:** heap OOB read (read-only primitive). No write capability.
**Escalation:** none possible — this is a pure read primitive. The OOB read leaks
kernel heap data (~512 KB of adjacent slab/kmem objects per mount attempt) and
corrupts `s_table_entries` (driving further OOB reads in `udf_translate`). On the
GENERIC kernel (INVARIANTS ON), this manifests as a panic when the read crosses a
page boundary; with smaller `st_size` (slab-zone allocation), the OOB reads
mapped heap silently (info leak).

**Impact ceiling:** Mount-time heap info-leak + DoS (panic). Realistic
precondition: root mounts an attacker-supplied UDF image (or
`vfs.usermount=1` + root-created image owned by the attacker).

## Fix

`fix.diff` — two changes in `udf_find_partmaps()`:

1. **Validate `st_size`** before `kmalloc`: reject if `st_size < sizeof(struct
   udf_sparing_table)` (header + at least 1 entry must fit).

2. **Bound the entry scan loop** by the number of entries that actually fit in
   `st_size`:
   ```c
   max_entries = (pms->st_size -
       offsetof(struct udf_sparing_table, entries)) /
       sizeof(struct spare_map_entry);
   for (i = 0; i < udfmp->s_table->rt_l && i < max_entries; i++) {
   ```

### Fix validation (Phase 8)

| Phase | Kernel | Result |
|-------|--------|--------|
| Baseline (unpatched `#0`) | `6.5-DEVELOPMENT #0` | **PANIC** — page fault in `udf_mount.part.2+0x8c9` |
| Patched (`#1`, fix applied) | `6.5-DEVELOPMENT #1` | **CLEAN** — EINVAL, no panic, guest alive (×2 runs) |

The fix is deterministic: `rt_l` is now clamped to `max_entries`, the scan stays
within the allocation, and no page fault occurs.

## PoC changes

Authored from scratch (no prior PoC existed):
- `craft_evil_udf.py` — Python UDF image crafter (parameterized st_size/rt_l)
- `evil.udf` — crafted image with `st_size=32768, rt_l=65535`
- `build.sh` / `run.sh` — exact reproduction commands
- `fix.diff` — git-apply-able fix (validated on built+booted kernel)

## Kernel references

- `sys/vfs/udf/udf_vfsops.c:662` — `kmalloc(pms->st_size)` allocation
- `sys/vfs/udf/udf_vfsops.c:681` — `bcopy(..., pms->st_size)` (correctly bounded)
- `sys/vfs/udf/udf_vfsops.c:692` — **THE BUG:** `for (i=0; i<s_table->rt_l; i++)` unbounded by st_size
- `sys/vfs/udf/udf_vfsops.c:694` — `entries[i].org` OOB read
- `sys/vfs/udf/ecma167-udf.h:263-270` — `struct udf_sparing_table` layout (entries at offset 56)
- `sys/vfs/udf/ecma167-udf.h:234-246` — `struct part_map_spare` (st_size field)
- `sys/vfs/udf/udf_vnops.c:1167` — downstream `udf_translate` scan using corrupted `s_table_entries`
