# DF-0106 — dkcksum32 OOB read via crafted on-disk label in writedisklabel (PoC)

`l32_writedisklabel()` (`sys/kern/subr_disklabel32.c:363-364`) reads the
existing on-disk label from media and calls `dkcksum32(dlp)` on it **without**
the `d_npartitions > MAXPARTITIONS32` guard that protects the read path
(`l32_readdisklabel`, `subr_disklabel32.c:225-226`). `dkcksum32()`
(`sys/sys/disklabel32.h:150-161`) computes its end pointer from the
attacker-controlled `d_npartitions` field. A crafted disk with
`d_npartitions = 0xFFFF` makes `dkcksum32` walk ~1 MiB past the I/O buffer.

## Build

On the guest, as the unprivileged build user:

```
cd poc/DF-0106
sh build.sh        # cc -O0 -o poc_writedisklabel poc_writedisklabel.c
```

## Run

On the guest, **as root** (creates a memory disk + writes the crafted sector +
issues the ioctl):

```
cd poc/DF-0106
sh run.sh
```

The PoC:
1. `dd` an 8 MiB image, `vnconfig -c vn0 /tmp/oob.img` → `/dev/vn0`, `/dev/vn0s0`.
2. `pwrite` a **crafted on-disk disklabel32** at sector 1 (byte 512) of
   `/dev/vn0`: `d_magic = d_magic2 = DISKMAGIC32`, `d_npartitions = 0xFFFF`
   (sector-aligned 512-byte write).
3. `open("/dev/vn0s0", O_RDWR)`, fetch a valid label via `DIOCGDVIRGIN32`,
   then `ioctl(fd, DIOCWDINFO32, &virgin)`.

`DIOCWDINFO32` first installs the valid label internally
(`DIOCSDINFO`/`l32_setdisklabel`, passes), then calls
`op_writedisklabel = l32_writedisklabel`, which READS sector 1 (the crafted
label) and runs the unguarded `dkcksum32(dlp)` (`subr_disklabel32.c:363-364`).

## Expected / observed outcome

The **missing-guard bug is confirmed in source** (`subr_disklabel32.c:363-364`
calls `dkcksum32(dlp)` with no `d_npartitions` bound, unlike the guarded read
path at `:225-226`), and `dkcksum32` provably executes on this path (the scan
loop at `:358-364` evaluates `dkcksum32(dlp)` at sector offset 0 where the
crafted label's `d_magic`/`d_magic2` both match).

**However**, on this master DEV kernel the writedisklabel read buffer is a
`getpbuf_mem` buffer whose data area (`bp->b_data = b_kvabase`) lives in a
single ~24 MiB contiguous, fully-wired region (`swapbkva_mem`,
`sys/vm/vm_pager.c:235,263-291`). The ~1 MiB `dkcksum32` walk therefore stays
inside mapped memory for ~184 of the ~190 pbufs and returns nonzero (the
loop then finds no "valid" on-disk label and returns `ESRCH`). It would fault
only for the ~6 pbufs whose `b_kvabase` is in the last ~1 MiB of the region —
and those sit at the *tails* of the 16-bucket LIFO free list
(`getpbuf_mem`, `vm_pager.c:505-547`), unreachable from userspace with only 4
`vn` devices.

In **400+ harness iterations** (tight loops, 80-way concurrency across 4 `vn`
devices, concurrent I/O churn) the writedisklabel path **never panicked**;
every invocation returned `ESRCH`. So the *claimed deterministic panic* does
not reproduce on this kernel's writedisklabel path.

The **identical root-cause panic IS reproduced live** via sibling DF-0107,
where `dkcksum32` is called from `l32_setdisklabel` on a `kmalloc(404)`
buffer that reliably lands near a thread-stack guard page on fresh boots
(`panic: vm_fault: fault on stack guard ... l32_setdisklabel+0x57`). Fixing
`dkcksum32` (the shared helper) closes both findings.

## Files

| file | purpose |
|------|---------|
| `poc_writedisklabel.c` | trigger: plant crafted on-disk label + DIOCWDINFO32 |
| `build.sh` / `run.sh` | exact build / run invocations |
| `build.log` | final successful build, full output |
| `run.log` | 10 representative iterations (ESRCH, no panic) + outcome note |
| `panic.txt` | no live panic on this path; points to sibling DF-0107's signature |
| `env.txt` | guest `uname`, `cc` version, device permissions, ids |
| `fix.diff` | git-apply-able root-cause fix (clamp `dkcksum32`) — identical to DF-0107 |
| `VERDICT.md` | full narrative |
| `manifest.json` | machine-readable catalog |
