# DF-2878 — l32_setdisklabel installs partition tables without slice-bounds validation → slice escape (R/W outside the slice via partition device)

## Impact
`DIOCSDINFO32` / `DIOCWDINFO32` on a cooked disk slice accept an in-core
`disklabel32` whose non-raw partitions have arbitrary `p_offset` (u32, up to
~2 TiB past the slice start at 512 B/sector). Every subsequent I/O through
the partition device node (e.g. `/dev/vn0s1a`) is translated by `dscheck()`
to `(ds_offset + p_offset + secno) * secsize` — **outside the slice** —
giving reads and writes of disk data the slice device was never supposed to
reach (other slices of the same disk; past-slice-end areas of the media).

The label64 twin validates exactly this on the same ioctl path
(`l64_setdisklabel`, sys/kern/subr_disklabel64.c:275-306:
`p_boffset < d_pbase → ENOSPC`, `p_boffset + p_bsize > d_total_size →
ENOSPC`, plus `d_npartitions > MAXPARTITIONS64 → EINVAL`). The label32 read
path enforces it too (`l32_fixlabel`, sys/kern/subr_disklabel32.c:593-609
bounds every partition within the slice). The label32 **set** path is the
one unguarded twin: it checks only `p_size > sp->ds_size`
(subr_disklabel32.c:310-313) and `RAW_PART.p_offset != 0` (:306).

Reachability gate (same as DF-2741): opening a cooked disk node requires
`SYSCAP_RESTRICTEDROOT` (sys/kern/subr_disk.c:1072), so the trigger is
root-class on a stock host. The kernel nevertheless guarantees
slice-relative bounding of partition devices everywhere else; this is the
missing enforcement (defence-in-depth + jail/devfs scenarios where a slice
device is delegated).

Bonus divergence proof: on the write path, `l32_writedisklabel` →
`l32_fixlabel(TRUE)` silently bzeroes out-of-bounds partitions in the buffer
copy it writes to media (:609), so the on-disk label stays bounded while the
in-core one is not — confirming the intended invariant the set path forgets.

## Reproduce (guest, as root)
```
# host:  python3 mkimg.py df2878.img && scp df2878.img poc2878.c root@guest:/root/
# guest:
cd /root && cc -O -Wall -o poc2878 poc2878.c
vnconfig -c vn0 /root/df2878.img && sleep 1     # probe creates vn0s1a (in-slice 'a')
./poc2878 /root/df2878.img
vnconfig -u vn0
```

## Expected output (stock kernel)
```
BEFORE ioctl, vn0s1a@0  ... : df2878-inside-slice
BEFORE ioctl, vn0s1a@61796 ...: read=-1 errno=22     <- boundary normally holds
DIOCSDINFO32: accepted ...
AFTER  ioctl, vn0s1a@0  ... : DF2878-OUTSIDE-SLICE-SECRET
READ-ESCAPE: PASS ...
backing image @63845 ... : DF2878-ESCAPED-WRITE
WRITE-ESCAPE: PASS ...
DF-2878 REPRODUCED
```

## Expected output (patched kernel, fix.diff)
`DIOCSDINFO32: REJECTED: No space left on device` (ENOSPC) — the kernel
refuses the out-of-slice label; `vn0s1a` keeps serving in-slice data.

## Fix
`fix.diff` — in `l32_setdisklabel`: cap `d_npartitions` *before*
`dkcksum32()` (also closes the DF-0106/0107 dkcksum OOB family on this
path) and require `p_offset + p_size <= ds_size` for every partition,
mirroring `l64_setdisklabel`.
