DF-2952 / verdict.json
{ "finding_id": "DF-2952", "status": "reproduced", "reproduced": 1, "impact": "leak", "confidence": "certain", "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.", "exploit_chain": "root runs 'resident <prog>' (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": "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", "kernel_refs": [ "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" ], "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.", "attempts": 12, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 300, "guest_dirty": 0, "build_cmd": "cc -O0 -o /tmp/df2952/victim /tmp/df2952/victim.c", "run_cmd": "sh /tmp/df2952/iter.sh 1 # root: registers with poisoned env, then: su -m nobody -c 'env -i /tmp/df2952/victim leakcheck'", "code_hash": "7d54931a69ed5052efe9e546047769635d50a6303210cd055a52e13a6f58be02", "notes": "Pass-2 context: known findings DF-0182/0183/0184 re-verified present in source (uninit xresident at :148 with error-path/padding variants; tsleep-wchan UAF at :291-311; lockless exec_res_id reads at :138/:143) - not re-reported. Negative results this pass: vnode-recycle stale v_resident impossible (allocvnode reuses only auxref-free vnodes and bzero()s them, vfs_lock.c:1170-1200); vget on reclaimed held vnode returns ENOENT not panic (vfs_lock.c:598); vmspace_exec forks rather than refs so no double-ref at exec (vm_map.c:4310); register has no fallible steps after vhold (M_WAITOK infallible) so no unwind leak; vr_refs handshake vs unregister is properly lock-serialized (only the known DF-0183 tsleep-wchan issue); exec_new_vmspace error swallow matches imgact_elf.c:706 style; exec_res_id int wrap needs 2^31 registrations (infeasible); fill_xresident res_file-tail/padding uninit is subsumed by DF-0182. Guest reset to with-src snapshot after fix validation.", "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)", "fix_status": "fixed", "fix_kernel_uname": "DragonFly 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", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "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_evidence": "fix.diff, fix_build.log, fix_run.log" } |