# DF-2878 — VERDICT

**Finding:** `l32_setdisklabel()` (sys/kern/subr_disklabel32.c:249-315) installs an
in-core `disklabel32` via `DIOCSDINFO32`/`DIOCWDINFO32` with **no structural bounds
validation of the partition table** — `d_partitions[].p_offset` is never checked
against the slice (the final loop at :310-313 checks only `p_size > sp->ds_size`),
`d_npartitions` is never capped (that half is the DF-0106/0107 dkcksum family),
and there is no `p_offset + p_size` bounds/overflow check.

## Verdict: REPRODUCED (deterministic, 3/3 runs, fresh image)

**Primitive:** slice-boundary escape (out-of-slice read **and** write on the
underlying device, through an ordinary cooked partition node).

### How it works, end to end
1. Hostile/delegated label via `ioctl(vn0s1_fd, DIOCSDINFO32, &label)`:
   `dsioctl()` (sys/kern/subr_diskslice.c:561-618) requires
   `slice != WHOLE_DISK_SLICE && part == WHOLE_SLICE_PART`, FWRITE, and — if a
   label already exists — same label type (:590-591). It hands the user label to
   `ops->op_setdisklabel` = `l32_setdisklabel`.
2. `l32_setdisklabel` verifies only magic + `dkcksum32` (:264-265), the
   open-partition compat loop (:272-301, no partitions open ⇒ skipped), copies
   the label into the in-core slot (:304), and checks `RAW_PART.p_offset == 0`
   (:306) and `p_size <= ds_size` per partition (:310-313). **No `p_offset`
   bound, no `d_npartitions` cap.** The temp label becomes the installed
   in-core label (subr_diskslice.c:616-617).
3. `dsopen`/`dscheck` on the (already existing) partition node `/dev/vn0s1a`:
   `l32_getpartbounds` (subr_disklabel32.c:123-135) returns the attacker's
   `p_offset`/`p_size`; `dscheck` (subr_diskslice.c:206-212, 280-281)
   translates every I/O to `(sp->ds_offset + p_offset + secno) * dss_secsize`
   — absolute sectors **outside the slice**.
4. Observed on the guest (64 MiB vn image, MBR slice = sectors [2048, 63744),
   marker at absolute 63844):
   - BEFORE: `vn0s1a` returns in-slice filler; out-of-slice offset → `EINVAL`
     (the boundary holds with the on-disk label).
   - `DIOCSDINFO32` **accepted** with 'a' at slice-relative 61796
     (= 100 sectors past the slice end), p_size 8.
   - AFTER: `pread(vn0s1a, 0)` returns `DF2878-OUTSIDE-SLICE-SECRET`
     (planted at absolute 63844, outside the slice).
   - `pwrite(vn0s1a, 512)` landed at absolute 63845 in the backing image
     (verified by reading the backing file; sector was zero before).

### Why this is a real invariant violation (not just "root being root")
- The **reader** enforces slice bounds for every partition:
  `l32_fixlabel` (subr_disklabel32.c:593-609) bounds each on-media partition to
  `[ds_offset, ds_offset+ds_size)` (including the u32 wraparound case, :596)
  and zeroes offenders.
- The **label64 twin** enforces it on the same set ioctl:
  `l64_setdisklabel` (subr_disklabel64.c:275-306): `d_npartitions > MAXPARTITIONS64 →
  EINVAL`, `p_boffset < d_pbase → ENOSPC`, `p_boffset + p_bsize > d_total_size →
  ENOSPC`, plus alignment checks.
- The **writer** enforces it too: `l32_writedisklabel` → `l32_fixlabel(TRUE)`
  silently bzeroes out-of-bounds partitions in the copy it writes to media
  (:609) — so the *on-disk* label stays bounded while the *in-core* one is not:
  the kernel itself proves the invariant the set path forgets.
- Reachability gate (verified during DF-2741, same guest): opening cooked disk
  nodes requires `SYSCAP_RESTRICTEDROOT` (sys/kern/subr_disk.c:1072), so on a
  stock host the trigger is root-class. The boundary matters wherever a slice
  device is delegated below the host root (jail/devfs rulesets, VMs with a
  slice passthrough): the kernel promises partition nodes stay inside their
  slice, and everywhere except `l32_setdisklabel` it keeps that promise.

Severity filed Low per project rubric ("already-privileged user" on a stock
host); consequence class identical to DF-0134 (Medium, label64 reader) which
needs no privileges at all, and the fix mirrors label64 exactly.

## Fix validation
`fix.diff` adds to `l32_setdisklabel`:
- `nlp->d_npartitions > MAXPARTITIONS32 → EINVAL` **before** `dkcksum32()`
  (also closes the DF-0106/0107 dkcksum32 OOB-read family on this path), and
- `p_offset + p_size > sp->ds_size → ENOSPC` for every partition (u64 add, no
  wrap), replacing the `p_size`-only check.

Applied to the guest's /usr/src, `make nativekernel` (RC=0, 14,897 lines),
`make installkernel` (RC=0), reboot into kernel
`#1: Wed Sep  2 19:31:54 UTC 2026`, exact PoC re-run on a pristine image
(run.fixed.log):

```
BEFORE ioctl, vn0s1a@0  ... : df2878-inside-slice      <- cooked access intact
DIOCSDINFO32: REJECTED: No space left on device        <- ENOSPC, label64 semantics
=> kernel enforces slice bounds on the set path (patched?)
```

- fix_status: **fixed** — the out-of-slice label is rejected (ENOSPC, exactly
  `l64_setdisklabel`'s behavior at subr_disklabel64.c:304-305); the previously
  observed READ-ESCAPE/WRITE-ESCAPE are gone; the guest stayed healthy.
- No regression: `regress.c` installs an equivalent IN-SLICE label ('a' at
  slice-relative 2048) on the patched kernel — accepted (`in-slice label
  accepted`, RC=0). Normal labeling still works.
- Guest reset to the clean-source snapshot after validation.

Baseline (stock kernel #0): see run.log, run.2.log, run.3.log — READ-ESCAPE
PASS + WRITE-ESCAPE PASS, exit 0, 3/3.

## Kernel references
sys/kern/subr_disklabel32.c:264-266, 306-313 (bug), :123-135 (consumer),
:593-609 (reader enforces the invariant), :592-614 (writer sanitizes);
sys/kern/subr_diskslice.c:561-618 (DIOCSDINFO glue), :199-212+279-281 (dscheck
offset translation); sys/kern/subr_disklabel64.c:275-306 (validating twin);
sys/kern/subr_disk.c:228-270 (partition node creation), :1072
(SYSCAP_RESTRICTEDROOT open gate).
