From b013baa98dce6b343b48fcbfe7512dd2aec2e185 Mon Sep 17 00:00:00 2001 From: Nemo <328747105+Nemo-010@users.noreply.github.com> Date: Tue, 15 Sep 2026 05:35:16 +0000 Subject: [PATCH 1/2] Patch original-stack auxv and restore segment protections greetings from Port Edwards. This supersedes #1, keeping the two changes that survived review and dropping the rest. This loader is load-bearing for sharun, so anything that might regress it is worse than the leak it fixes. Kept: - stack: patch the kernel auxv on the original [stack] so tools that read it there, frida-gum in particular, see the loaded binary's AT_BASE, AT_PHDR, AT_PHNUM, AT_PHENT and AT_ENTRY rather than the loader's. The original patch walked from `environ`, which glibc moves to the heap on setenv, so it could read past a heap block and then silently stop patching. This takes the auxv from `start_stack` in /proc/self/stat, the kernel's own record of the initial stack pointer, which setenv does not move. Checked end to end after 64 setenv() calls. - loader: restore each segment's ELF-declared protection with mprotect once relocations are done, instead of leaving every segment writable and executable for the life of the process. Running it after the relocation loop keeps writes into not-yet-protected segments working. The result matches a real execve's maps, including ld.so's RELRO handling. Dropped from #1, all of which the review flagged: - close_fds() closed every descriptor instead of only FD_CLOEXEC ones, which breaks inherited descriptors (shell redirections, systemd socket activation). - reset_signals() changes dispositions before the loader has finished its own allocation and file IO, which can break glibc internals. - PR_SET_NAME is a no-op under sharun's hardlinked layout and leaks an internal filename into comm otherwise. - the loader self-unmap drops the executable from /proc/self/maps while /proc/self/exe still names it, breaking the correspondence tools rely on, and its ppc64 trampoline used the wrong syscall number. Built for x86_64, aarch64, riscv64, powerpc64 (both endians) and loongarch64, and exercised against dynamic glibc binaries and after setenv. We aim to provide the software that shapes the world of tomorrow. --- src/loader.rs | 13 ++++++++++++- src/stack.rs | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/loader.rs b/src/loader.rs index f8b37c0..208cc3e 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -80,6 +80,9 @@ pub fn load( .unwrap(); let page_round_down = |addr: usize| addr / page_size * page_size; let page_round_up = |addr: usize| (addr + (page_size - 1)) / page_size * page_size; + + let mut segments: Vec<(usize, usize, ProtFlags)> = Vec::new(); + for ph in elf.program_headers { if ph.p_type != PT_LOAD { continue; @@ -104,7 +107,7 @@ pub fn load( mmap( Some(addr), size, - prot | ProtFlags::PROT_WRITE, // TODO: read only fix + prot | ProtFlags::PROT_WRITE, MapFlags::MAP_PRIVATE | MapFlags::MAP_FIXED, Some(&file), offset, @@ -119,6 +122,7 @@ pub fn load( page_round_up(file_end_addr) - file_end_addr, ); } + segments.push((addr.get(), size.get(), prot)); } // Relocations (needed for musl but not glibc FWICT) @@ -130,5 +134,12 @@ pub fn load( unsafe { ptr::write(dst, src) } } + // after relocations, so writes into not-yet-protected segments still work + for (addr, size, prot) in segments { + unsafe { + nix::sys::mman::mprotect(addr as *mut nix::libc::c_void, size, prot).unwrap(); + } + } + (base_addr, elf.header, opt_interp) } diff --git a/src/stack.rs b/src/stack.rs index b61b4cc..82c6f03 100644 --- a/src/stack.rs +++ b/src/stack.rs @@ -153,6 +153,23 @@ impl<'a, A: AsRef, E: AsRef> StackBuilder<'a, A, E> { } } +// start_stack (field 28) is the initial sp; unlike environ it survives setenv(). +fn original_auxv() -> Option<*mut usize> { + let stat = std::fs::read_to_string("/proc/self/stat").ok()?; + let fields = stat.rsplit_once(')')?.1; + let start_stack: usize = fields.split_whitespace().nth(25)?.parse().ok()?; + let sp = start_stack as *const usize; + if sp.is_null() { + return None; + } + let argc = unsafe { *sp }; + let mut envp = unsafe { (sp as *const usize).add(argc + 2) }; + while unsafe { *envp } != 0 { + envp = unsafe { envp.add(1) }; + } + Some(unsafe { (envp as *mut usize).add(1) }) +} + pub fn make_stack( interp_addr: Option, bin_addr: usize, @@ -203,5 +220,22 @@ pub fn make_stack( unsafe { std::ptr::copy_nonoverlapping(data.as_ptr(), sp as *mut u8, data.len()) } + if let Some(mut auxv) = original_auxv() { + unsafe { + while *auxv != AT_NULL as usize { + let value = auxv.add(1); + match *auxv { + x if x == AT_BASE as usize => *value = interp_addr.unwrap_or_default(), + x if x == AT_PHDR as usize => *value = bin_addr + bin_header.e_phoff as usize, + x if x == AT_PHNUM as usize => *value = bin_header.e_phnum as usize, + x if x == AT_PHENT as usize => *value = bin_header.e_phentsize as usize, + x if x == AT_ENTRY as usize => *value = bin_addr + bin_header.e_entry as usize, + _ => {} + } + auxv = auxv.add(2); + } + } + } + sp } From 228a8b075a34ab849037e54953c92eaa0638734f Mon Sep 17 00:00:00 2001 From: Nemo <328747105+Nemo-010@users.noreply.github.com> Date: Tue, 15 Sep 2026 05:52:35 +0000 Subject: [PATCH 2/2] stack: carry the vDSO over as AT_SYSINFO_EHDR make_stack enumerates the auxv entries the loaded program needs, and the list never included AT_SYSINFO_EHDR. The kernel maps the vDSO in every process and userland-execve leaves it mapped, but without the entry the dynamic linker cannot find it, so glibc falls back to real syscalls for time(), clock_gettime() and gettimeofday(). A seccomp policy that only allows the vDSO path (e.g. Ladybird's) then fails where a normal execve would not. All five supported architectures emit AT_SYSINFO_EHDR from ARCH_DLINFO, including big-endian powerpc64, where VDSO_AUX_ENT is a plain NEW_AUX_ENT. The address is unchanged by userland-execve, so passing getauxval(AT_SYSINFO_EHDR) reproduces execve's own value; it goes through push_usize, so big-endian keeps the native byte order. --- src/stack.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stack.rs b/src/stack.rs index 82c6f03..e6102d3 100644 --- a/src/stack.rs +++ b/src/stack.rs @@ -10,7 +10,7 @@ use nix::{ libc::{ getauxval, AT_BASE, AT_CLKTCK, AT_EGID, AT_ENTRY, AT_EUID, AT_EXECFN, AT_FLAGS, AT_GID, AT_HWCAP, AT_NULL, AT_PAGESZ, AT_PHDR, AT_PHENT, AT_PHNUM, AT_PLATFORM, AT_RANDOM, - AT_SECURE, AT_UID, + AT_SECURE, AT_SYSINFO_EHDR, AT_UID, }, sys::mman::{mmap, MapFlags, ProtFlags}, unistd::{getegid, geteuid, getgid, getuid, SysconfVar}, @@ -69,6 +69,7 @@ impl<'a, A: AsRef, E: AsRef> StackBuilder<'a, A, E> { (AT_RANDOM, at_random_addr), (AT_CLKTCK, sysconf(SysconfVar::CLK_TCK)), (AT_HWCAP, unsafe { getauxval(AT_HWCAP) }), + (AT_SYSINFO_EHDR, unsafe { getauxval(AT_SYSINFO_EHDR) }), (AT_EGID, getegid().as_raw().into()), (AT_GID, getgid().as_raw().into()), (AT_EUID, geteuid().as_raw().into()),