β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2952

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2952 Β· 17 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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)

  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).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Fri Sep 4 01:03:31 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

root runs 'resident ' (SYSCAP_NOVM_RESIDENT) -> rtld calls exec_sys_register() from the root invocation (root env strings + rtld/libc frames on its stack) -> vmspace_fork snapshots the whole address space incl. stack (imgact_resident.c:236) -> any user execs the binary -> exec_new_vmspace forks the snapshot; vm_map_stack fails KERN_NO_SPACE (kern_exec.c:991, vm_map.c:4001-4046, pc64 USRSTACK==VM_MAX_USER_ADDRESS); error swallowed (imgact_resident.c:185) -> process runs on stale COW stack -> unprivileged process scans its own stack below/above SP and recovers the root registration run's stack contents (env strings, rtld frames, auxv region).

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.