# DF-0940 — vm_map_growstack `int grow_amount` truncation — VERDICT

## Verdict: NOT REPRODUCED (false positive for the claimed threat model)

The C-level truncation defect is **real**: at `sys/vm/vm_map.c:4110` an `int
grow_amount;` receives the 64-bit result of `roundup(stack_entry->ba.start -
addr, PAGE_SIZE)` at `:4173`, silently dropping the high 32 bits. **However**,
the finding's claimed impact (unprivileged local DoS via mmap of an >4 GiB
`MAP_STACK`) **does not manifest**, because the only path that ever fed a
user-controlled `max_ssize` into `vm_map_stack()` — and thus the only way to
produce `aux.avail_ssize > 2^32` — is **dead code**. The vulnerability is
unreachable from an unprivileged user on the default GENERIC kernel.

## Why the PoC does not trigger it (path:line)

### (1) Userland `MAP_STACK` is stripped before `vm_map_stack` runs

`sys/vm/vm_mmap.c:425-439` (inside the `mmap` syscall) explicitly removes
`MAP_STACK` and substitutes `MAP_ANON` for every userland caller, with a
comment naming the intent:

```c
/* The only remaining true MAP_STACK we allow is the user stack as
 * created by the exec code.  All userland MAP_STACK's are converted
 * to normal mmap()s right here.  */
if (flags & MAP_STACK) {
    if (uap->fd != -1)
        return (EINVAL);
    if ((uap->prot & (PROT_READ|PROT_WRITE)) !=
        (PROT_READ|PROT_WRITE)) {
        return (EINVAL);
    }
    flags &= ~MAP_STACK;
    flags |= MAP_ANON;
    upos = 0;
}
```

Therefore the `vm_map_stack()` call at `sys/vm/vm_mmap.c:1463` is **never
reached** with user-controlled `max_ssize` — by the time control reaches
`kern_mmap` at `vm_mmap.c:1248+`, `MAP_STACK` has been stripped and the
mapping is dispatched as a normal anon `vm_map_find` at `vm_mmap.c:1478`.

