# DF-2746 — VERDICT

**File:** `sys/vm/vm_mmap.c` (sys_mincore) · **Severity:** Low · **Class:** CWE-787 / CWE-193 (off-by-one OOB write; userspace-directed) + index truncation

## Reproduced?

**YES — reproduced, unprivileged, stable across 3 runs** on the stock
INVARIANTS kernel (`DragonFly dfbsd 6.5-DEVELOPMENT #0 ... X86_64_GENERIC`,
guest `uname -a` in `env.txt`). `run.log` contains the decisive output:

```
test1 (all-unmapped range, tail-fill loop):
  vec[-1] = 0x00  (must still be 0x5A)     <- kernel wrote NUL one byte BELOW vec
  => VULNERABLE: kernel wrote NUL at vec[-1]
test2 (leading hole, gap-fill loop):
  vec[-1] = 0x00  (must still be 0x5A)
  => VULNERABLE: kernel wrote NUL at vec[-1]
RESULT: VULNERABLE (2)
```

Both trigger shapes hit: (1) a fully unmapped range reaches the tail-fill
loop `vm_mmap.c:961-968`; (2) a range whose leading page is a hole reaches
the gap-fill loop `vm_mmap.c:920-927` via `vm_map_lookup_entry()` failing
so iteration starts at `RB_MIN` (`vm_mmap.c:821-822`) and the first
reported page has `vecindex >= 1`. In each loop the very first iteration
executes `subyte(vec + lastvecindex, 0)` with `lastvecindex == -1`
(`vm_mmap.c:829`) ⇒ one NUL byte stored at `vec[-1]`.

## Exploit chain / impact ceiling

Rigorously bounded — **no privilege escalation path exists**:

* The write value is always `0` (gap/tail fill); the per-page write uses a
  `MINCORE_*` nibble (`vm_mmap.c:932`).
* The target is always `vec + k` for the caller-chosen `vec`; `std_subyte`
  refuses addresses ≥ `VM_MAX_USER_ADDRESS`
  (`sys/platform/pc64/x86_64/support.s:632-634`), so a kernel-range `vec`
  or wrapped index returns EFAULT — **no kernel-memory write primitive**.
* Everything lands in the *calling process's own* address space, which the
  caller can already write arbitrarily. Residual harm: silent single-byte
  corruption of whatever the application placed immediately before the
  vector (e.g. a heap chunk header/length prefix) ⇒ in-process corruption
  the app cannot anticipate; POSIX mincore(2) promises to touch only
  `vec[0..ceil(len/PAGE_SIZE))`.
* Secondary manifestation: `vecindex`/`lastvecindex` are `int`
  (`vm_mmap.c:791`) while `OFF_TO_IDX()` yields `vm_pindex_t`; ranges
  ≥ 2^31 pages (≥ 8 TiB, allowed — user space is 128 TiB) truncate the
  index so fills/writes land ±2 GiB from `vec` (still user-space only).

Impact: `none` for privilege; the honest classification is a kernel
correctness/contract bug with in-process corruption — filed Low.

## Root cause

`sys/vm/vm_mmap.c:920-927` and `:961-968`: loop body writes
`vec + lastvecindex` but increments after the write; with the `-1`
sentinel (`:829`) the first store belongs at `vec + lastvecindex + 1`.

## Fix (validated)

`fix.diff` — write `subyte(vec + lastvecindex + 1, 0)` in both loops and
widen `vecindex`/`lastvecindex` to `vm_pindex_t` (sentinel becomes
`(vm_pindex_t)-1`; `lastvecindex + 1` still wraps to 0).

### Fix validation

* Baseline (stock kernel): `RESULT: VULNERABLE (2)`, exit 0 (see `run.log`).
* `vm.sh reset with-src`, `patch -p1` applied to guest `/usr/src` (clean,
  4/4 hunks), `make -j4 nativekernel KERNCONF=X86_64_GENERIC`, installkernel,
  reboot.
* Patched kernel: `RESULT: not vulnerable`, exit 2 — sentinels intact
  `0x5A`, `vec[0]/vec[1]` still correctly reported 0x00 / 0x1f (see
  `run.fixed.log`). Behavior otherwise identical (mincore still returns 0).

## References

* `sys/vm/vm_mmap.c:791` (int indices), `:829` (-1 sentinel), `:920-927`
  (gap-fill), `:932` (page write), `:961-968` (tail-fill)
* `sys/platform/pc64/x86_64/support.s:625-644` (`std_subyte` user-range check)
