# DF-0108 — Unvalidated d_secsize in l32_writedisklabel

## Verdict

**REPRODUCED (panic/DoS on default GENERIC) + FIX VALIDATED.** The bug is real:
`d_secsize` from a user-supplied `disklabel32` reaches `l32_writedisklabel`
without validation, and on the GENERIC kernel (INVARIANTS ON) the KKASSERT at
`subr_disklabel32.c:336` fires as a kernel panic. The single-fix kernel
(`fix.diff`) returns `EINVAL` instead and the panic is gone (3/3 clean runs).

**Privilege boundary:** root/operator-only — there is **no unprivileged→root**
escalation. `DIOCWDINFO32` requires the slice device open `FWRITE`, and slice
device nodes are `root:operator` mode `0640`. The audit user `maxx` (uid 1001,
not in `operator`) cannot trigger it. This is a root→kernel **hardening gap**.

## Mechanism (confirmed, path:line at each hop)

1. **Attacker** opens a disk slice device (e.g. `/dev/vn0s0`) `O_RDWR` and issues
   `ioctl(fd, DIOCWDINFO32, &crafted_label)` — requires root/operator
   (`subr_diskslice.c:583` checks `flags & FWRITE`; device nodes are
   `root:operator 0640`).

2. `diskioctl()` (`subr_disk.c:1202`) → `dsioctl()` (`subr_diskslice.c:650-670`).
   The DIOCWDINFO path first calls `DIOCSDINFO32` internally, which invokes
   `l32_setdisklabel` (`subr_disklabel32.c:250`).

3. **`l32_setdisklabel` validates magic, checksum, RAW_PART offset, secperunit,
   and each partition size — but NOT `d_secsize`** (`subr_disklabel32.c:264-314`).
   The crafted label (valid magic/checksum, `d_secsize = 0x200000` = 2 MiB,
   `RAW_PART.p_offset = 0`) passes all checks and is installed in-core.

4. Back in `dsioctl`, `ops->op_writedisklabel(dev, ssp, sp, sp->ds_label)` is
   called (`subr_diskslice.c:670`) → `l32_writedisklabel`.

5. **`l32_writedisklabel`** (`subr_disklabel32.c:335-340`):
   ```c
   bp = getpbuf_mem(NULL);                                   /* :335 */
   KKASSERT((int)lp->d_secsize <= bp->b_bufsize);            /* :336  PANIC */
   bp->b_bio1.bio_offset = (off_t)LABELSECTOR32 * lp->d_secsize;
   ...
   bp->b_bcount = lp->d_secsize;                             /* :340 */
   ```
   `bp->b_bufsize == MAXPHYS == 128 KiB` (`vm/vm_pager.c:391 initpbuf`).
   With `d_secsize = 2 MiB > 128 KiB`, the KKASSERT fails. On GENERIC
   (INVARIANTS ON, `sys/sys/systm.h:94-101`) `KKASSERT` expands to a `panic()`;
   on a production kernel (`INVARIANTS` OFF, `systm.h:117-118`) it is a no-op
   and the oversized `b_bcount` flows to the device strategy routine.

## Observed impact

- **GENERIC kernel (#0, INVARIANTS ON — the default):** immediate kernel panic
  at `subr_disklabel32.c:336`. Reproduced 2× from independent `vm.sh reset
  with-src` boots, identical signature. This is a **DoS** (root can already
  reboot the box, so the panic itself is not a privilege gain).

- **Production kernel (INVARIANTS OFF, non-default):** the KKASSERT is skipped
  and `bp->b_bcount = d_secsize` (2 MiB) exceeds the pbuf's `b_bufsize`
  (128 KiB) backing KVA. The oversized transfer reaches the device strategy
  routine. Whether this corrupts memory depends on the driver — most
  `dev_dstrategy` paths clamp `b_bcount` or use separate DMA mapping. This is a
  *potential* memory-corruption primitive, but only on the non-default
  `noinv`-class kernel, and only reachable by root/operator.

## Exploit chain / escalation

**None — root/operator-only path (valid hard blocker per Phase 6).**
`DIOCWDINFO32` requires `FWRITE` on a `root:operator 0640` device node
(`subr_diskslice.c:583`, `id maxx` ⇒ `groups=1001(maxx)` only). An
unprivileged user cannot cross the privilege boundary to reach this code. No
unprivileged path was found (`vnconfig`/`mdconfig`/devfs rules all gate on
root). Root→kernel is game-over by definition; this is a hardening gap, not a
privesc. No `exploit.c`/`chain.c` was authored because there is no
unprivileged victim to escalate against.

## PoC changes

- Created `poc_secsize.c` (the finding shipped no PoC source — only a scaffold
  reference). The PoC builds a valid `disklabel32` (correct `DISKMAGIC32`,
  `dkcksum32==0`, `RAW_PART.p_offset==0`, small partition sizes so
  `l32_setdisklabel` accepts it) with the hostile field `d_secsize = 0x200000`
  (2 MiB ≫ `MAXPHYS`). It uses the header-provided `dkcksum32()` static inline
  (not a redefinition) and `d_type=0` (the `DTYPE_*` enum is not exposed in the
  userland header).
- Uses a scratch `vn`-backed device (`/dev/vn0s0` on a 64 MiB zero image) so the
  boot disk is never at risk.

## Fix (`fix.diff` — supersedes the finding's proposal)

The finding's proposed fix places the check *after* `getpbuf_mem(NULL)`, which
would leak the pbuf on the error path (`relpbuf` at `:395` is skipped by an
early `return`). My fix places the validation **before** `getpbuf_mem`:

```c
if (lp->d_partitions[RAW_PART].p_offset != 0)
    return (EXDEV);			/* not quite right */

/* Validate d_secsize before allocating the pbuf ... */
if (lp->d_secsize < DEV_BSIZE || lp->d_secsize > MAXPHYS)
    return (EINVAL);

bp = getpbuf_mem(NULL);
KKASSERT((int)lp->d_secsize <= bp->b_bufsize);   /* now always true for user input */
```

- `DEV_BSIZE` (512) lower bound rejects `d_secsize == 0` (which would also
  wrap the loop-bound pointer arithmetic at `:360`).
- `MAXPHYS` (128 KiB) upper bound equals `bp->b_bufsize`, matching the KKASSERT
  invariant but as a real `return (EINVAL)`.
- Placed before `getpbuf_mem` → no pbuf leak.
- The KKASSERT is retained as a secondary invariant (now unreachable for
  user-supplied labels).

## Fix validation (Phase 8)

| kernel | version | PoC result | guest |
|--------|---------|------------|-------|
| unpatched baseline | `#0` Jul 2 06:02 | **panic** KKASSERT `d_secsize <= b_bufsize` at `:336` | down |
| single-fix | `#1` Jul 13 00:55 | **EINVAL** (errno 22), 3/3 runs | up |

`fix.diff` applies cleanly (`patch -p1` Hunk #1 succeeded), the single-fix
kernel compiles (`nativekernel rc=0`), boots as `#1`, and the identical PoC
that panicked the baseline now returns `EINVAL` with the guest staying up.

## Reproduce

```
./build.sh                 # cc -o poc_secsize poc_secsize.c
sudo ./run.sh              # sets up scratch /dev/vn0s0, runs the PoC
# unpatched: panic at subr_disklabel32.c:336 (check dfbsd-qemu/boot.log)
# patched:   "errno=22 (Invalid argument)", guest stays up
```
