# DF-0074 — VERDICT

## Verdict: **REPRODUCED** (kernel heap overflow -> panic) on master DEV

The `DIOCGSLICEINFO` heap-buffer overflow described in the finding is real,
present in the current DragonFlyBSD master DEV kernel, and was reproduced as a
kernel panic on every fresh-boot attempt. The overflow is mathematically
deterministic on every invocation; the resulting crash site is heap-layout
dependent (three distinct panic signatures observed across three fresh-boot
runs, all from the same 28 KB overrun).

**Guest tested:** `DragonFly dfbsd 6.5-DEVELOPMENT DragonFly
v6.5.0.1712.g89e6a-DEVELOPMENT #1: Mon Jun 29 14:18:01 UTC 2026
x86_64 / X86_64_GENERIC` (the audit's master DEV build).

## Root cause (confirmed line-by-line in `sys/`)

The `DIOCGSLICEINFO` handler copies the **live** kernel `struct diskslices`
into the ioctl `data` buffer using a length derived from the *actual* slice
count, but the destination buffer is only sized for `MAX_SLICES = 16` slots:

```c
/* sys/kern/subr_diskslice.c:556-559 */
case DIOCGSLICEINFO:
    bcopy(ssp, data, (char *)&ssp->dss_slices[ssp->dss_nslices] -
                     (char *)ssp);
    return (0);
```

- **Destination size:** `DIOCGSLICEINFO` is `_IOR('d', 111, struct diskslices)`
  (`sys/sys/diskslice.h:96`). `mapped_ioctl` allocates the `data` buffer with
  `size = IOCPARM_LEN(com)` = `sizeof(struct diskslices)` =
  `offsetof(dss_slices) + MAX_SLICES * sizeof(struct diskslice)` =
  `32 + 16 * 256 = 4128` bytes (`sys/kern/sys_generic.c:668,675`).
- **Source size for a GPT disk:** `subr_diskgpt.c:175` calls
  `dsmakeslicestruct(BASE_SLICE + MAX_GPT_ENTRIES, info)` = 130 slots
  (`BASE_SLICE = 2`, `MAX_GPT_ENTRIES = 128`), and `subr_diskgpt.c:222` sets
  `ssp->dss_nslices = BASE_SLICE + i` (= 130 for any GPT whose header
  `entries >= 128`). `dsmakeslicestruct` (`subr_diskslice.c:720-723`)
  `kmalloc`s the full 130-slot object, so the **source** is in-bounds — only
  the **destination** overflows.
- **The overflow:** the `bcopy` length becomes
  `offsetof(dss_slices) + dss_nslices * sizeof(struct diskslice)` =
  `32 + 130 * 256 = 33312` bytes, written into the 4128-byte `data` buffer →
  **29184 bytes (~28 KB) of overrun** past the end of an `M_IOCTLOPS` slab
  object, on **every** call. The overrun bytes are attacker-influenced
  (`ds_offset`, `ds_size`, `ds_type_uuid`, `ds_stor_uuid` come straight from
  the crafted GPT entries — `subr_diskgpt.c:237+`).
- The trailing `copyout` (`sys_generic.c:730`) only copies the declared
  `sizeof(struct diskslices)` back, so the corruption is confined to kernel
  heap (the user merely observes `dss_nslices = 130`).

Struct sizes were verified on the guest with a small probe:
`sizeof(struct diskslice) = 256`, `sizeof(struct diskslices) = 4128`,
`offsetof(dss_slices) = 32`, overflow = `(130-16)*256 = 29184` (~28 KB).

## Trigger & evidence

A crafted 1 MiB GPT image (`build_gpt.py`, header `entries = 128`, 17 non-nil
entries) is attached with `vnconfig -c vn0 overflow.img`. The kernel
auto-probes the GPT and creates `/dev/vn0s0`..`/dev/vn0s16` (already proving
`dss_nslices > 16`). Issuing `DIOCGSLICEINFO` on `/dev/vn0s1` returns
`nslices=130` — proof the oversized `bcopy` executed — and writes 28 KB past
the `data` buffer into adjacent kernel heap.

Three fresh-boot runs (via `vm.sh reset -> clean-install`) all panicked, each
at a different downstream site of the same slab corruption (see `panic.txt`):

| Run | Panic site | Mechanism |
|-----|------------|-----------|
| 1 | `slab_cleanup+0x1c9` (NULL deref, trap 12, idle) | async slab reclaimer walked corrupted zone metadata |
| 2 | `panic: slaballoc: corrupted zone` in `_kmalloc <- fork1 <- sys_fork` | slab allocator's own KKASSERT caught the corrupted zone during a flood-induced fork |
| 3 | `panic: assertion "parent->error == 0" in hammer2_chain_create` preceded by `dscheck(vbd0s1d): slice too large` | the 28 KB overrun corrupted the **root** disk's slice metadata in heap, breaking hammer2 |

Run 3 is especially telling: the overflow corrupted slab objects describing
the **unrelated root disk** (`vbd0s1d`), demonstrating the overrun reaches
arbitrary adjacent kernel state — not just the crafted `vn0` disk. The
`panic: slaballoc: corrupted zone` (Run 2) is the most direct signature: the
slab allocator's zone-consistency assertion fired because of the overflow.

The crash is **asynchronous and probabilistic in exact site/timing** (the
overrun corrupts whatever slab object happens to be adjacent at runtime), but
the underlying 28 KB heap overflow is **100% deterministic** on every call —
`nslices=130` is returned on every single invocation, including the ~15 runs
that did not immediately panic.

## Reachability / privilege (severity calibration)

- **Default devfs:** slice device nodes (`/dev/vn0*`, `/dev/md0*`, `/dev/serno/*/s*`,
  `/dev/vbd0*`) are `root:operator crw-r-----`, and the `operator` group
  contains only `root` (`/etc/group`: `operator:*:5:root`). An unprivileged
  user (`maxx`, uid 1001) gets `open: Permission denied` (EACCES) — confirmed
  on the guest. So on a default single-user/workstation box the bug is
  **root-only** to trigger directly.
- **However**, the same defect is reachable by any principal that can open a
  slice device and issue the ioctl: any user in the `operator` group, any
  devfs ruleset that exposes slice devices to a group/class (common on
  multi-user servers, build/jail hosts, disk-inspection utilities,
  container/VM hosts that pass through storage), and any disk-probing
  utility that runs with such access. Presenting the crafted image via USB
  mass storage reaches the auto-probe path with no ioctl needed from the
  attacker for the `dss_nslices` inflation; only the `DIOCGSLICEINFO` (or any
  consumer of the raw-struct ioctl) needs to fire.
- **Primitive:** a deterministic ~28 KB attacker-influenced kernel-heap write
  (CWE-122). Reliable kernel-panic DoS is trivially demonstrated. With slab
  grooming (spray victim objects carrying function pointers / `ucred *` /
  refcounts into the `M_IOCTLOPS` neighborhood adjacent to the 4128-byte
  allocation, punch a hole, trigger the overflow into it), the corruption is
  convertible to kernel-mode code execution / local privilege escalation. The
  overflow size (28 KB) and the attacker-controlled content make this a
  strong primitive; full LPE was not developed in this verification pass
  (time-bounded), but the path is open.

## PoC changes made during verification

1. **`trigger_stress.c` (new):** the original `trigger.c` only issues a single
   `DIOCGSLICEINFO`. On master DEV the single-shot overflow did not
   *synchronously* panic (the corrupted neighbor wasn't exercised in-band).
   `trigger_stress.c` issues the ioctl, then churns the slab allocator
   (open/close 64 fds, fork/exit 40 children) and a 16-process parallel flood
   to force a corrupted neighbor to be allocated/freed/validated, reliably
   surfacing the panic within a run. The original `trigger.c` is retained as
   the minimal proof (it returns `nslices=130`, proving the overflow).
2. **`build.sh` / `run.sh` (new):** self-contained, runnable reproduce scripts.
   `run.sh` documents that it must run as root (or operator) and that the
   panic is asynchronous (proof in `dfbsd-qemu/boot.log`).
3. **Image build clarified:** `python3` is **not** installed on the DragonFly
   guest, so `build_gpt.py` runs on a host with python3 and the resulting
   `overflow.img` is shipped to the guest. `build.sh` warns if the image is
   missing.
4. **Device naming:** DragonFly uses `/dev/vn0s1` + `vnconfig -c vn0` (the
   original PoC text referenced NetBSD-style `/dev/vnd0s1` / `vnd0` /
   `mdconfig`, none of which apply here — `mdconfig` is absent on this guest).

No attack-logic changes were needed; `build_gpt.py` produced a valid GPT
(header `entries=128`, correct CRCs) on the first host-side run and the kernel
probed it correctly.

## Recommended fix

`fix.diff` (validates with both `git apply --check` and `patch --dry-run`):
cap the `bcopy` length at `MAX_SLICES` so it never exceeds the destination
buffer size. This **matches** the finding markdown's `## Recommended fix`
proposal (the same one-line clamp), with an added explanatory comment and a
local `u_int ncap` to keep the `bcopy` expression readable. The deeper fix
(replacing the raw-struct ioctl with a structured, pointer-free output — see
DF-0075) remains desirable but is out of scope for this minimal bounds fix.
