# DF-0106 — VERDICT: NOT REPRODUCED (claimed panic); bug CONFIRMED in source

## One-line verdict

**NOT REPRODUCED at runtime** on the writedisklabel path: the missing
`d_npartitions` guard at `subr_disklabel32.c:363-364` is **confirmed real** in
the audited master DEV source and `dkcksum32` **provably executes** on the
path, but the claimed kernel **panic does not manifest** on this kernel because
the `getpbuf_mem` read buffer sits in a ~24 MiB contiguous wired region and the
~1 MiB OOB walk almost always stays mapped (returns `ESRCH`, no fault). The
**identical root-cause panic IS reproduced live** via sibling DF-0107
(`dkcksum32` from `l32_setdisklabel`).

## Source confirmation (the bug is real)

`l32_writedisklabel` reads the on-disk label into `bp->b_data` and scans for a
valid one (`subr_disklabel32.c:358-364`):

```c
for (dlp = (struct disklabel32 *)bp->b_data;
     dlp <= (struct disklabel32 *)((char *)bp->b_data + lp->d_secsize - sizeof(*dlp));
     dlp = (struct disklabel32 *)((char *)dlp + sizeof(long)))
{
    if (dlp->d_magic == DISKMAGIC32 &&
        dlp->d_magic2 == DISKMAGIC32 && dkcksum32(dlp) == 0) {   /* NO GUARD */
```

There is **no** `dlp->d_npartitions > MAXPARTITIONS32` check before
`dkcksum32(dlp)`, in contrast to the read path `l32_readdisklabel`
(`:225-226`) which guards correctly. The PoC plants a crafted sector at byte
512 with `d_magic = d_magic2 = DISKMAGIC32` and `d_npartitions = 0xFFFF`, so
at sector offset 0 both magics match and the `&&` short-circuit evaluates
`dkcksum32(dlp)`. That is the unbounded ~1 MiB walk (`disklabel32.h:157`:
`end = &lp->d_partitions[lp->d_npartitions]`).

## Why it does not panic on this kernel (the traced reason)

1. The read buffer comes from `getpbuf_mem(NULL)` (`subr_disklabel32.c:335`),
   whose `bp->b_data = bp->b_kvabase` (`sys/vm/vm_pager.c:271`).
2. Every `pbuf_mem` `b_kvabase` is `i * MAXPHYS + swapbkva_mem`
   (`vm_pager.c:263`), and `swapbkva_mem` is **one** contiguous
   `kmem_alloc_pageable` of `nswbuf_mem * MAXPHYS` = `190 * 128 KiB ≈ 24 MiB`
   (`vm_pager.c:235`; `nswbuf_mem = max(nbuf/32,8)` with `vfs.nbuf=6081` → 190;
   `MAXPHYS = 128 KiB`, `sys/cpu/x86_64/include/param.h:122`), backed by wired
   kernel pages (`vm_pager.c:277-288`).
3. A 1 MiB walk from `b_kvabase` faults **only if** `b_kvabase` is in the last
   ~1 MiB of that 24 MiB region (i.e. pbuf indices ≈ 184-189). The other ~184
   pbufs' walks stay fully inside mapped pages and return nonzero.
4. Those last-region pbufs sit at the **tails** of the 16 per-bucket LIFO free
   lists (`bswlist_mem[16]`, `getpbuf_mem` at `vm_pager.c:505-547`); reaching
   them would require ~184 pbufs checked out simultaneously. Only 4 `vn`
   devices exist (`/dev/vn0..vn3`; the `vn` driver does not auto-create
   `vn4+` via devfs), and each device serializes its `writedisklabel`, so the
   harness can hold at most ~4 `pbuf_mem` buffers at once — far short of what
   is needed to drain the free list to a faulting pbuf.

**Empirical:** 400+ iterations (tight sequential loops, 80-way concurrency
across all 4 `vn` devices, plus concurrent `dd` I/O churn to rotate the free
lists) produced **zero** panics; every invocation returned `ESRCH`
(`l32_writedisklabel:381`, the loop found no valid on-disk label because the
OOB-corrupted `dkcksum32` result is nonzero). The kernel config
(`sys/config/X86_64_GENERIC`) has `INVARIANTS` but **no** `SLAB_DEBUG`/
redzone, so there is no guard page around `getpbuf_mem` data to make the fault
deterministic.

This is the (d) outcome in the procedure — "genuinely not reachable for a
panic on this kernel" for the writedisklabel call site specifically, even
though the code bug is real and the OOB read executes. It is **not** a false
positive: the guard is genuinely missing at `:363-364`, confirmed against the
read-path guard at `:225-226`.

## Why DF-0107 (same root cause) DOES panic

`l32_setdisklabel` operates on the **ioctl** label buffer, a `kmalloc(404,
M_IOCTLOPS)` heap object (`sys/kern/sys_generic.c:674-676`). That buffer lands
in the general kernel malloc arena, where thread-stack guard pages are
interspersed, so the 1 MiB walk faults readily on fresh boots — reproduced 3×
(`panic: vm_fault: fault on stack guard ... l32_setdisklabel+0x57`). Same
`dkcksum32`, same unbounded `d_npartitions`, different (luckier-for-attacker)
buffer placement.

## Impact

The writedisklabel-path OOB read executes on every trigger (CWE-125), but on
this kernel it is non-faulting and its 16-bit XOR-folded result is internal
only — so there is **no observable runtime impact** (no panic, no extractable
leak) on the writedisklabel path. The realistic, demonstrated DoS for the
shared `dkcksum32` root cause is via DF-0107.

## PoC changes

Authored from scratch (the finding shipped with no PoC folder). First
iteration failed (`pwrite` of the 404-byte label returned `EINVAL` because
disk I/O must be sector-aligned); fixed by writing a full 512-byte sector with
the label in its first `sizeof(label)` bytes. The valid "new" label is
obtained from the kernel via `DIOCGDVIRGIN32` so the `DIOCSDINFO` sub-step of
`DIOCWDINFO` passes cleanly and execution reaches `l32_writedisklabel`.

## Recommended fix

Root-cause: clamp `dkcksum32`'s end pointer in `sys/sys/disklabel32.h:157`:
```c
end = (u_int16_t *)&lp->d_partitions[MIN(lp->d_npartitions, MAXPARTITIONS32)];
```
This single change closes DF-0106 **and** DF-0107 (and all other callers) at
the helper. Defense-in-depth: also mirror the read-path guard in
`l32_writedisklabel` (`subr_disklabel32.c:363-364`) with
`dlp->d_npartitions > MAXPARTITIONS32 ||`. The standalone `fix.diff`
implements the root-cause clamp (identical to DF-0107's `fix.diff`); it
**supersedes** the finding markdown's writedisklabel-only caller patch by
fixing the shared helper once. Verified with `git apply --check` (clean).
