# DF-2952 VERDICT — resident-exec stale-stack disclosure

**Status: reproduced (leak). Confidence: certain. Fix validated on guest
(fixed).**

## Bottom line

On the stock audit guest (DragonFly 6.5-DEVELOPMENT #0, X86_64_GENERIC,
INVARIANTS), any unprivileged local user who execs a binary registered with
`resident(8)` inherits a COW fork of the **registration-time stack of the
root process** that performed the registration, and can read it by simply
scanning below/above its own stack pointer. Root-environment data placed at
registration time (and, in general, rtld/libc init frames of the root
registration run) is disclosed across the privilege boundary.

Reproduced 5/5 iterations with ~7600 marker hits per exec (7603/7615/7601/
7616/7580), plus a fresh post-reset confirmation (7603 hits) and an early
small-payload variant (~600 B of markers → 11 hits ≈ 300 B leaked). The
control (after `resident -R` unregister, same binary, same unprivileged
user) shows 0 hits.

## Root cause (path:line)

1. `sys_exec_sys_register` snapshots the caller's *entire* vmspace,
   stack included, COW: sys/kern/imgact_resident.c:236
   (`vmspace_fork(p->p_vmspace, NULL, NULL)`).
   The caller must hold SYSCAP_NOVM_RESIDENT (root). With the stock flow
   (`/usr/sbin/resident <prog>`, usr.sbin/resident/resident.c:148-154 →
   libexec/rtld-elf/rtld.c:675-684) the snapshot is taken from inside
   rtld of the **root** invocation, with root's argv/env strings sitting
   on the stack.

2. On each later exec, `exec_new_vmspace(imgp, vmcopy)` forks the
   snapshot again (kern_exec.c:956-960 → vm_map.c:4298-4331
   `vmspace_exec` → `vmspace_fork`), which COW-copies **all**
   VM_INHERIT_COPY entries — including the stack entry
   (vm_map.c:3725-3744, 3865-3890). The `pmap_remove_pages()` at
   kern_exec.c:959 operates on the brand-new fork's *empty* pmap and
   removes nothing.

3. `vm_map_stack(&vmspace->vm_map, USRSTACK-maxssiz, maxssiz, 0, ...)`
   (kern_exec.c:991) then tries to lay a fresh stack at the standard
   location — but on pc64 `USRSTACK == VM_MAX_USER_ADDRESS`
   (sys/platform/pc64/include/vmparam.h:142) and the forked snapshot's
   stack entry already occupies the top of the address space, so
   `vm_map_findspace` cannot fit `maxssiz` (vm_map.c:1546 `end >
   vm_map_max(map)`) and `vm_map_stack` returns `KERN_NO_SPACE`
   (vm_map.c:4001-4046) on **every** resident exec. The intended
   "remap the stack" (kern_exec.c:949-954 comment) has never actually
   happened.

4. `exec_resident_imgact` **discards** `exec_new_vmspace`'s return value
   (sys/kern/imgact_resident.c:185) and returns 0, so kern_exec marches
   on: `exec_copyout_strings` writes the new argv/env onto the stale COW
   stack, and the process runs with the registration-time stack contents
   intact everywhere the new exec didn't overwrite.

Note: `imgact_elf.c:706` also ignores the return, but the ELF path's map
is empty at that point, so `vm_map_stack` there succeeds; only the
resident path is broken.

## Reproduction

- `victim.c` — trivial dynamic binary; with argv[1]=="leakcheck" it scans
  its own stack ([SP-256K, USRSTACK-64]) for `DF2952MARKER` with a
  SIGSEGV-guarded byte scan; exit 42 + `RESULT: LEAK` on hit.
- Register as root with ~140 KB of unique `DF2952MARKER_Sxx_…` env data:
  `resident -f /tmp/df2952/victim` (rtld registers and exits before main).
- Leak check: `su -m nobody -c "env -i /tmp/df2952/victim leakcheck"`
  → uid 65534, empty env, marker hits printed with addresses
  (run.log / run.2.log / iter.log).

The leaked bytes cannot come from the exec'ing user: `env -i` gives it an
empty environment, and the same exec after unregistering shows 0 hits.

## Fix validation

`fix.diff` (authored after verification, applied only to the guest's
/usr/src copy):

- kern_exec.c: `vm_map_remove(map, stack_addr, USRSTACK)` in the resident
  branch before `vm_map_stack()` — destroys the forked snapshot's stack
  mappings so the fresh stack is actually created at the standard
  location;
- imgact_resident.c: propagate `exec_new_vmspace()` failure (fail-safe:
  kern_exec's post-point-of-no-return error handling exits the process).

Guest rebuild: `make -j6 nativekernel KERNCONF=X86_64_GENERIC` rc=0,
-Werror clean (fix_build.log), `make installkernel`, reboot into
`#1 Fri Sep  4 01:03:31 UTC 2026`.

Patched-kernel result (fix_run.log): registration works (`resident -l`
lists it), the resident exec works (binary runs, argv parsed, exit 0),
and the same PoC shows **0 hits in 3/3 iterations** (plus one extra
confirmation). Baseline vs patched: 5/5 × ~7600 hits → 3/3 × 0 hits.

Guest was reset to the `with-src` snapshot after validation (stock kernel
#0, pristine /usr/src, md5s match the repo).

## Severity rationale

Medium: cross-privilege information disclosure (privileged process's
stack contents → any unprivileged user who execs a registered binary).
Precondition is an administrator-registered resident binary — exactly the
feature's intended deployment; registration is root-only
(SYSCAP_NOVM_RESIDENT). Not a memory-safety bug: the disclosed pages are
valid COW user pages; no corruption observed (INVARIANTS kernel stayed
healthy through all runs).
