Resident-exec stack remap silently fails (vm_map_stack KERN_NO_SPACE swallowed at imgact_resident.c:185): every exec of a registered binary runs on the registration snapshot's COW stack, disclosing the registering root process's stack contents to unprivileged exec'ers
| Field | Value |
|---|---|
| ID | DF-2952 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N |
| CWE | CWE-200 (secondary CWE-252) |
| File | sys/kern/imgact_resident.c |
| Lines | 185, 236 (mechanism: kern_exec.c:956-997; vm_map.c:4001-4046) |
| Area | kern/exec |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | kernleak |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
exec_resident_imgact() discards exec_new_vmspace()'s return value. On the resident path exec_new_vmspace() forks the registration snapshot β whose vm_map already contains the registering process's COW stack entry at [USRSTACK-maxssiz, USRSTACK) β and then vm_map_stack() for the same range fails with KERN_NO_SPACE on every resident exec, because on pc64 USRSTACK == VM_MAX_USER_ADDRESS leaves no free space below the snapshot's stack entry. The pmap_remove_pages() hits the fresh fork's empty pmap and removes nothing. The error is invisible: the exec proceeds on the stale snapshot stack. Everything below the freshly-copied argv/env β the registration-time stack of the registering process β remains readable by whoever execs the binary. Registration requires root (SYSCAP_NOVM_RESIDENT) and the stock flow (/usr/sbin/resident β rtld exec_sys_register) snapshots a root invocation, so root's environment strings, rtld/libc init frames and auxv region are disclosed to any unprivileged user who execs a registered binary β the feature's intended deployment. Not memory corruption (INVARIANTS kernel stayed healthy through all runs).
Proof of contest
VERIFIED (findings/poc/DF-2952/): root registers victim with ~140 KB
of unique environment markers; su -m nobody execs it with an empty
env β 7603/7615/7601/7616/7580 marker hits with addresses over 5
iterations (~140 KB of root-env data; ~300 B even with a small root
env); control after unregistering: 0 hits. Fix validated in-guest
(vm_map_remove the snapshot's stack before the fresh stack + propagate
exec_new_vmspace failures): 0 hits 3/3, feature still functional.
Recommended fix
Validated fix.diff in findings/poc/DF-2952/.
Timeline
- 2026-09-02 Discovered during pass-2 audit of imgact_resident.c (GLM 5.3); cross-privilege disclosure reproduced 5/5 + fix validated.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2952 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| victim.c | β | 2.1 KB | view raw | |
| run_poc.sh | β | 3.4 KB | view raw | |
| iter.sh | β | 1.5 KB | view raw | |
| build.sh | β | 156 B | view raw | |
| run.sh | β | 228 B | view raw | |
| build.log | β | 196 B | view raw | |
| run.log | β | 623.3 KB | β download | |
| run.2.log | β | 3.0 MB | β download | |
| iter.log | β | 3.0 MB | β download | |
| run.oneshot.log | β | 1.2 KB | view raw | |
| fix_run.log | β | 1.5 KB | view raw | |
| fix_build.log | β | 5.6 MB | β download | |
| fix.diff | β | 4.0 KB | view raw | |
| env.txt | β | 512 B | view raw | |
| VERDICT.md | β | 5.1 KB | β raw | |
| manifest.json | β | 1.5 KB | view raw | |
| verdict.json | β | 5.7 KB | view raw |
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)
-
sys_exec_sys_registersnapshots 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. -
On each later exec,
exec_new_vmspace(imgp, vmcopy)forks the snapshot again (kern_exec.c:956-960 β vm_map.c:4298-4331vmspace_execβvmspace_fork), which COW-copies all VM_INHERIT_COPY entries β including the stack entry (vm_map.c:3725-3744, 3865-3890). Thepmap_remove_pages()at kern_exec.c:959 operates on the brand-new fork's empty pmap and removes nothing. -
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 pc64USRSTACK == 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, sovm_map_findspacecannot fitmaxssiz(vm_map.c:1546end > vm_map_max(map)) andvm_map_stackreturnsKERN_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. -
exec_resident_imgactdiscardsexec_new_vmspace's return value (sys/kern/imgact_resident.c:185) and returns 0, so kern_exec marches on:exec_copyout_stringswrites 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]) forDF2952MARKERwith a SIGSEGV-guarded byte scan; exit 42 +RESULT: LEAKon 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 beforevm_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).
Fix verification
fixedfix.diff applied to guest /usr/src; 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; identical PoC procedure: registration succeeds (resident -l lists id 1), resident exec succeeds (victim runs, argv parsed, exit 0), marker scan 0 hits in 3/3 iterations plus one extra confirmation (fix_run.log). Baseline 5/5 x ~7600 hits -> patched 3/3 x 0 hits. Guest reset to with-src afterwards.
fix.diff, fix_build.log, fix_run.log
Confirmed kernel references
- sys/kern/imgact_resident.c:185
- sys/kern/imgact_resident.c:236
- sys/kern/kern_exec.c:949
- sys/kern/kern_exec.c:956
- sys/kern/kern_exec.c:959
- sys/kern/kern_exec.c:991
- sys/vm/vm_map.c:4001
- sys/vm/vm_map.c:4042
- sys/vm/vm_map.c:3725
- sys/vm/vm_map.c:3865
- sys/vm/vm_map.c:4298
- sys/vm/vm_map.c:1546
- sys/platform/pc64/include/vmparam.h:142
- libexec/rtld-elf/rtld.c:675
- usr.sbin/resident/resident.c:148
Detail
Exploit chain
root runs 'resident
Evidence (decisive lines)
run.log (fresh pristine-guest decisive run: 7603 hits, RESULT: LEAK, exit 42); run.2.log/iter.log (5 iterations, 7603/7615/7601/7616/7580 hits, hit addresses logged); run.oneshot.log (small-payload variant, 11 hits; and unregister control, 0 hits); fix_run.log (patched kernel: 3/3 zero hits, resident -l shows registration, binary runs normally); fix_build.log (make -j6 nativekernel rc=0, -Werror clean); VERDICT.md (full narrative); env.txt
PoC changes
Seed sketch assumed the registered program could place markers via its own code before exec_sys_register; in the stock flow registration happens inside rtld BEFORE main runs, so the markers had to be delivered through the root registration environment instead (poisoned DF2952_SECRET_* env). Scanner rewritten with SIGSEGV/siglongjmp page guards and both directions (below and above SP); discovered above-SP hits vary 0..30 with kern.stackgap_random=1024 while below-SP hits (~7600) are the stable bulk. One-shot script kept for documentation; iter.sh is the decisive driver.
Verified recommended fix
exec_new_vmspace(): vm_map_remove(map, stack_addr, USRSTACK) of the forked snapshot's stack region before vm_map_stack(); exec_resident_imgact(): propagate exec_new_vmspace() failure instead of returning 0 (see fix.diff)
Verdict
Every exec of a resident-registered binary forks the registration snapshot (whose vm_map contains the registering process's COW stack entry at [USRSTACK-maxssiz, USRSTACK)) and then vm_map_stack() for the same range fails with KERN_NO_SPACE because on pc64 USRSTACK == VM_MAX_USER_ADDRESS leaves no room; exec_resident_imgact (imgact_resident.c:185) discards the error, so the exec runs on the stale snapshot stack. An unprivileged user who execs the binary reads the registration-time (root) process's stack contents: 5/5 iterations leaked ~7600 marker hits (~140 KB unique root-env data), ~300 B even with a small root env, 0 hits after unregistering (control). The guest stayed healthy (INVARIANTS kernel). Fix validated: vm_map_remove() of the snapshot stack region + error propagation; patched kernel rebuilt in-guest, 0 hits 3/3, resident exec still functional.
No comments yet.