use std::ffi::{CStr, CString};
use std::mem::size_of;
use std::os::fd::AsFd;
use std::os::unix::ffi::OsStringExt;
use std::path::Path;
use goblin::elf::Elf;
use goblin::elf::program_header::PT_LOAD;
use rustix::mm::{MapFlags, MprotectFlags, ProtFlags, mmap, mmap_anonymous, mprotect};
const AT_NULL: usize = 0;
const AT_PHDR: usize = 3;
const AT_PHENT: usize = 4;
const AT_PHNUM: usize = 5;
const AT_PAGESZ: usize = 6;
const AT_BASE: usize = 7;
const AT_ENTRY: usize = 9;
const AT_EXECFN: usize = 31;
struct Loaded {
entry: usize,
phoff: usize,
phnum: usize,
phent: usize,
}
#[cfg(target_arch = "x86_64")]
unsafe fn enter(sp: usize, entry: usize) -> ! {
unsafe {
core::arch::asm!(
"mov rsp, {sp}",
"jmp {entry}",
sp = in(reg) sp,
entry = in(reg) entry,
in("rax") 0usize,
in("rdx") 0usize,
options(noreturn),
)
}
}
#[cfg(target_arch = "aarch64")]
unsafe fn enter(sp: usize, entry: usize) -> ! {
unsafe {
core::arch::asm!(
"mov sp, {sp}",
"br {entry}",
sp = in(reg) sp,
entry = in(reg) entry,
in("x0") 0usize,
options(noreturn),
)
}
}
#[cfg(target_arch = "x86")]
unsafe fn enter(sp: usize, entry: usize) -> ! {
unsafe {
core::arch::asm!(
"mov esp, ecx",
"jmp edi",
in("ecx") sp,
in("edi") entry,
in("eax") 0usize,
in("edx") 0usize,
options(noreturn),
)
}
}
fn read_auxv() -> Vec<(usize, usize)> {
let bytes = std::fs::read("/proc/self/auxv").unwrap_or_default();
let w = size_of::<usize>();
let mut out = Vec::new();
let mut i = 0;
while i + 2 * w <= bytes.len() {
let t = usize::from_ne_bytes(bytes[i..i + w].try_into().unwrap());
let v = usize::from_ne_bytes(bytes[i + w..i + 2 * w].try_into().unwrap());
if t == AT_NULL {
break;
}
out.push((t, v));
i += 2 * w;
}
out
}
fn auxval(av: &[(usize, usize)], tag: usize) -> Option<usize> {
av.iter().find(|(t, _)| *t == tag).map(|(_, v)| *v)
}
fn load(path: &Path, page: usize) -> Loaded {
let bytes = std::fs::read(path).expect("ulexec: read interpreter");
let elf = Elf::parse(&bytes).expect("ulexec: parse interpreter");
let file = std::fs::File::open(path).expect("ulexec: open interpreter");
let round_down = |a: usize| a & !(page - 1);
let round_up = |a: usize| (a + page - 1) & !(page - 1);
let first = elf
.program_headers
.iter()
.find(|h| h.p_type == PT_LOAD)
.expect("ulexec: no PT_LOAD");
assert!(
first.p_vaddr == 0,
"ulexec: interpreter is not position-independent"
);
let total: usize = elf
.program_headers
.iter()
.filter(|h| h.p_type == PT_LOAD)
.map(|h| (h.p_vaddr + h.p_memsz) as usize)
.max()
.expect("ulexec: no PT_LOAD");
let base = unsafe {
mmap_anonymous(
std::ptr::null_mut(),
round_up(total),
ProtFlags::READ | ProtFlags::WRITE,
MapFlags::PRIVATE,
)
}
.expect("ulexec: reserve image") as usize;
for ph in elf.program_headers.iter().filter(|h| h.p_type == PT_LOAD) {
let filesz = ph.p_filesz as usize;
if filesz == 0 {
continue; }
let mut prot = ProtFlags::empty();
if ph.p_flags & 0b100 != 0 {
prot |= ProtFlags::READ;
}
if ph.p_flags & 0b010 != 0 {
prot |= ProtFlags::WRITE;
}
if ph.p_flags & 0b001 != 0 {
prot |= ProtFlags::EXEC;
}
let unaligned = base + ph.p_vaddr as usize;
let addr = round_down(unaligned);
let align = unaligned - addr;
unsafe {
mmap(
addr as *mut _,
filesz + align,
prot | ProtFlags::WRITE, MapFlags::PRIVATE | MapFlags::FIXED,
file.as_fd(),
(ph.p_offset as usize - align) as u64,
)
.expect("ulexec: map segment");
}
let file_end = addr + align + filesz;
unsafe {
std::ptr::write_bytes(file_end as *mut u8, 0, round_up(file_end) - file_end);
}
if !prot.contains(ProtFlags::WRITE) {
let _ = unsafe {
mprotect(
addr as *mut _,
round_up(align + filesz),
MprotectFlags::from_bits_truncate(prot.bits()),
)
};
}
}
Loaded {
entry: base + elf.header.e_entry as usize,
phoff: base + elf.header.e_phoff as usize,
phnum: elf.header.e_phnum as usize,
phent: elf.header.e_phentsize as usize,
}
}
struct StackBuilder {
top: usize,
rev: Vec<u8>,
}
impl StackBuilder {
fn push_bytes(&mut self, bytes: &[u8]) -> usize {
for b in bytes.iter().rev() {
self.rev.push(*b);
}
self.top - self.rev.len()
}
fn push_word(&mut self, w: usize) {
self.push_bytes(&w.to_ne_bytes());
}
fn push_cstr(&mut self, s: &CStr) -> usize {
self.push_bytes(s.to_bytes_with_nul())
}
}
fn make_stack(
loaded: &Loaded,
av: &[(usize, usize)],
exe: &CStr,
args: &[CString],
env: &[CString],
) -> usize {
let stack_size = 8 * 1024 * 1024;
let stack = unsafe {
mmap_anonymous(
std::ptr::null_mut(),
stack_size,
ProtFlags::READ | ProtFlags::WRITE,
MapFlags::PRIVATE,
)
}
.expect("ulexec: stack") as usize;
let top = stack + stack_size;
let word = size_of::<usize>();
let mut b = StackBuilder {
top,
rev: Vec::new(),
};
let exe_addr = b.push_cstr(exe);
let env_addrs: Vec<usize> = env.iter().rev().map(|e| b.push_cstr(e)).collect();
let arg_addrs: Vec<usize> = args.iter().rev().map(|a| b.push_cstr(a)).collect();
let mut auxv: Vec<(usize, usize)> = vec![(AT_NULL, 0)];
for &(t, v) in av {
let nv = match t {
AT_PHDR => loaded.phoff,
AT_PHENT => loaded.phent,
AT_PHNUM => loaded.phnum,
AT_BASE => 0, AT_ENTRY => loaded.entry,
AT_EXECFN => exe_addr,
_ => v,
};
auxv.push((t, nv));
}
let fixed_words = auxv.len() * 2 + (env.len() + 1) + (args.len() + 1) + 1;
while (b.rev.len() + fixed_words * word) % 16 != 0 {
b.rev.push(0);
}
for (t, v) in &auxv {
b.push_word(*v);
b.push_word(*t);
}
b.push_word(0); for a in &env_addrs {
b.push_word(*a);
}
b.push_word(0); for a in &arg_addrs {
b.push_word(*a);
}
b.push_word(args.len());
let mut data = b.rev;
data.reverse();
let sp = top - data.len();
unsafe { std::ptr::copy_nonoverlapping(data.as_ptr(), sp as *mut u8, data.len()) };
sp
}
pub fn exec_with_interp(
target: &Path,
interpreter: &Path,
lib_path: &str,
argv0: &str,
args: &[String],
) -> ! {
let interp_str = interpreter.to_string_lossy();
let target_str = target.to_string_lossy();
let is_musl = interpreter
.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.starts_with("ld-musl-"));
let mut argv: Vec<CString> = vec![CString::new(interp_str.as_ref()).unwrap()];
if !is_musl {
argv.push(CString::new("--inhibit-cache").unwrap());
}
if !lib_path.is_empty() {
argv.push(CString::new("--library-path").unwrap());
argv.push(CString::new(lib_path).unwrap());
}
argv.push(CString::new("--argv0").unwrap());
argv.push(CString::new(argv0).unwrap());
argv.push(CString::new(target_str.as_ref()).unwrap());
for arg in args {
argv.push(CString::new(arg.as_str()).unwrap());
}
let env: Vec<CString> = std::env::vars_os()
.filter_map(|(k, v)| {
let mut pair = k.into_vec();
pair.push(b'=');
pair.extend_from_slice(&v.into_vec());
CString::new(pair).ok()
})
.collect();
let auxv = read_auxv();
let page = auxval(&auxv, AT_PAGESZ)
.filter(|p| p.is_power_of_two())
.unwrap_or(4096);
let exe = CString::new(interp_str.as_ref()).unwrap();
let loaded = load(interpreter, page);
let sp = make_stack(&loaded, &auxv, &exe, &argv, &env);
unsafe { enter(sp, loaded.entry) }
}
pub const fn is_supported() -> bool {
cfg!(all(
target_os = "linux",
any(
target_arch = "x86_64",
target_arch = "aarch64",
target_arch = "x86"
)
))
}