# DF-0940 — vm_map_growstack `int grow_amount` truncation

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

The `int grow_amount` truncation described in the finding is **real in the C
source** (a 64-bit `roundup()` result is stored into a 32-bit `int`), but it is
**not reachable from an unprivileged user** under the default kernel
configuration, so the claimed local-DoS / kernel-memory-exhaustion impact does
not manifest.

## Why it does not reproduce (path:line evidence)

The finding's threat model assumes a user can create a `MAP_STACK` mapping with
an attacker-controlled `max_ssize > 2^32`. That assumption is wrong:

1. **Userland `MAP_STACK` is stripped.** `sys/vm/vm_mmap.c:429-436` (inside the
   `mmap` syscall path) explicitly removes `MAP_STACK` and substitutes
   `MAP_ANON` for every userland caller:

   ```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) {
       ...
       flags &= ~MAP_STACK;
       flags |= MAP_ANON;
       upos = 0;
   }
   ```

   This is verified empirically: `mmap(NULL, 8 GiB, ..., MAP_STACK|MAP_ANON, -1, 0)`
   succeeds and returns a region whose `/proc/curproc/map` shows two **normal**
   anon entries (`0x2180` / `0x0` subsys, no growable-stack semantics) — see
   `map_dump.txt`. The vulnerable `vm_map_growstack` path is never entered for
   this mapping.

2. **The only live caller of `vm_map_stack()` uses a kernel-controlled
   `max_ssize`.** `sys/kern/kern_exec.c:991` invokes it for the main process
   stack with `max_ssize = (vm_size_t)maxssiz`. `maxssiz` is a boot-time
   tunable, default 512 MiB on pc64
   (`sys/platform/pc64/include/vmparam.h:69` + `subr_param.c:225`), and is
   exported read-only (`CTLFLAG_RD`, `subr_param.c:102`):

   ```
   $ sysctl kern.maxssiz
   kern.maxssiz: 536870912          # 512 MiB
   ```

3. **`avail_ssize` is therefore always ≤ 512 MiB − 128 KiB**, far below
   `INT_MAX` (2 GiB). For any fault on the main stack, the 64-bit
   `roundup(stack_entry->ba.start - addr, PAGE_SIZE)` always fits in `int`, so
   the truncation at `vm_map.c:4173` never fires. The dead user-mmap caller at
   `vm_mmap.c:1463` is the only path that ever passed a user-controlled
   `max_ssize` to `vm_map_stack`, and it is unreachable.

4. **RLIMIT_STACK cannot rescue it.** `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 affects the runtime check at
   `vm_map.c:4206-4210`, not the `avail_ssize` set at exec time.

## What the original PoC actually does

`trig.c` mmaps 8 GiB `MAP_STACK|MAP_ANON`, then dereferences deep pointers.
Because `MAP_STACK` is stripped, the region is a normal anon mapping — the deep
read just returns `0x00` from a zero-filled anon page, the process exits 0,
and the kernel is unaffected. Verified over 3 consecutive runs (see `run.log`,
`run.2.log`, `run.3.log`); guest stayed `up` after each.

A separate deep-recursion test on the **main** stack (`deeprec.c`) confirms the
real growstack path is exercised on the main stack and behaves correctly —
SIGSEGV cleanly at the `maxssiz` boundary, kernel healthy.

## Defense-in-depth fix

The `int grow_amount` declaration at `vm_map.c:4110` is genuinely wrong — it
truncates a 64-bit value. The truncation is currently latent (unreachable from
userspace), but it is a real code defect that would resurface if userland
`MAP_STACK` were ever re-enabled or if `maxssiz` were tuned above 2 GiB. The
fix is a one-line type widening: `int grow_amount;` → `vm_size_t grow_amount;`.
All subsequent comparisons are already against unsigned 64-bit lvalues, so the
widening is semantics-preserving. See `fix.diff`.

## Build & run

```
./build.sh     # cc -O0 -o trig trig.c
./run.sh       # ./trig   (exits 0, no effect — demonstrates the FP)
```