Empirical confirmation: the PoC's `mmap(NULL, 8 GiB, ...,
MAP_STACK|MAP_ANON, -1, 0)` returns a region whose `/proc/curproc/map`
shows a single normal anon entry — `0x0000000800a00000 0x0000000a00a00000
-1 -1 0 rw- 0 0 0x0000 NCOW NNC none -` (no `VM_SUBSYS_STACK`, no
`aux.avail_ssize`, no growable semantics). See `run.log` / `map_dump.txt`.

### (2) The only live `vm_map_stack` caller uses kernel-controlled `max_ssize`

`sys/kern/kern_exec.c:991` is the sole live caller, invoked during exec to
create the main process stack:

```c
error = vm_map_stack(&vmspace->vm_map, &stack_addr, (vm_size_t)maxssiz, ...);
```

`maxssiz` is a boot-time tunable, **read-only** at runtime:

- Declared `u_quad_t maxssiz;` (`sys/kern/subr_param.c:95`).
- Default `MAXSSIZ = 512 MiB` on pc64
  (`sys/platform/pc64/include/vmparam.h:69`).
- `SYSCTL_QUAD(_kern, OID_AUTO, maxssiz, CTLFLAG_RD, ...)` at
  `subr_param.c:102` — **`CTLFLAG_RD`**, not writable.
- Only set at boot via `TUNABLE_QUAD_FETCH("kern.maxssiz", ...)` at
  `subr_param.c:226`.

Verified on the guest: `sysctl kern.maxssiz` ⇒ `536870912` (512 MiB).

### (3) `avail_ssize` is therefore always < INT_MAX

For the main stack, `aux.avail_ssize = max_ssize - init_ssize = maxssiz -
sgrowsiz = 512 MiB − 128 KiB = 536821760` (well below `INT_MAX = 2^31 − 1 ≈
2 GiB`). Any `addr` inside the growable stack region yields
`ba.start - addr ≤ avail_ssize < 2^31`, so `roundup(ba.start - addr, PAGE_SIZE)`
always fits in `int` — **the truncation at `:4173` never fires for any
realistic fault**. The bounds check at `:4166-4170` already excludes any
`addr` outside `[ba.start - avail_ssize, ba.start)`, so a wild pointer can't
sneak a large distance in either.

### (4) `RLIMIT_STACK` cannot rescue it

`sys/kern/kern_plimit.c:324-327` clamps `RLIMIT_STACK` to `maxssiz`:

```c
if (limp->rlim_cur > maxssiz) limp->rlim_cur = maxssiz;
if (limp->rlim_max > maxssiz) limp->rlim_max = maxssiz;
```

And in any case `RLIMIT_STACK` only governs the runtime check at
`vm_map.c:4206-4210` (and the cap at `:4217-4220`); it does not influence
`aux.avail_ssize`, which is fixed at exec time.

## What the original PoC actually does

The finding's PoC (`trig.c` as delivered) mmaps 8 GiB `MAP_STACK|MAP_ANON`,
writes the lowest byte, and reads an address ~5 GiB "below" the top.
Because `MAP_STACK` is stripped, the entire 8 GiB is a normal anon mapping;
every access is satisfied by the anon pager. The deep read returns `0x00`
(zero-filled page). The process exits 0. `vm_map_growstack` is never called
(no `VM_SUBSYS_STACK` entry exists). No panic, no memory growth, no DoS.

Verified over 3 consecutive runs (`run.log`, `run.2.log`, `run.3.log`); guest
stayed `up` after each.

A second test (`deeprec.c`) exercises the **real** growstack path via deep
recursion on the main stack — it SIGSEGVs cleanly at the `maxssiz` boundary,
kernel unaffected. This is the expected behavior of a correctly-functioning
bounded stack.

## Impact assessment

- **Claimed impact:** Local DoS — unbounded kernel memory growth / panic from
  `mapentzone` exhaustion via `RetryFault` loop.
- **Actual impact: none.** The triggering precondition
  (`MAP_STACK` mmap with `size > 2^32`) is unreachable from userspace; and
  even were it reachable (e.g. by an admin tuning `kern.maxssiz` above 4 GiB
  in `/boot/loader.conf`), the analysis shows the typical failure mode is a
  single wrong-size fragment followed by `SIGSEGV`, **not** an unbounded loop
  — because once `D` (= `ba.start - addr`) drops below `2^32` after one
  truncated growth, the next truncated value becomes negative-as-int, which
  compares (after promotion to `u64`) greater than `avail_ssize` and bails
  at `:4174`. The finding's "RetryFault loops indefinitely" premise is
  incorrect; `vm_map_insert` at `:1275-1277` returns `KERN_INVALID_ADDRESS`
  for zero-size inserts, breaking the loop.

This is a **reviewer false positive on threat-model / reachability grounds**,
not a "the code is fine" false positive — the type bug is real, just latent.

## Defense-in-depth fix

Although the bug is unreachable from userspace today, the `int grow_amount`
declaration at `vm_map.c:4110` is genuinely wrong and would become exploitable
if userland `MAP_STACK` were ever re-enabled (the dead code at
`vm_mmap.c:1463` would resurrect) or if an admin tuned `maxssiz` above 2 GiB.
The one-line type widening in `fix.diff` (`int grow_amount;` →
`vm_size_t grow_amount;`) closes the latent defect with no semantic change —
all subsequent comparisons (`:4174`, `:4189`, `:4214`, `:4223`) are already
against unsigned 64-bit lvalues. This **matches** the finding markdown's
`## Recommended fix` proposal.

## Fix validation

Per Phase 8: `fix.diff` `git apply --check -p1` passes; the single-fix kernel
built cleanly (`make -j6 nativekernel KERNCONF=X86_64_GENERIC` →
`=== NK_DONE rc=0 ===`, 4299 cc invocations, zero errors); the patched
`#1` kernel boots and the trigger still exits 0 with no effect (`fix_run.log`).

`fix_status = not_testable` — because the bug is unreachable, there is no
"bad behavior" to reproduce on the baseline that could then be shown absent
on the patched kernel. The fix is validated to (a) apply, (b) compile, (c)
not regress the guest; the latent code path it closes was confirmed by
source tracing above.

## PoC changes from the as-delivered version

- The original `trig.c` touched `*p` (lowest byte) and `*(p + 3 GiB)` and
  relied on the (incorrect) premise that this would loop in `RetryFault`.
  The rewritten `trig.c` additionally dumps `/proc/curproc/map` to make the
  false positive self-evident (the 8 GiB region is one normal anon entry,
  not a growable stack), and prints explicit "no growstack" markers at each
  access. The original semantics (deep access on an 8 GiB `MAP_STACK`) are
  preserved.
