# DF-0883 — Unvalidated logical block size `lb_size` enables divide-by-zero and UB shifts

## Verdict

**REPRODUCED** — deterministic kernel divide-by-zero (#DE) panic at mount time.
Fix **VALIDATED**: single-fix `udf.ko` module rejects `lb_size=0` with `EINVAL`;
the previously-fatal mount now returns cleanly with no panic.

## Summary

In `udf_mountfs()` (`sys/vfs/udf/udf_vfsops.c:306`), the Logical Volume
Descriptor's `lb_size` field — a `uint32_t` read verbatim from the on-disk
image — is assigned directly to `udfmp->bsize` with **no validation**:

```c
306:  udfmp->bsize = lvd->lb_size;              // from disk, UNVALIDATED
307:  udfmp->bmask = udfmp->bsize - 1;           // bsize=0 => bmask = 0xFFFFFFFF
308:  udfmp->bshift = ffs(udfmp->bsize) - 1;     // bsize=0 => ffs(0)=0, bshift = -1
```

Then in `udf_find_partmaps()` (line 667), `bsize` is used as a divisor:

```c
667:  udfmp->p_sectors = pms->packet_len / udfmp->bsize;   // #DE divide-by-zero
```

A crafted UDF image with `lb_size=0` and a Type 2 Sparable partition map
(`packet_len=2048`) triggers a fatal integer-divide fault (#DE, trap 18) in
kernel mode the moment the image is mounted.

## Mechanism (trigger → primitive → effect)

1. **Trigger**: root (or `vfs.usermount=1` unprivileged user) mounts a crafted
   UDF image via `mount -t udf -o rdonly /dev/vn0 /mnt`.
2. **Anchor read**: `udf_mountfs` reads the Anchor VDP at sector 256
   (`udf_vfsops.c:280-284`), which points to the VDS at sector 0.
3. **LVD parse**: the VDS loop (`:298-327`) finds the Logical Volume Descriptor
   at sector 0 and enters the `TAGID_LOGVOL` branch (`:305`).
4. **Unvalidated assignment**: `udfmp->bsize = lvd->lb_size = 0` (`:306`).
   `bmask` becomes `0xFFFFFFFF`, `bshift` becomes `-1` (undefined behavior).
5. **Divide-by-zero**: `udf_find_partmaps` is called (`:311`). For the Type 2
   Sparable partition map, line 667 executes `2048 / 0` → **integer divide
   fault (trap 18)**.
6. **Effect**: kernel panic, guest enters DDB, system halted. Deterministic,
   single-shot, no race conditions.

## Escalation

This is a pure **Denial of Service** (divide-by-zero panic). There is no memory
corruption — the #DE trap fires before any write occurs. No escalation to
`uid=0` is possible or relevant. The finding's impact ceiling is **local DoS
via crafted filesystem image mount**.

## Evidence

### Unpatched kernel #0 (panic)
```
Fatal trap 18: integer divide fault while in kernel mode
cpuid = 2; lapic id = 2
instruction pointer        = 0x8:0xffffffff82600fc4
stack pointer               = 0x10:0xfffff80118295340
current process             = 1019
kernel: type 18 trap, code=0

Stopped at      udf_mount.part.2+0x814: idivl   0x200(%r14),%eax
db>
```

### Patched udf.ko (fix applied, no panic)
```
[+] attempting UDF mount (lb_size=0, packet_len=2048)...
mount_udf: /dev/vn0: Invalid argument
[!] mount returned 1
[+] guest still alive:
10:59PM  up 2 mins, 0 users, load averages: 0.11, 0.06, 0.02

dmesg: udf: invalid logical block size 0
```

## PoC changes

- Authored `craft_evil_udf.py`: Python3 UDF image crafter that builds a
  minimal image with `lb_size=0` in the LVD and a Type 2 Sparable partition
  map with `packet_len=2048` to drive the division at `udf_vfsops.c:667`.
- Fixed a field-order bug in the Anchor VDP's `extent_ad` (len before loc,
  matching `struct extent_ad { uint32_t len; uint32_t loc; }`).
- `build.sh` / `run.sh`: standard vnconfig + mount harness.

## Fix

`fix.diff` adds a validation check immediately after `udfmp->bsize =
lvd->lb_size` (line 306): if `lb_size` is not a power of two in
`[DEV_BSIZE (512), MAXBSIZE (65536)]`, print a diagnostic and return `EINVAL`.
This closes both the divide-by-zero (lb_size=0) and the undefined-shift /
mask-corruption (non-power-of-2) variants.

**Supersedes** the finding proposal, which recommended the same range but did
not specify the power-of-2 bitmask check (`lb_size & (lb_size - 1)`) that
additionally rejects non-power-of-2 values which would corrupt `bmask`/`bshift`
arithmetic throughout the UDF mount/read paths.

Note: UDF is a loadable kernel module (`kldload udf`). The fix lives in
`/boot/kernel/udf.ko`, not the kernel binary. The before/after comparison uses
the same `#0` kernel with the original vs fixed `udf.ko` module — the only
variable is the one-file source change in `udf_vfsops.c`.
