# DF-5010 — vkernel vdisk (`vkd`) lacks driver-level I/O bounds checking

**File:** `sys/dev/virtual/vkernel/disk/vdisk.c`
**Class:** CWE-787/CWE-125 (latent), defense-in-depth
**Severity:** Info (hardening) — NOT exploitable on any currently reachable path
**Verification:** built a full vkernel64 inside the QEMU DF guest, booted it with a
COW disk (`-R`), and fired 512-aligned raw OOB reads/writes at `/dev/vkd1` and
`/dev/vkd2` as guest root (PID 1). All were rejected by the generic slice layer
(`dscheck()`) before reaching `vkd_doio()`; the vkernel host process survived
every attempt and the backing files never grew.

## What the trigger does

`vkd_bounds.c` runs as PID 1 inside the vkernel (via `-e init_path=/poc`):

* mounts devfs,
* `pwrite`/`pread` 512-byte blocks at: offset 0 (in bounds), exact EOF,
  `size+512`, and `1 TB` on both the plain (`-r`) and COW (`-R`) disks.

Expected (and observed) on a stock kernel:

* in-bounds I/O succeeds;
* OOB I/O returns `-1`/`EINVAL` (silent `dscheck` bounds rejection,
  `sys/kern/subr_diskslice.c:253→294`);
* the vkernel process survives; `plain.img` never grows;
* when PID 1 exits the vkernel panics with
  `panic: Going nowhere without my init!` — expected and harmless.

`vkd_oob.c` is the earlier unaligned variant (4-byte writes); it demonstrates
the *alignment* rejection (`dscheck(vkdN): b_bcount 4 is not on a sector
boundary`, see `run.log`).

## How to reproduce (inside the DF QEMU guest, as root)

```sh
# 1) build the vkernel (see build.sh; note: vkernel64 link is currently
#    broken in HEAD — kern_mib.c needs __build_id_start/end which the
#    vkernel conf/Makefile SYSTEM_LD override never defines; work around
#    with -Wl,--defsym, see build.sh)
# 2) build a UFS root image containing the static PoC at /poc (build.sh)
# 3) run:
sysctl vm.vkernel_enable=1
cd /root/vk
timeout 90 ./vkernel64 -m 256M -r root.img -r plain.img -R cow.img \
    -e init_path=/poc < /dev/null > run3.log 2>&1
# 4) observe: "vkd2: COW disk", "init died (signal 0, exit 0)", no kernel
#    fault, plain.img still 16777216 bytes.
```

## Bottom line

* `vkd_doio()` (vdisk.c:303-347) performs `bcopy(sc->map_buf + bio_offset, …)`
  (COW mode) and `pread/pwrite()` (regular mode) with **no validation of
  `bio_offset`/`b_bcount` against `sc->size`**.
* Every currently reachable path into `vkdstrategy()` is bounds-checked by
  `dscheck()` first — including the whole-disk nodes, because `disk_create()`
  exposes only the disk-core (`disk1_ops`/`disk2_ops`) covering devices in
  devfs (`sys/kern/subr_disk.c:691-712`).
* The in-tree precedent for *bypassing* dscheck exists: whole-disk raw-access
  partitions ≥128 (`sys/kern/subr_diskslice.c:171-175`, created e.g. by
  `fd.c:1036-1054`) are passed to the driver **unbounded**. If vkd ever grows
  such nodes (or any bio source skips dscheck), COW mode turns into an
  arbitrary-offset OOB read/write inside the vkernel host process.
* The fix (see `fix.diff`) adds the driver-level check and also clamps the
  `b_resid` computation for `pread/pwrite == -1` (error) returns, which today
  computes `resid = bcount + 1`; harmless on x86-64 because
  `std_copyout`'s address-wrap guard (`sys/platform/pc64/x86_64/support.s:247-249`)
  turns the resulting `(size_t)-1` copyout length into an immediate `EFAULT`.
