From 7b76480d63e331b04c0993cc8b2660be85c5fa24 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 24 Apr 2026 18:22:53 -0700 Subject: [PATCH 1/4] Runtime ELF patching and rtld_audit removal Replace the rtld_audit LD_AUDIT-based syscall interception with runtime ELF patching during mmap. When a PROT_EXEC segment is mapped, the shim now patches syscall instructions in-place and places trampoline stubs in a dynamically-allocated region near the code. Key changes: - Add patch_code_segment() public API to syscall rewriter for runtime use - Add ElfPatchState/ElfPatchCache for per-fd tracking of patch state - Add maybe_patch_exec_segment() called from do_mmap_file for PROT_EXEC - Add init_elf_patch_state() to parse ELF headers and detect pre-patched binaries via trampoline magic at file tail - Add finalize_elf_patch() on fd close to clean up trampoline mappings - Add reserve_trampoline parameter to ElfParsedFile::load() to bump brk past the runtime trampoline region for unpatched binaries - Add UnpatchedBinary error variant for loader trampoline parsing - Remove litebox_rtld_audit/ (C LD_AUDIT library) - Remove rtld_audit.so packaging from litebox_packager and runner crates - Remove LD_AUDIT environment variable injection from runners - Remove build.rs files that compiled rtld_audit.so --- Cargo.lock | 1 + dev_bench/unixbench/prepare_unixbench.py | 5 +- dev_tests/src/boilerplate.rs | 1 - litebox/src/mm/linux.rs | 2 +- litebox_common_linux/src/loader.rs | 27 +- litebox_packager/build.rs | 43 -- litebox_packager/src/lib.rs | 17 - litebox_rtld_audit/.gitignore | 1 - litebox_rtld_audit/Makefile | 26 - litebox_rtld_audit/rtld_audit.c | 384 ------------ .../src/lib.rs | 23 +- .../tests/loader.rs | 32 +- litebox_runner_linux_userland/build.rs | 48 -- litebox_runner_linux_userland/src/lib.rs | 34 +- .../tests/common/mod.rs | 2 +- litebox_runner_linux_userland/tests/loader.rs | 2 +- litebox_runner_linux_userland/tests/run.rs | 3 +- litebox_shim_linux/Cargo.toml | 1 + litebox_shim_linux/src/lib.rs | 3 + litebox_shim_linux/src/loader/elf.rs | 47 +- litebox_shim_linux/src/syscalls/file.rs | 12 +- litebox_shim_linux/src/syscalls/mm.rs | 585 +++++++++++++++++- litebox_shim_optee/src/loader/elf.rs | 8 +- litebox_syscall_rewriter/src/lib.rs | 49 ++ 24 files changed, 725 insertions(+), 631 deletions(-) delete mode 100644 litebox_packager/build.rs delete mode 100644 litebox_rtld_audit/.gitignore delete mode 100644 litebox_rtld_audit/Makefile delete mode 100644 litebox_rtld_audit/rtld_audit.c delete mode 100644 litebox_runner_linux_userland/build.rs diff --git a/Cargo.lock b/Cargo.lock index ad0c763ca..0e2e56337 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1698,6 +1698,7 @@ dependencies = [ "litebox", "litebox_common_linux", "litebox_platform_multiplex", + "litebox_syscall_rewriter", "litebox_util_log", "once_cell", "ringbuf", diff --git a/dev_bench/unixbench/prepare_unixbench.py b/dev_bench/unixbench/prepare_unixbench.py index 0d472d505..570ef6f7a 100644 --- a/dev_bench/unixbench/prepare_unixbench.py +++ b/dev_bench/unixbench/prepare_unixbench.py @@ -61,9 +61,8 @@ def prepare_benchmark( """ Prepare a single benchmark using litebox_packager. - The packager discovers dependencies, rewrites all ELFs, and creates a tar - (including litebox_rtld_audit.so). The rewritten main binary is extracted - from the tar and placed alongside it. + The packager discovers dependencies, rewrites all ELFs, and creates a tar. + The rewritten main binary is extracted from the tar and placed alongside it. Returns True on success. """ diff --git a/dev_tests/src/boilerplate.rs b/dev_tests/src/boilerplate.rs index a32cf70b6..c29e14ebf 100644 --- a/dev_tests/src/boilerplate.rs +++ b/dev_tests/src/boilerplate.rs @@ -133,7 +133,6 @@ const SKIP_FILES: &[&str] = &[ "LICENSE", "litebox/src/sync/mutex.rs", "litebox/src/sync/rwlock.rs", - "litebox_rtld_audit/Makefile", "litebox_runner_linux_on_windows_userland/tests/test-bins/hello_exec_nolibc", "litebox_runner_linux_on_windows_userland/tests/test-bins/hello_thread", "litebox_runner_linux_on_windows_userland/tests/test-bins/hello_thread_static", diff --git a/litebox/src/mm/linux.rs b/litebox/src/mm/linux.rs index f33094971..951519d46 100644 --- a/litebox/src/mm/linux.rs +++ b/litebox/src/mm/linux.rs @@ -121,7 +121,7 @@ impl From for MemoryRegionPermissions { } } -const DEFAULT_RESERVED_SPACE_SIZE: usize = 0x100_0000; // 16 MiB +pub const DEFAULT_RESERVED_SPACE_SIZE: usize = 0x100_0000; // 16 MiB bitflags::bitflags! { /// Options for page creation. diff --git a/litebox_common_linux/src/loader.rs b/litebox_common_linux/src/loader.rs index 243c8bfdc..46066c4b8 100644 --- a/litebox_common_linux/src/loader.rs +++ b/litebox_common_linux/src/loader.rs @@ -126,6 +126,8 @@ pub enum ElfParseError { BadTrampoline, #[error("Invalid trampoline version")] BadTrampolineVersion, + #[error("Binary not patched for syscall rewriting")] + UnpatchedBinary, #[error("Unsupported ELF type")] UnsupportedType, #[error("Bad interpreter")] @@ -139,6 +141,7 @@ impl> From> for Errno { | ElfParseError::BadFormat | ElfParseError::BadTrampoline | ElfParseError::BadTrampolineVersion + | ElfParseError::UnpatchedBinary | ElfParseError::BadInterp | ElfParseError::UnsupportedType => Errno::ENOEXEC, ElfParseError::Io(err) => err.into(), @@ -216,6 +219,11 @@ impl ElfParsedFile { }) } + /// Returns `true` if a trampoline was parsed and will be mapped by `load()`. + pub fn has_trampoline(&self) -> bool { + self.trampoline.is_some() + } + /// Parse the LiteBox trampoline data, if any. /// /// The trampoline header is located at the end of the file (last 32/20 bytes). @@ -249,7 +257,8 @@ impl ElfParsedFile { // File must be large enough to contain the header if file_size < header_size as u64 { - return Ok(()); + // Too small for a trampoline header — binary is unpatched. + return Err(ElfParseError::UnpatchedBinary); } // Read the header from the end of the file @@ -265,8 +274,8 @@ impl ElfParsedFile { if &header_buf[0..7] == b"LITEBOX" { return Err(ElfParseError::BadTrampolineVersion); } - // No trampoline found, which is OK (not all binaries are rewritten) - return Ok(()); + // No trampoline found. + return Err(ElfParseError::UnpatchedBinary); } let (file_offset, vaddr, trampoline_size) = if cfg!(target_pointer_width = "64") { @@ -291,9 +300,11 @@ impl ElfParsedFile { ) }; - // Validate trampoline size + // trampoline_size == 0 means the rewriter checked this binary and found + // no syscall instructions. The magic header acts as a "checked" marker so + // the runtime skips eager code-segment patching. No trampoline to map. if trampoline_size == 0 { - return Err(ElfParseError::BadTrampoline); + return Ok(()); } // Verify the file offset is page-aligned (as required by the rewriter) @@ -366,6 +377,7 @@ impl ElfParsedFile { &self, mapper: &mut M, mem: &mut impl AccessMemory, + reserve_trampoline: Option, ) -> Result> { let base_addr = if self.header.e_type == elf::abi::ET_DYN { // Find an aligned load address that will fit all PT_LOAD segments. @@ -473,6 +485,11 @@ impl ElfParsedFile { if self.trampoline.is_some() { self.load_trampoline(mapper, mem, &mut info)?; + } else if let Some(size) = reserve_trampoline { + // Reserve space for a runtime trampoline so brk starts past it. + // The runtime patching path (do_mmap_file → maybe_patch_exec_segment) + // will allocate the actual trampoline in this region via MAP_FIXED. + info.brk = page_align_up(info.brk) + page_align_up(size); } Ok(info) diff --git a/litebox_packager/build.rs b/litebox_packager/build.rs deleted file mode 100644 index 77956be92..000000000 --- a/litebox_packager/build.rs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -use std::path::PathBuf; - -const RTLD_AUDIT_DIR: &str = "../litebox_rtld_audit"; - -fn main() { - let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); - if target_arch != "x86_64" { - return; - } - - let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); - let mut make_cmd = std::process::Command::new("make"); - make_cmd - .current_dir(RTLD_AUDIT_DIR) - .env("OUT_DIR", &out_dir) - .env("ARCH", &target_arch); - // Always build without DEBUG for the packager -- packaged binaries are - // release artifacts. - make_cmd.env_remove("DEBUG"); - // Force rebuild in case a stale artifact exists from a different config. - let _ = std::fs::remove_file(out_dir.join("litebox_rtld_audit.so")); - - let output = make_cmd - .output() - .expect("Failed to execute make for rtld_audit"); - assert!( - output.status.success(), - "failed to build rtld_audit.so via make:\nstdout: {}\nstderr: {}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ); - assert!( - out_dir.join("litebox_rtld_audit.so").exists(), - "Build failed to create litebox_rtld_audit.so" - ); - - println!("cargo:rerun-if-changed={RTLD_AUDIT_DIR}/rtld_audit.c"); - println!("cargo:rerun-if-changed={RTLD_AUDIT_DIR}/Makefile"); - println!("cargo:rerun-if-changed=build.rs"); -} diff --git a/litebox_packager/src/lib.rs b/litebox_packager/src/lib.rs index 57120ec4e..53760e5b2 100644 --- a/litebox_packager/src/lib.rs +++ b/litebox_packager/src/lib.rs @@ -356,23 +356,6 @@ fn finalize_tar( }); } - // Include the rtld audit library so the rewriter backend can load it. - #[cfg(target_arch = "x86_64")] - { - const RTLD_AUDIT_TAR_PATH: &str = "lib/litebox_rtld_audit.so"; - if !added_tar_paths.insert(RTLD_AUDIT_TAR_PATH.to_string()) { - bail!( - "tar already contains {RTLD_AUDIT_TAR_PATH} -- \ - remove the conflicting entry or use --no-rewrite" - ); - } - tar_entries.push(TarEntry { - tar_path: RTLD_AUDIT_TAR_PATH.to_string(), - data: include_bytes!(concat!(env!("OUT_DIR"), "/litebox_rtld_audit.so")).to_vec(), - mode: 0o755, - }); - } - // Build tar. eprintln!("Creating {}...", args.output.display()); build_tar(&tar_entries, &args.output)?; diff --git a/litebox_rtld_audit/.gitignore b/litebox_rtld_audit/.gitignore deleted file mode 100644 index 140f8cf80..000000000 --- a/litebox_rtld_audit/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.so diff --git a/litebox_rtld_audit/Makefile b/litebox_rtld_audit/Makefile deleted file mode 100644 index b3a3ad3a3..000000000 --- a/litebox_rtld_audit/Makefile +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -SRC = rtld_audit.c -OUT_DIR ?= . -OUTPUT = $(OUT_DIR)/litebox_rtld_audit.so -CC ?= cc -CFLAGS ?= -Wall -Werror -fPIC -shared -nostdlib -ARCH ?= $(shell uname -m) -ifeq ($(ARCH),x86_64) - CFLAGS += -m64 -else - $(error Unsupported target architecture: $(ARCH)) -endif -ifdef DEBUG - CFLAGS += -DDEBUG -endif -all: $(OUTPUT) - -$(OUTPUT): $(SRC) - $(CC) $(CFLAGS) -o $@ $< - -clean: - rm -f $(OUTPUT) - -.PHONY: all clean diff --git a/litebox_rtld_audit/rtld_audit.c b/litebox_rtld_audit/rtld_audit.c deleted file mode 100644 index 51713f941..000000000 --- a/litebox_rtld_audit/rtld_audit.c +++ /dev/null @@ -1,384 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#define _GNU_SOURCE -#include -#include -#include - -// The magic number used to identify the LiteBox trampoline. -// This must match `TRAMPOLINE_MAGIC` in `litebox_syscall_rewriter` and `litebox_common_linux`. -// Value 0x30584f424554494c is "LITEBOX0" in little-endian (bytes: 'L','I','T','E','B','O','X','0') -#define TRAMPOLINE_MAGIC ((uint64_t)0x30584f424554494c) - -#if !defined(__x86_64__) -# error "rtld_audit.c: build target must be x86_64" -#endif - -// Linux syscall numbers (x86_64) -#define SYS_openat 257 -#define SYS_read 0 -#define SYS_write 1 -#define SYS_close 3 -#define SYS_fstat 5 -#define SYS_mmap 9 -#define SYS_mprotect 10 -#define SYS_munmap 11 -#define SYS_exit_group 231 -#define AT_FDCWD -100 - -// Maximum valid userspace address (48-bit address space) -#define MAX_USERSPACE_ADDR 0x7FFFFFFFFFFFUL - -// Trampoline header layout for x86_64: magic(8) + file_offset(8) + vaddr(8) + size(8) = 32 bytes -struct __attribute__((packed)) TrampolineHeader { - uint64_t magic; - uint64_t file_offset; - uint64_t vaddr; - uint64_t trampoline_size; -}; - -// Linux flags -#define MAP_PRIVATE 0x02 -#define MAP_FIXED 0x10 -#define PROT_READ 0x1 -#define PROT_WRITE 0x2 -#define PROT_EXEC 0x4 - -typedef long (*syscall_stub_t)(void); -static syscall_stub_t syscall_entry = 0; -static char interp[256] = {0}; // Buffer for interpreter path - -#ifdef DEBUG -#define syscall_print(str, len) \ - do_syscall(SYS_write, 1, (long)(str), len, 0, 0, 0) -#else -#define syscall_print(str, len) -#endif - -static long do_syscall(long num, long a1, long a2, long a3, long a4, long a5, - long a6) { - if (!syscall_entry) - return -1; - - register long rax __asm__("rax") = num; - register long rdi __asm__("rdi") = a1; - register long rsi __asm__("rsi") = a2; - register long rdx __asm__("rdx") = a3; - register long r10 __asm__("r10") = a4; - register long r8 __asm__("r8") = a5; - register long r9 __asm__("r9") = a6; - - __asm__ volatile("leaq 1f(%%rip), %%rcx\n" - "jmp *%[entry]\n" - "1:\n" - : "+r"(rax) - : [entry] "r"(syscall_entry), "r"(rdi), "r"(rsi), "r"(rdx), - "r"(r10), "r"(r8), "r"(r9) - : "rcx", "r11", "memory"); - return rax; -} - -/* Re-implement some utility functions and re-define the structures to avoid - * dependency on libc. */ - -// Define the FileStat structure -struct FileStat { - unsigned long st_dev; - unsigned long st_ino; - unsigned long st_nlink; - - unsigned int st_mode; - unsigned int st_uid; - unsigned int st_gid; - unsigned int __pad0; - unsigned long st_rdev; - long st_size; - long st_blksize; - long st_blocks; /* Number 512-byte blocks allocated. */ - - unsigned long st_atime; - unsigned long st_atime_nsec; - unsigned long st_mtime; - unsigned long st_mtime_nsec; - unsigned long st_ctime; - unsigned long st_ctime_nsec; - long __unused[3]; -}; - -int memcmp(const void *s1, const void *s2, size_t n) { - const unsigned char *p1 = s1; - const unsigned char *p2 = s2; - while (n--) { - if (*p1 != *p2) { - return *p1 - *p2; - } - p1++; - p2++; - } - return 0; -} - -int strcmp(const char *s1, const char *s2) { - while (*s1 && (*s1 == *s2)) { - s1++; - s2++; - } - return *(unsigned char *)s1 - *(unsigned char *)s2; -} - -char *strncpy(char *dest, const char *src, size_t n) { - char *d = dest; - const char *s = src; - while (n-- && *s) { - *d++ = *s++; - } - while (n--) { - *d++ = '\0'; - } - return dest; -} - -static uint64_t read_u64(const void *p) { - uint64_t v; - __builtin_memcpy(&v, p, 8); - return v; -} - -static size_t align_up(size_t val, size_t align) { - size_t result = (val + align - 1) & ~(align - 1); - // Check for overflow (result < val means we wrapped) - if (result < val) return (size_t)-1; - return result; -} - -unsigned int la_version(unsigned int version __attribute__((unused))) { - return LAV_CURRENT; -} - -/// print value in hex -void print_hex(uint64_t data) { -#ifdef DEBUG - for (int i = 15; i >= 0; i--) { - unsigned char byte = (data >> (i * 4)) & 0xF; - if (byte < 10) { - syscall_print((&"0123456789"[byte]), 1); - } else { - syscall_print((&"abcdef"[byte - 10]), 1); - } - } - syscall_print("\n", 1); -#endif -} - -/// @brief Parse object to find the syscall entry point and the interpreter -/// path. -/// -/// The trampoline is already mapped by the litebox loader at (base + vaddr). -/// The entry point is at offset 0 of the mapped trampoline. The litebox loader -/// already validated the magic when parsing the file header. -int parse_object(const struct link_map *map) { - unsigned long max_addr = 0; - Elf64_Ehdr *eh = (Elf64_Ehdr *)map->l_addr; - if (memcmp(eh->e_ident, - "\x7f" - "ELF", - 4) != 0) { - syscall_print("[audit] not an ELF file\n", 24); - return 1; - } - Elf64_Phdr *phdrs = (Elf64_Phdr *)((char *)map->l_addr + eh->e_phoff); - for (int i = 0; i < eh->e_phnum; i++) { - if (phdrs[i].p_type == PT_LOAD) { - unsigned long vaddr_end = (phdrs[i].p_vaddr + phdrs[i].p_memsz); - if (vaddr_end > max_addr) { - max_addr = vaddr_end; - } - } else if (phdrs[i].p_type == PT_INTERP) { - strncpy(interp, (char *)map->l_addr + phdrs[i].p_vaddr, - sizeof(interp) - 1); - interp[sizeof(interp) - 1] = '\0'; // Ensure null termination - } - } - max_addr = align_up(max_addr, 0x1000); - void *trampoline_addr = (void *)map->l_addr + max_addr; - // The trampoline code has the syscall entry point at offset 0. - syscall_entry = (syscall_stub_t)read_u64(trampoline_addr); - if (syscall_entry == 0) { - syscall_print("[audit] syscall entry is null\n", 30); - return 1; - } - print_hex((uint64_t)syscall_entry); - return 0; -} - -unsigned int la_objopen(struct link_map *map, - Lmid_t lmid __attribute__((unused)), - uintptr_t *cookie __attribute__((unused))) { - syscall_print("[audit] la_objopen called\n", 26); - const char *path = map->l_name; - - if (!path || path[0] == '\0') { - // main binary should be called first. - if (map->l_addr != 0) { - // `map->l_addr` is zero for the main binary if it is not position - // independent. - if (parse_object(map) != 0) { - syscall_print("[audit] failed to parse main binary\n", 36); - return 0; - } - syscall_print("[audit] main binary is patched by libOS\n", 40); - syscall_print("[audit] interp=", 15); - syscall_print(interp, sizeof(interp) - 1); - syscall_print("\n", 1); - } - return 0; // main binary is patched by libOS - } - - if (syscall_entry == 0) { - // failed to get the syscall entry point from the main binary - // fall back to get it from ld-*.so, which should be called next. - if (parse_object(map) != 0) { - syscall_print("[audit] failed to parse ld\n", 27); - return 0; - } - syscall_print("[audit] ld is patched by libOS: \n", 33); - syscall_print(path, 32); - syscall_print("\n", 1); - return 0; // ld.so is patched by libOS - } - - if (interp[0] != '\0' && strcmp(path, interp) == 0) { - // successfully get the entry point and interpreter from the main binary - syscall_print("[audit] ld-*.so is patched by libOS\n", 36); - return 0; // ld.so is patched by libOS - } - - // Other shared libraries - syscall_print("[audit] la_objopen: path=", 25); - syscall_print(path, 32); - syscall_print("\n", 1); - - if (!syscall_entry) { - return 0; - } - - int fd = do_syscall(SYS_openat, AT_FDCWD, (long)path, 0, 0, 0, 0); - if (fd < 0) { - syscall_print("[audit] failed to open file\n", 28); - return 0; - } - - struct FileStat st; - if (do_syscall(SYS_fstat, fd, (long)&st, 0, 0, 0, 0) < 0) { - syscall_print("[audit] fstat failed\n", 21); - do_syscall(SYS_close, fd, 0, 0, 0, 0, 0); - return 0; - } - long file_size = st.st_size; - - // File must be large enough to contain at least a trampoline header - if (file_size < (long)sizeof(struct TrampolineHeader)) { - do_syscall(SYS_close, fd, 0, 0, 0, 0, 0); - return 0; - } - - // The trampoline header is at the end of the file (last 32 bytes for x86_64). - // File layout: [ELF][padding][trampoline code][header] - // Read the last page that contains the header. - long header_offset = file_size - sizeof(struct TrampolineHeader); - long header_page_offset = header_offset & ~0xFFFUL; - - // Map the page containing the header - void *header_page = (void *)do_syscall(SYS_mmap, 0, 0x1000, PROT_READ, MAP_PRIVATE, fd, header_page_offset); - if ((uintptr_t)header_page >= (uintptr_t)-4096) { - syscall_print("[audit] mmap header page failed\n", 32); - do_syscall(SYS_close, fd, 0, 0, 0, 0, 0); - return 0; - } - - // Read header from the mapped page - long header_in_page_offset = header_offset - header_page_offset; - const struct TrampolineHeader *header = (const struct TrampolineHeader *)((const char *)header_page + header_in_page_offset); - - // Check magic - if (header->magic != TRAMPOLINE_MAGIC) { - // If the prefix matches but the version differs, fail explicitly. - if (memcmp(header, "LITEBOX", 7) == 0) { - syscall_print("[audit] invalid trampoline version\n", 36); - do_syscall(SYS_munmap, (long)header_page, 0x1000, 0, 0, 0, 0); - do_syscall(SYS_close, fd, 0, 0, 0, 0, 0); - return 0; - } - // No trampoline found - do_syscall(SYS_munmap, (long)header_page, 0x1000, 0, 0, 0, 0); - do_syscall(SYS_close, fd, 0, 0, 0, 0, 0); - return 0; - } - - // Copy fields before unmapping - uint64_t tramp_file_offset = header->file_offset; - uint64_t tramp_vaddr = header->vaddr; - uint64_t tramp_size_raw = header->trampoline_size; - - do_syscall(SYS_munmap, (long)header_page, 0x1000, 0, 0, 0, 0); - syscall_print("[audit] found trampoline header at end of file\n", 47); - - // Validate trampoline size - if (tramp_size_raw == 0) { - syscall_print("[audit] trampoline code size invalid\n", 37); - do_syscall(SYS_close, fd, 0, 0, 0, 0, 0); - return 0; - } - - // Verify file offset is page-aligned - if ((tramp_file_offset & 0xFFF) != 0) { - syscall_print("[audit] trampoline code not page-aligned\n", 41); - do_syscall(SYS_close, fd, 0, 0, 0, 0, 0); - return 0; - } - - // The trampoline code should immediately precede the header. - if (tramp_file_offset + tramp_size_raw != (uint64_t)header_offset) { - syscall_print("[audit] trampoline extends beyond header\n", 41); - do_syscall(SYS_close, fd, 0, 0, 0, 0, 0); - return 0; - } - - // Validate tramp_vaddr is within reasonable userspace bounds and page-aligned - if (tramp_vaddr > MAX_USERSPACE_ADDR || (tramp_vaddr & 0xFFF) != 0) { - syscall_print("[audit] trampoline vaddr out of bounds\n", 39); - do_syscall(SYS_close, fd, 0, 0, 0, 0, 0); - return 0; - } - - uint64_t tramp_addr = map->l_addr + tramp_vaddr; - uint64_t tramp_size = align_up(tramp_size_raw, 0x1000); - - // Check for overflow in align_up or address calculation - if (tramp_size == (size_t)-1 || tramp_addr < map->l_addr) { - syscall_print("[audit] trampoline size/addr overflow\n", 38); - do_syscall(SYS_close, fd, 0, 0, 0, 0, 0); - return 0; - } - - // Use MAP_FIXED to place the trampoline at the exact required address. - // The loader ensures this range is not used by other mappings. - void *mapped = - (void *)do_syscall(SYS_mmap, tramp_addr, tramp_size, - PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_FIXED, fd, tramp_file_offset); - if ((uintptr_t)mapped >= (uintptr_t)-4096) { - syscall_print("[audit] mmap failed for trampoline\n", 35); - do_syscall(SYS_close, fd, 0, 0, 0, 0, 0); - return 0; - } - - // Write the syscall entry point at the start of the trampoline code - __builtin_memcpy((char *)mapped, (const void *)&syscall_entry, 8); - do_syscall(SYS_mprotect, (long)mapped, tramp_size, PROT_READ | PROT_EXEC, 0, - 0, 0); - syscall_print("[audit] trampoline patched and protected\n", 41); - - do_syscall(SYS_close, fd, 0, 0, 0, 0, 0); - return 0; -} diff --git a/litebox_runner_linux_on_windows_userland/src/lib.rs b/litebox_runner_linux_on_windows_userland/src/lib.rs index 5afc2a208..658b20ca5 100644 --- a/litebox_runner_linux_on_windows_userland/src/lib.rs +++ b/litebox_runner_linux_on_windows_userland/src/lib.rs @@ -14,16 +14,16 @@ use std::path::PathBuf; /// Run Linux programs with LiteBox on unmodified Windows. /// -/// The program binary and all its dependencies (including `litebox_rtld_audit.so`) -/// must be provided inside a tar archive via `--initial-files`. The program path -/// refers to a path inside the tar archive. +/// The program binary and all its dependencies must be provided inside a tar +/// archive via `--initial-files`. The program path refers to a path inside the +/// tar archive. #[derive(Parser, Debug)] pub struct CliArgs { /// The program and arguments passed to it (e.g., `/bin/ls --color`). /// /// The program path refers to a path inside the tar archive provided via /// `--initial-files`. All binaries must be pre-rewritten with the syscall - /// rewriter and the tar must include `litebox_rtld_audit.so`. + /// rewriter. #[arg(required = true, trailing_var_arg = true, value_hint = clap::ValueHint::CommandWithArguments)] pub program_and_arguments: Vec, /// Environment variables passed to the program (`K=V` pairs; can be invoked multiple times) @@ -35,7 +35,7 @@ pub struct CliArgs { /// Allow using unstable options #[arg(short = 'Z', long = "unstable")] pub unstable: bool, - /// Tar archive containing the program, its shared libraries, and litebox_rtld_audit.so. + /// Tar archive containing the program and its shared libraries. /// /// All ELF binaries should be pre-rewritten with the syscall rewriter /// (e.g., via `litebox-packager`). @@ -70,7 +70,7 @@ pub fn run(cli_args: CliArgs) -> Result<()> { let platform = Platform::new(); litebox_platform_multiplex::set_platform(platform); - let mut shim_builder = litebox_shim_linux::LinuxShimBuilder::new(); + let shim_builder = litebox_shim_linux::LinuxShimBuilder::new(); let litebox = shim_builder.litebox(); // The program path is a Unix-style path inside the tar archive. @@ -93,7 +93,6 @@ pub fn run(cli_args: CliArgs) -> Result<()> { }; let initial_file_system = std::sync::Arc::new(initial_file_system); - shim_builder.set_load_filter(fixup_env); let shim = shim_builder.build(); let argv = cli_args .program_and_arguments @@ -138,13 +137,3 @@ pub fn run(cli_args: CliArgs) -> Result<()> { } std::process::exit(program.process.wait()) } - -fn fixup_env(envp: &mut Vec) { - // Always inject LD_AUDIT so the dynamic linker loads the audit library - // that sets up trampolines for rewritten binaries. - let p = c"LD_AUDIT=/lib/litebox_rtld_audit.so"; - let has_ld_audit = envp.iter().any(|var| var.as_c_str() == p); - if !has_ld_audit { - envp.push(p.into()); - } -} diff --git a/litebox_runner_linux_on_windows_userland/tests/loader.rs b/litebox_runner_linux_on_windows_userland/tests/loader.rs index e6f470e34..1a0849aef 100644 --- a/litebox_runner_linux_on_windows_userland/tests/loader.rs +++ b/litebox_runner_linux_on_windows_userland/tests/loader.rs @@ -4,9 +4,8 @@ //! Tests for the Windows userland runner. //! //! **NOTE:** These tests depend on pre-built Linux ELF binaries in `tests/test-bins/`, -//! including `litebox_rtld_audit.so`, shared libraries (`libc.so.6`, `ld-linux-x86-64.so.2`), -//! and test executables. These binaries must be rebuilt on Linux and re-committed whenever -//! the corresponding source code changes (e.g., `litebox_rtld_audit/rtld_audit.c`). +//! including shared libraries (`libc.so.6`, `ld-linux-x86-64.so.2`) +//! and test executables. #![cfg(all(target_os = "windows", target_arch = "x86_64"))] @@ -198,7 +197,6 @@ fn test_static_linked_prog_with_rewriter() { fn run_dynamic_linked_prog_with_rewriter( libs_to_rewrite: &[(&str, &str)], - libs_without_rewrite: &[(&str, &str)], exec_name: &str, cmd_args: &[&str], install_files: fn(std::path::PathBuf), @@ -276,22 +274,6 @@ fn run_dynamic_linked_prog_with_rewriter( ); } - // Copy libraries that are not needed to be rewritten (`litebox_rtld_audit.so`) - // to the tar directory - for (file, prefix) in libs_without_rewrite { - let src = test_dir.join(file); - let dst_dir = tar_src_path.join(prefix.trim_start_matches('/')); - let dst = dst_dir.join(file); - std::fs::create_dir_all(&dst_dir).unwrap(); - let _ = std::fs::remove_file(&dst); - println!( - "Copying {} to {}", - src.to_str().unwrap(), - dst.to_str().unwrap() - ); - std::fs::copy(&src, &dst).unwrap(); - } - // Install the required files (e.g., scripts) to tar directory's /out install_files(tar_src_path.join("out")); @@ -361,14 +343,6 @@ fn test_testcase_dynamic_with_rewriter() { ("libc.so.6", "/lib/x86_64-linux-gnu"), ("ld-linux-x86-64.so.2", "/lib64"), ]; - let libs_without_rewrite = [("litebox_rtld_audit.so", "/lib")]; - // Run - run_dynamic_linked_prog_with_rewriter( - &libs_to_rewrite, - &libs_without_rewrite, - exec_name, - &[], - |_| {}, - ); + run_dynamic_linked_prog_with_rewriter(&libs_to_rewrite, exec_name, &[], |_| {}); } diff --git a/litebox_runner_linux_userland/build.rs b/litebox_runner_linux_userland/build.rs deleted file mode 100644 index 3360e452a..000000000 --- a/litebox_runner_linux_userland/build.rs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -use std::path::PathBuf; - -const RTLD_AUDIT_DIR: &str = "../litebox_rtld_audit"; - -fn main() { - let mut make_cmd = std::process::Command::new("make"); - let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); - let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); - if target_arch != "x86_64" { - // XXX: Currently 32-bit x86 is unsupported (unimplemented), skip building - return; - } - make_cmd - .current_dir(RTLD_AUDIT_DIR) - .env("OUT_DIR", &out_dir) - .env("ARCH", target_arch); - if std::env::var("PROFILE").unwrap_or_default() == "debug" { - make_cmd.env("DEBUG", "1"); - } else { - // Explicitly remove DEBUG to prevent inheriting it from the - // parent environment, which would cause the C library to be - // built with debug prints enabled. - make_cmd.env_remove("DEBUG"); - } - // Force rebuild in case CFLAGS changed (e.g., debug -> release) but - // the source did not. - let _ = std::fs::remove_file(out_dir.join("litebox_rtld_audit.so")); - let output = make_cmd - .output() - .expect("Failed to execute make for rtld_audit"); - assert!( - output.status.success(), - "failed to build rtld_audit.so via make:\nstdout: {}\nstderr: {}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ); - assert!( - out_dir.join("litebox_rtld_audit.so").exists(), - "Build failed to create necessary file" - ); - - println!("cargo:rerun-if-changed={RTLD_AUDIT_DIR}/rtld_audit.c"); - println!("cargo:rerun-if-changed={RTLD_AUDIT_DIR}/Makefile"); - println!("cargo:rerun-if-changed=build.rs"); -} diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 90b24878b..213059d98 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -196,7 +196,7 @@ pub fn run(cli_args: CliArgs) -> Result<()> { } litebox_platform_multiplex::set_platform(platform); - let mut shim_builder = litebox_shim_linux::LinuxShimBuilder::new(); + let shim_builder = litebox_shim_linux::LinuxShimBuilder::new(); let litebox = shim_builder.litebox(); let initial_file_system = { let mut in_mem = litebox::fs::in_mem::FileSystem::new(litebox); @@ -275,28 +275,6 @@ pub fn run(cli_args: CliArgs) -> Result<()> { } }); - // include litebox_rtld_audit.so in the filesystem so tests and users don't need to include it in tar files - #[cfg(not(target_arch = "x86_64"))] - eprintln!("WARN: litebox_rtld_audit not currently supported on non-x86_64 arch"); - #[cfg(target_arch = "x86_64")] - in_mem.with_root_privileges(|fs| { - let rwxr_xr_x = Mode::RWXU | Mode::RGRP | Mode::XGRP | Mode::ROTH | Mode::XOTH; - let _ = fs.mkdir("/lib", rwxr_xr_x); - let fd = fs - .open( - "/lib/litebox_rtld_audit.so", - litebox::fs::OFlags::WRONLY | litebox::fs::OFlags::CREAT, - rwxr_xr_x, - ) - .expect("Failed to create /lib/litebox_rtld_audit.so"); - fs.initialize_primarily_read_heavy_file( - &fd, - include_bytes!(concat!(env!("OUT_DIR"), "/litebox_rtld_audit.so")).into(), - ); - fs.close(&fd) - .expect("Failed to close /lib/litebox_rtld_audit.so"); - }); - let tar_ro = litebox::fs::tar_ro::FileSystem::new(litebox, tar_data.into()); shim_builder.default_fs(in_mem, tar_ro) }; @@ -318,7 +296,6 @@ pub fn run(cli_args: CliArgs) -> Result<()> { let initial_file_system = std::sync::Arc::new(initial_file_system); - shim_builder.set_load_filter(fixup_env); let shim = shim_builder.build(); let shutdown = std::sync::Arc::new(core::sync::atomic::AtomicBool::new(false)); @@ -431,12 +408,3 @@ fn pin_thread_to_cpu(cpu: usize) { } } } - -fn fixup_env(envp: &mut Vec) { - // Enable the audit library to load trampoline code for rewritten binaries. - let p = c"LD_AUDIT=/lib/litebox_rtld_audit.so"; - let has_ld_audit = envp.iter().any(|var| var.as_c_str() == p); - if !has_ld_audit { - envp.push(p.into()); - } -} diff --git a/litebox_runner_linux_userland/tests/common/mod.rs b/litebox_runner_linux_userland/tests/common/mod.rs index 60d760aa9..aee207d58 100644 --- a/litebox_runner_linux_userland/tests/common/mod.rs +++ b/litebox_runner_linux_userland/tests/common/mod.rs @@ -98,7 +98,7 @@ fn find_rewriter_source_files() -> Vec { /// Compile C code into an executable with caching pub fn compile(src_path: &str, unique_name: &str, exec_or_lib: bool, nolibc: bool) -> PathBuf { - let dir_path = std::env::var("OUT_DIR").unwrap(); + let dir_path = env!("CARGO_TARGET_TMPDIR").to_string(); let path = std::path::Path::new(dir_path.as_str()).join(unique_name); let output = path.to_str().unwrap(); diff --git a/litebox_runner_linux_userland/tests/loader.rs b/litebox_runner_linux_userland/tests/loader.rs index c96168f50..0c06c8a01 100644 --- a/litebox_runner_linux_userland/tests/loader.rs +++ b/litebox_runner_linux_userland/tests/loader.rs @@ -240,7 +240,7 @@ void _start() { #[test] fn test_syscall_rewriter() { - let dir_path = std::env::var("OUT_DIR").unwrap(); + let dir_path = env!("CARGO_TARGET_TMPDIR").to_string(); let src_path = std::path::Path::new(dir_path.as_str()).join("hello_exec_nolibc.c"); std::fs::write(src_path.clone(), HELLO_WORLD_NOLIBC).unwrap(); let path = std::path::Path::new(dir_path.as_str()).join("hello_exec_nolibc"); diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index f9e3c6b7a..72c073811 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -22,7 +22,7 @@ struct Runner { impl Runner { fn new(target: &Path, unique_name: &str) -> Self { - let dir_path = PathBuf::from(std::env::var_os("OUT_DIR").unwrap()); + let dir_path = PathBuf::from(env!("CARGO_TARGET_TMPDIR")); let path = { // new path in out_dir with .hooked suffix let out_path = dir_path.join(format!( @@ -186,7 +186,6 @@ fn find_c_test_files(dir: &str) -> Vec { files } -// our rtld_audit does not support x86 yet #[cfg(target_arch = "x86_64")] #[test] fn test_dynamic_lib_with_rewriter() { diff --git a/litebox_shim_linux/Cargo.toml b/litebox_shim_linux/Cargo.toml index c89fed9ed..8d5d3579c 100644 --- a/litebox_shim_linux/Cargo.toml +++ b/litebox_shim_linux/Cargo.toml @@ -17,6 +17,7 @@ syscalls = { version = "0.6", default-features = false } seq-macro = "0.3" ringbuf = { version = "0.4.8", default-features = false, features = ["alloc"] } zerocopy = { version = "0.8", default-features = false, features = ["derive"] } +litebox_syscall_rewriter = { version = "0.1.0", path = "../litebox_syscall_rewriter", default-features = false } [features] default = ["platform_linux_userland"] diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index c1c3d1b47..fec1ef99c 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -197,6 +197,7 @@ impl LinuxShimBuilder { next_thread_id: 2.into(), // start from 2, as 1 is used by the main thread litebox: self.litebox, unix_addr_table: litebox::sync::RwLock::new(syscalls::unix::UnixAddrTable::new()), + elf_patch_cache: litebox::sync::Mutex::new(alloc::collections::BTreeMap::new()), }); LinuxShim(global) } @@ -1022,6 +1023,8 @@ struct GlobalState { next_thread_id: core::sync::atomic::AtomicI32, /// UNIX domain socket address table unix_addr_table: litebox::sync::RwLock>, + /// Per-process collection of ELF patching state for runtime syscall rewriting. + elf_patch_cache: litebox::sync::Mutex, } struct Task { diff --git a/litebox_shim_linux/src/loader/elf.rs b/litebox_shim_linux/src/loader/elf.rs index 0d62030a8..f52a5f565 100644 --- a/litebox_shim_linux/src/loader/elf.rs +++ b/litebox_shim_linux/src/loader/elf.rs @@ -172,9 +172,43 @@ impl<'a, FS: ShimFS> FileAndParsed<'a, FS> { let file = ElfFile::new(task, path).map_err(ElfLoaderError::OpenError)?; let mut parsed = litebox_common_linux::loader::ElfParsedFile::parse(&mut &file) .map_err(ElfLoaderError::ParseError)?; - parsed.parse_trampoline(&mut &file, task.global.platform.get_syscall_entry_point())?; + + let syscall_entry_point = task.global.platform.get_syscall_entry_point(); + + // Try to parse an embedded trampoline. For pre-patched binaries this + // succeeds and load_trampoline() will map it. For unpatched binaries + // (UnpatchedBinary error), the runtime patching during mmap will patch + // code segments as they are mapped. + if syscall_entry_point != 0 { + match parsed.parse_trampoline(&mut &file, syscall_entry_point) { + Ok(()) | Err(litebox_common_linux::loader::ElfParseError::UnpatchedBinary) => { + // Ok: pre-patched trampoline found, or unpatched binary + // that the runtime mmap hook will handle. + } + Err(e) => return Err(ElfLoaderError::ParseError(e)), + } + } + Ok(Self { file, parsed }) } + + /// Load the ELF into guest memory. + fn load_mapped( + &mut self, + platform: &(impl litebox::platform::RawPointerProvider + litebox::platform::SystemInfoProvider), + ) -> Result { + let syscall_entry_point = self.file.task.global.platform.get_syscall_entry_point(); + // When the platform requires syscall rewriting but the binary has no + // embedded trampoline, reserve space so that brk starts past the + // runtime trampoline region. + let reserve = if syscall_entry_point != 0 && !self.parsed.has_trampoline() { + Some(litebox::mm::linux::DEFAULT_RESERVED_SPACE_SIZE) + } else { + None + }; + let result = self.parsed.load(&mut self.file, &mut &*platform, reserve); + Ok(result?) + } } impl<'a, FS: ShimFS> ElfLoader<'a, FS> { @@ -204,18 +238,11 @@ impl<'a, FS: ShimFS> ElfLoader<'a, FS> { let global = &self.main.file.task.global; // Load the main ELF file first so that it gets privileged addresses. - let info = self - .main - .parsed - .load(&mut self.main.file, &mut &*global.platform)?; + let info = self.main.load_mapped(global.platform)?; // Load the interpreter ELF file, if any. let interp = if let Some(interp) = &mut self.interp { - Some( - interp - .parsed - .load(&mut interp.file, &mut &*global.platform)?, - ) + Some(interp.load_mapped(global.platform)?) } else { None }; diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 7b900243d..39eeb7a6b 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -570,6 +570,10 @@ impl Task { /// Handle syscall `close` pub(crate) fn sys_close(&self, fd: i32) -> Result<(), Errno> { + // Finalize any in-progress ELF patching for this fd (mprotect + // trampoline RW→RX) before closing the descriptor. + self.finalize_elf_patch(fd); + let Ok(raw_fd) = u32::try_from(fd).and_then(usize::try_from) else { return Err(Errno::EBADF); }; @@ -2055,8 +2059,14 @@ impl Task { Ok(oldfd) }; } - // Close whatever is at newfd before duping into it + // Close whatever is at newfd before duping into it. + // Finalize any in-progress ELF patching for the target fd first, + // since dup2/dup3 implicitly closes it without going through + // sys_close. let newfd_usize = usize::try_from(newfd).or(Err(Errno::EBADF))?; + if let Ok(fd) = i32::try_from(newfd) { + self.finalize_elf_patch(fd); + } let _ = self.do_close(newfd_usize); self.do_dup_inner( oldfd_usize, diff --git a/litebox_shim_linux/src/syscalls/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index ce6c3513c..d2152e6f5 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -4,10 +4,12 @@ //! Implementation of memory management related syscalls, eg., `mmap`, `munmap`, etc. //! Most of these syscalls which are not backed by files are implemented in [`litebox_common_linux::mm`]. +use alloc::collections::BTreeMap; +use alloc::collections::BTreeSet; use litebox::{ mm::linux::{MappingError, PAGE_SIZE, PageRange}, platform::{ - PageManagementProvider, RawConstPointer, RawMutPointer, + PageManagementProvider, RawConstPointer, RawMutPointer, SystemInfoProvider, page_mgmt::{FixedAddressBehavior, MemoryRegionPermissions}, }, }; @@ -17,6 +19,40 @@ use crate::MutPtr; use crate::ShimFS; use crate::Task; +/// ELF program header type for loadable segments. +const PT_LOAD: u32 = 1; +/// ELF type for shared objects and position-independent executables (PIE). +const ET_DYN: u16 = 3; + +/// Per-fd state for the shim's runtime ELF syscall rewriter. +/// +/// Tracks base address and trampoline write cursor for each ELF file that +/// has executable segments mapped via `do_mmap_file()`. +pub(crate) struct ElfPatchState { + /// Whether this file is already pre-patched (trampoline magic found at file tail). + pub pre_patched: bool, + /// For pre-patched binaries: file offset and size of the trampoline data. + pub trampoline_file_offset: u64, + pub trampoline_file_size: usize, + /// Start address of the trampoline region (runtime). + pub trampoline_addr: usize, + /// Current write position within the trampoline (byte offset from `trampoline_addr`). + pub trampoline_cursor: usize, + /// Whether the trampoline region has been allocated. + pub trampoline_mapped: bool, + /// Total number of trampoline bytes currently mapped. + pub trampoline_mapped_len: usize, + /// Whether any runtime-generated stubs were successfully linked from code + /// in this fd to the trampoline. + pub runtime_patches_committed: bool, + /// File offsets of segments that have already been patched. Guards against + /// double-patching if the same segment is re-mapped (e.g. MAP_FIXED). + pub patched_offsets: BTreeSet, +} + +/// Per-process collection of ELF patching state, keyed by fd number. +pub(crate) type ElfPatchCache = BTreeMap; + #[inline] fn align_up(addr: usize, align: usize) -> usize { debug_assert!(align.is_power_of_two()); @@ -76,12 +112,37 @@ impl Task { fd: i32, offset: usize, ) -> Result, MappingError> { - if let Some(cow_result) = + let is_exec = prot.contains(ProtFlags::PROT_EXEC); + + // Perform the normal mmap first (CoW or memcpy fallback). + let result = if let Some(cow_result) = self.try_cow_mmap_file(suggested_addr, len, &prot, &flags, fd, offset) { - return cow_result; + cow_result? + } else { + self.do_mmap_file_memcpy(suggested_addr, len, prot, flags, fd, offset)? + }; + + // Runtime syscall rewriting: patch PROT_EXEC segments in-place. + if is_exec { + let syscall_entry = self.global.platform.get_syscall_entry_point(); + if syscall_entry != 0 + && !self.maybe_patch_exec_segment(result, len, fd, offset, syscall_entry) + { + // Trampoline setup failed for a pre-patched binary whose + // .text already contains JMPs to the trampoline address. + // Continuing would guarantee a SIGSEGV on the first + // rewritten syscall, so fail the mmap instead. + let _ = self.sys_munmap(result, len); + return Err(MappingError::OutOfMemory); + } + } else if !self.global.elf_patch_cache.lock().contains_key(&fd) { + // First mmap for this fd (non-exec): record patch state for later + // exec segment mappings. + self.init_elf_patch_state(fd, result.as_usize()); } - self.do_mmap_file_memcpy(suggested_addr, len, prot, flags, fd, offset) + + Ok(result) } /// Attempt to create a CoW mapping for a file with static backing data. @@ -352,6 +413,522 @@ impl Task { ) -> Result<(), Errno> { litebox_common_linux::mm::sys_madvise(&self.global.pm, addr, len, advice) } + + // ── Runtime ELF syscall patching ───────────────────────────────────── + + /// Initialize ELF patch state for an fd on its first mmap at offset 0. + /// + /// Reads the ELF header to determine the trampoline address (page-aligned + /// end of the highest PT_LOAD segment) and checks the file tail for the + /// trampoline magic to determine if it's pre-patched. + /// + /// x86_64 only: assumes 64-bit ELF layout and program header offsets. + #[allow(clippy::cast_possible_truncation)] + fn init_elf_patch_state(&self, fd: i32, base_addr: usize) { + // Quick check: skip if already initialized. + if self.global.elf_patch_cache.lock().contains_key(&fd) { + return; + } + + // Read the ELF header (first 64 bytes covers both 32-bit and 64-bit). + let mut ehdr_buf = [0u8; 64]; + match self.sys_read(fd, &mut ehdr_buf, Some(0)) { + Ok(n) if n == ehdr_buf.len() => {} + _ => return, // Not readable or short read, skip + } + + // Verify ELF magic + if &ehdr_buf[0..4] != b"\x7fELF" { + return; // Not an ELF file + } + + // Parse as 64-bit ELF (runtime patching is x86-64 only). + let e_phoff = u64::from_le_bytes(ehdr_buf[32..40].try_into().unwrap()) as usize; + let e_phentsize = u16::from_le_bytes(ehdr_buf[54..56].try_into().unwrap()) as usize; + let e_phnum = u16::from_le_bytes(ehdr_buf[56..58].try_into().unwrap()) as usize; + let e_type = u16::from_le_bytes(ehdr_buf[16..18].try_into().unwrap()); + + // Validate e_phentsize: must be at least sizeof(Elf64_Phdr) = 56 bytes, + // otherwise the field accesses (e.g. ph[40..48] for p_memsz) will panic. + if e_phentsize < 56 { + return; + } + + // Read program headers to find max PT_LOAD end + let Some(phdrs_size) = e_phentsize.checked_mul(e_phnum) else { + return; + }; + if phdrs_size == 0 || phdrs_size > 0x10000 { + return; // Sanity check + } + let mut phdrs_buf = alloc::vec![0u8; phdrs_size]; + match self.sys_read(fd, &mut phdrs_buf, Some(e_phoff)) { + Ok(n) if n == phdrs_buf.len() => {} + _ => return, + } + + // Find highest PT_LOAD end (p_vaddr + p_memsz) + let mut max_load_end: u64 = 0; + for i in 0..e_phnum { + let ph = &phdrs_buf[i * e_phentsize..][..e_phentsize]; + let p_type = u32::from_le_bytes(ph[0..4].try_into().unwrap()); + if p_type != PT_LOAD { + continue; + } + let p_vaddr = u64::from_le_bytes(ph[16..24].try_into().unwrap()); + let p_memsz = u64::from_le_bytes(ph[40..48].try_into().unwrap()); + let Some(end) = p_vaddr.checked_add(p_memsz) else { + litebox_util_log::warn!( + p_vaddr:? = p_vaddr, p_memsz:? = p_memsz; + "PT_LOAD p_vaddr + p_memsz overflow, skipping segment" + ); + continue; + }; + if end > max_load_end { + max_load_end = end; + } + } + + if max_load_end == 0 { + return; // No PT_LOAD segments + } + + // For ET_DYN (PIE/shared libs), p_vaddr is relative to base_addr. + // For ET_EXEC, p_vaddr is absolute and base_addr is 0. + let trampoline_vaddr = if e_type == ET_DYN { + // ET_DYN + base_addr + (max_load_end as usize).next_multiple_of(PAGE_SIZE) + } else { + // ET_EXEC + (max_load_end as usize).next_multiple_of(PAGE_SIZE) + }; + + // Check if file is pre-patched by reading the last 32 bytes for magic + let (pre_patched, tramp_file_offset, tramp_vaddr, tramp_file_size) = + self.check_trampoline_magic(fd); + + // For pre-patched binaries, use the vaddr from the header instead. + let trampoline_vaddr = if pre_patched { + if e_type == ET_DYN { + base_addr + tramp_vaddr as usize + } else { + tramp_vaddr as usize + } + } else { + trampoline_vaddr + }; + + // Insert under lock (re-check for races). + let mut cache = self.global.elf_patch_cache.lock(); + cache.entry(fd).or_insert(ElfPatchState { + pre_patched, + trampoline_file_offset: tramp_file_offset, + trampoline_file_size: tramp_file_size as usize, + trampoline_addr: trampoline_vaddr, + trampoline_cursor: 0, + trampoline_mapped: false, + trampoline_mapped_len: 0, + runtime_patches_committed: false, + patched_offsets: BTreeSet::new(), + }); + } + + /// Check if a file has the LITEBOX trampoline magic at its tail. + /// Returns (is_pre_patched, file_offset, vaddr, trampoline_size). + fn check_trampoline_magic(&self, fd: i32) -> (bool, u64, u64, u64) { + const HEADER_SIZE: usize = 32; // TrampolineHeader64: magic(8) + file_offset(8) + vaddr(8) + size(8) + let Ok(stat) = self.sys_fstat(fd) else { + return (false, 0, 0, 0); + }; + let file_size = stat.st_size; + if file_size < HEADER_SIZE { + return (false, 0, 0, 0); + } + let mut tail = [0u8; HEADER_SIZE]; + match self.sys_read(fd, &mut tail, Some(file_size - HEADER_SIZE)) { + Ok(n) if n == HEADER_SIZE => {} + _ => return (false, 0, 0, 0), + } + if &tail[0..8] != litebox_syscall_rewriter::TRAMPOLINE_MAGIC { + return (false, 0, 0, 0); + } + let file_offset = u64::from_le_bytes(tail[8..16].try_into().unwrap()); + let vaddr = u64::from_le_bytes(tail[16..24].try_into().unwrap()); + let trampoline_size = u64::from_le_bytes(tail[24..32].try_into().unwrap()); + (true, file_offset, vaddr, trampoline_size) + } + + /// Patch an executable segment in-place after it has been mapped. + /// + /// For pre-patched binaries: maps the trampoline from the file and writes + /// the syscall entry point. + /// For unpatched binaries: calls `patch_code_segment()` to rewrite syscall + /// instructions and places the generated stubs in the trampoline region. + /// + /// Returns `true` on success or non-fatal skip. Returns `false` when a + /// pre-patched binary's trampoline could not be set up — the caller must + /// fail the mapping because the code already contains JMPs to the + /// trampoline address. + #[allow(clippy::cast_possible_truncation)] + /// Attempt to patch syscall instructions in a newly-mapped PROT_EXEC segment. + /// + /// Returns `true` if it is safe to keep the mapping (patching succeeded or + /// was skipped/best-effort for unpatched binaries), or `false` if the mmap + /// must be failed (trampoline setup failed for a pre-patched binary whose + /// .text already contains jumps to the trampoline address). + fn maybe_patch_exec_segment( + &self, + mapped_addr: MutPtr, + len: usize, + fd: i32, + offset: usize, + syscall_entry: usize, + ) -> bool { + // Initialize patch state if this is the first mmap for this fd. + // Typically the first mapping is at offset 0 (the ELF header), but + // some loaders may map an executable segment at a non-zero offset first. + if !self.global.elf_patch_cache.lock().contains_key(&fd) { + self.init_elf_patch_state(fd, mapped_addr.as_usize()); + } + + // This lock guards the elf_patch_cache and is held for the entire + // patching operation. In practice this is fine because the dynamic + // linker loads shared libraries sequentially. + let mut cache = self.global.elf_patch_cache.lock(); + let Some(state) = cache.get_mut(&fd) else { + return true; // No patch state — not an ELF we're tracking + }; + + if state.pre_patched { + // Pre-patched binary: map the trampoline data from the file. + if !state.trampoline_mapped && state.trampoline_file_size > 0 { + let tramp_addr = state.trampoline_addr; + let tramp_len = align_up(state.trampoline_file_size, PAGE_SIZE); + + // Allocate RW region at the trampoline address. Use MAP_FIXED + // because the code already contains JMPs to this exact address + // and we MUST map here. The region may already be reserved as + // PROT_NONE by the ElfLoader's reserve() call, which would + // cause MAP_FIXED_NOREPLACE to fail with EEXIST. + let alloc_result = self.do_mmap_anonymous( + Some(tramp_addr), + tramp_len, + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + MapFlags::MAP_ANONYMOUS | MapFlags::MAP_PRIVATE | MapFlags::MAP_FIXED, + ); + let Ok(alloc_ptr) = alloc_result else { + return false; + }; + let actual_addr = alloc_ptr.as_usize(); + if actual_addr != tramp_addr { + let _ = self.sys_munmap(MutPtr::::from_usize(actual_addr), tramp_len); + return false; + } + + // Read trampoline data from the file. + let mut tramp_data = alloc::vec![0u8; state.trampoline_file_size]; + let file_off = state.trampoline_file_offset as usize; + let tramp_ptr = MutPtr::::from_usize(tramp_addr); + match self.sys_read(fd, &mut tramp_data, Some(file_off)) { + Ok(n) if n == tramp_data.len() => {} + _ => { + let _ = self.sys_munmap(tramp_ptr, tramp_len); + return false; + } + } + + // Write syscall entry point to the first 8 bytes. + if tramp_data.len() >= 8 { + tramp_data[..8].copy_from_slice(&syscall_entry.to_le_bytes()); + } + + // Write to the mapped region. + if tramp_ptr.copy_from_slice(0, &tramp_data).is_none() { + let _ = self.sys_munmap(tramp_ptr, tramp_len); + return false; + } + + // Protect as RX immediately. + if self + .sys_mprotect( + tramp_ptr, + tramp_len, + ProtFlags::PROT_READ | ProtFlags::PROT_EXEC, + ) + .is_err() + { + let _ = self.sys_munmap(tramp_ptr, tramp_len); + return false; + } + + state.trampoline_mapped = true; + state.trampoline_mapped_len = tramp_len; + } + return true; + } + + // ── Runtime patching path (unpatched binaries) ─────────────── + + // Allocate the trampoline region if not yet done. + let addr_usize = mapped_addr.as_usize(); + if !state.trampoline_mapped { + let tramp_addr = state.trampoline_addr; + + // Try MAP_FIXED first — works when ensure_space_after reserved + // PROT_NONE space (shared libraries). Falls back to a hint-based + // allocation for the ElfLoader path where no headroom is reserved. + let actual_addr = self + .do_mmap_anonymous( + Some(tramp_addr), + PAGE_SIZE, + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + MapFlags::MAP_ANONYMOUS | MapFlags::MAP_PRIVATE | MapFlags::MAP_FIXED_NOREPLACE, + ) + .or_else(|_| { + self.do_mmap_anonymous( + Some(tramp_addr), + PAGE_SIZE, + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + MapFlags::MAP_ANONYMOUS | MapFlags::MAP_PRIVATE, + ) + }); + let Ok(actual_addr_ptr) = actual_addr else { + litebox_util_log::warn!("failed to allocate trampoline region"); + return true; + }; + let actual_addr = actual_addr_ptr.as_usize(); + + // Verify the trampoline is within JMP rel32 range (+-2GB) of the + // entire code segment, not just its start. + let far_end = addr_usize.saturating_add(len); + let distance = actual_addr + .abs_diff(addr_usize) + .max(actual_addr.abs_diff(far_end)); + if distance > 0x7FFF_0000 { + litebox_util_log::warn!( + distance:? = distance; + "trampoline too far from code segment, skipping patching" + ); + let _ = self.sys_munmap(MutPtr::::from_usize(actual_addr), PAGE_SIZE); + return true; + } + + state.trampoline_addr = actual_addr; + + // Write the 8-byte syscall entry point at the start. + let entry_ptr = MutPtr::::from_usize(actual_addr); + if entry_ptr + .copy_from_slice(0, &syscall_entry.to_le_bytes()) + .is_none() + { + litebox_util_log::warn!("failed to write syscall entry point to trampoline"); + let _ = self.sys_munmap(MutPtr::::from_usize(actual_addr), PAGE_SIZE); + return true; + } + state.trampoline_cursor = 8; // stubs start after the 8-byte entry + state.trampoline_mapped = true; + state.trampoline_mapped_len = PAGE_SIZE; + } + + // Guard against double-patching the same segment (e.g. MAP_FIXED + // re-mapping). Feeding already-rewritten JMP instructions back into + // the rewriter would corrupt code. + if !state.patched_offsets.insert(offset) { + return true; + } + + let restore_trampoline_rx = |task: &Self, state: &ElfPatchState| { + if state.trampoline_mapped_len > 0 { + let _ = task.sys_mprotect( + MutPtr::::from_usize(state.trampoline_addr), + state.trampoline_mapped_len, + ProtFlags::PROT_READ | ProtFlags::PROT_EXEC, + ); + } + }; + + // Make the trampoline RW for writing stubs. + if state.trampoline_mapped_len > 0 + && self + .sys_mprotect( + MutPtr::::from_usize(state.trampoline_addr), + state.trampoline_mapped_len, + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + ) + .is_err() + { + litebox_util_log::warn!("failed to mprotect trampoline to RW"); + return true; + } + if self + .sys_mprotect( + mapped_addr, + len, + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + ) + .is_err() + { + litebox_util_log::warn!("failed to mprotect code segment to RW for patching"); + restore_trampoline_rx(self, state); + return true; + } + + // Read the mapped code into a buffer, patch it, write back. + let Some(code_owned) = mapped_addr.to_owned_slice(len) else { + litebox_util_log::warn!("failed to read code segment for patching"); + let _ = self.sys_mprotect( + mapped_addr, + len, + ProtFlags::PROT_READ | ProtFlags::PROT_EXEC, + ); + restore_trampoline_rx(self, state); + return true; + }; + let mut code_buf = code_owned.into_vec(); + let original_code = code_buf.clone(); + + let code_vaddr = addr_usize as u64; + let trampoline_write_vaddr = (state.trampoline_addr + state.trampoline_cursor) as u64; + let syscall_entry_addr = state.trampoline_addr as u64; + + let patch_result = litebox_syscall_rewriter::patch_code_segment( + &mut code_buf, + code_vaddr, + trampoline_write_vaddr, + syscall_entry_addr, + ); + let patch_result = match patch_result { + Ok((stubs, skipped_addrs)) => { + if !skipped_addrs.is_empty() { + litebox_util_log::warn!( + count:? = skipped_addrs.len(), addrs:? = skipped_addrs; + "syscall instruction(s) could not be patched" + ); + } + Ok(stubs) + } + Err(e) => Err(e), + }; + match patch_result { + Ok(stubs) if !stubs.is_empty() => { + let Some(new_cursor) = state.trampoline_cursor.checked_add(stubs.len()) else { + litebox_util_log::warn!("trampoline cursor overflow"); + let _ = self.sys_mprotect( + mapped_addr, + len, + ProtFlags::PROT_READ | ProtFlags::PROT_EXEC, + ); + restore_trampoline_rx(self, state); + return true; + }; + let tramp_pages_needed = align_up(new_cursor, PAGE_SIZE); + if tramp_pages_needed > state.trampoline_mapped_len { + let extra_start = state.trampoline_addr + state.trampoline_mapped_len; + let extra_len = tramp_pages_needed - state.trampoline_mapped_len; + if self + .do_mmap_anonymous( + Some(extra_start), + extra_len, + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + MapFlags::MAP_ANONYMOUS + | MapFlags::MAP_PRIVATE + | MapFlags::MAP_FIXED_NOREPLACE, + ) + .is_err() + { + litebox_util_log::warn!("failed to expand trampoline region"); + let _ = self.sys_mprotect( + mapped_addr, + len, + ProtFlags::PROT_READ | ProtFlags::PROT_EXEC, + ); + restore_trampoline_rx(self, state); + return true; + } + state.trampoline_mapped_len = tramp_pages_needed; + } + + // Write stubs before patching the code so rewritten jumps + // never target an uninitialized trampoline. + let tramp_write_ptr = + MutPtr::::from_usize(state.trampoline_addr + state.trampoline_cursor); + if tramp_write_ptr.copy_from_slice(0, &stubs).is_none() { + litebox_util_log::warn!("failed to write trampoline stubs"); + let _ = self.sys_mprotect( + mapped_addr, + len, + ProtFlags::PROT_READ | ProtFlags::PROT_EXEC, + ); + restore_trampoline_rx(self, state); + return true; + } + + // Write patched code back to the mapped region. + if mapped_addr.copy_from_slice(0, &code_buf).is_none() { + litebox_util_log::warn!("failed to write patched code back to code segment"); + let _ = mapped_addr.copy_from_slice(0, &original_code); + let _ = self.sys_mprotect( + mapped_addr, + len, + ProtFlags::PROT_READ | ProtFlags::PROT_EXEC, + ); + restore_trampoline_rx(self, state); + return true; + } + state.trampoline_cursor = new_cursor; + state.runtime_patches_committed = true; + } + Ok(_) => { + // No syscalls found — no patching needed. + } + Err(e) => { + litebox_util_log::warn!(err:? = e; "failed to patch code segment"); + let _ = self.sys_mprotect( + mapped_addr, + len, + ProtFlags::PROT_READ | ProtFlags::PROT_EXEC, + ); + restore_trampoline_rx(self, state); + return true; + } + } + + // Restore the code segment to RX. + let _ = self.sys_mprotect( + mapped_addr, + len, + ProtFlags::PROT_READ | ProtFlags::PROT_EXEC, + ); + restore_trampoline_rx(self, state); + true + } + + /// Finalize the ELF patching state for `fd`. + /// + /// If the fd has a trampoline region that was allocated (RW), mprotect it + /// to RX so the trampoline stubs become executable and non-writable. + /// The cache entry is removed regardless. + pub(crate) fn finalize_elf_patch(&self, fd: i32) { + let state = self.global.elf_patch_cache.lock().remove(&fd); + if let Some(state) = state + && state.trampoline_mapped + && !state.pre_patched + { + let tramp_len = state.trampoline_mapped_len; + if tramp_len > 0 { + if !state.runtime_patches_committed { + let _ = + self.sys_munmap(MutPtr::::from_usize(state.trampoline_addr), tramp_len); + return; + } + let _ = self.sys_mprotect( + MutPtr::::from_usize(state.trampoline_addr), + tramp_len, + ProtFlags::PROT_READ | ProtFlags::PROT_EXEC, + ); + } + } + } } #[cfg(test)] diff --git a/litebox_shim_optee/src/loader/elf.rs b/litebox_shim_optee/src/loader/elf.rs index e75bbf6c7..ecef135f9 100644 --- a/litebox_shim_optee/src/loader/elf.rs +++ b/litebox_shim_optee/src/loader/elf.rs @@ -214,10 +214,10 @@ impl<'a> ElfLoader<'a> { } let task = self.main.file.task; let global = &task.global; - let ldelf_info = self - .main - .parsed - .load(&mut self.main.file, &mut &*global.platform)?; + let ldelf_info = + self.main + .parsed + .load(&mut self.main.file, &mut &*global.platform, None)?; let mut ta_stack = crate::loader::ta_stack::allocate_stack(task, None).ok_or( ElfLoaderError::MappingError(litebox::mm::linux::MappingError::OutOfMemory), diff --git a/litebox_syscall_rewriter/src/lib.rs b/litebox_syscall_rewriter/src/lib.rs index e45437691..a9ba81a09 100644 --- a/litebox_syscall_rewriter/src/lib.rs +++ b/litebox_syscall_rewriter/src/lib.rs @@ -724,6 +724,55 @@ fn rel32_bytes(target: u64, base: u64, context: &'static str) -> Result<[u8; 4]> Ok(disp.to_le_bytes()) } +/// Patch a single mapped code segment in-place, returning trampoline stubs and +/// the addresses of any syscall instructions that could not be patched +/// (replaced with `ICEBP; HLT` so they trap instead of escaping to the host +/// kernel). +/// +/// This is the runtime counterpart to [`hook_syscalls_in_elf`]. Instead of +/// processing a whole ELF file, it operates on a single already-mapped code +/// region — the caller is responsible for making the region writable before +/// calling and restoring permissions afterwards. +/// +/// # Returns +/// +/// `(trampoline_stubs, skipped_addrs)`. The caller must copy the stubs to +/// `trampoline_write_vaddr`. Returns empty vecs if no syscall instructions +/// are found in `code`. +pub fn patch_code_segment( + code: &mut [u8], + code_vaddr: u64, + trampoline_write_vaddr: u64, + syscall_entry_addr: u64, +) -> Result<(Vec, Vec)> { + // Build control-transfer targets for this segment. + let instructions = decode_section_instructions(Arch::X86_64, code, code_vaddr)?; + let mut control_transfer_targets = BTreeSet::new(); + for inst in &instructions { + let target = inst.near_branch_target(); + if target != 0 { + control_transfer_targets.insert(target); + } + } + + let mut trampoline_data = Vec::new(); + match hook_syscalls_in_section( + Arch::X86_64, + &control_transfer_targets, + code_vaddr, + code, + trampoline_write_vaddr, + syscall_entry_addr, + None, + &mut trampoline_data, + ) { + Ok(skipped_addrs) => Ok((trampoline_data, skipped_addrs)), + Err(InternalError::NoSyscallInstructionsFound) => Ok((Vec::new(), Vec::new())), + Err(InternalError::Public(e)) => Err(e), + Err(e) => unreachable!("unexpected internal error: {e:?}"), + } +} + fn find_addr_for_trampoline_code(file: &object::File<'_>) -> Result { // Find the highest virtual address among all PT_LOAD segments let max_virtual_addr = match file { From 2225fd26183fb502df8403174a33b80f9897675d Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Sat, 25 Apr 2026 14:43:37 -0700 Subject: [PATCH 2/4] Remove duplicate doc comment on maybe_patch_exec_segment --- litebox_common_linux/src/loader.rs | 3 +-- litebox_shim_linux/src/syscalls/mm.rs | 9 +++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/litebox_common_linux/src/loader.rs b/litebox_common_linux/src/loader.rs index 46066c4b8..c61bfc78b 100644 --- a/litebox_common_linux/src/loader.rs +++ b/litebox_common_linux/src/loader.rs @@ -301,8 +301,7 @@ impl ElfParsedFile { }; // trampoline_size == 0 means the rewriter checked this binary and found - // no syscall instructions. The magic header acts as a "checked" marker so - // the runtime skips eager code-segment patching. No trampoline to map. + // no syscall instructions. if trampoline_size == 0 { return Ok(()); } diff --git a/litebox_shim_linux/src/syscalls/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index d2152e6f5..80195eded 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -28,6 +28,9 @@ const ET_DYN: u16 = 3; /// /// Tracks base address and trampoline write cursor for each ELF file that /// has executable segments mapped via `do_mmap_file()`. +/// +/// TODO (wdcui): This Elf patching inside the shim is not a perfect solution. +/// We will revisit it in the future. pub(crate) struct ElfPatchState { /// Whether this file is already pre-patched (trampoline magic found at file tail). pub pre_patched: bool, @@ -570,12 +573,6 @@ impl Task { /// fail the mapping because the code already contains JMPs to the /// trampoline address. #[allow(clippy::cast_possible_truncation)] - /// Attempt to patch syscall instructions in a newly-mapped PROT_EXEC segment. - /// - /// Returns `true` if it is safe to keep the mapping (patching succeeded or - /// was skipped/best-effort for unpatched binaries), or `false` if the mmap - /// must be failed (trampoline setup failed for a pre-patched binary whose - /// .text already contains jumps to the trampoline address). fn maybe_patch_exec_segment( &self, mapped_addr: MutPtr, From 4b1b4aa136d4efc0a09e4b603b4767531ec52117 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Sat, 25 Apr 2026 14:48:39 -0700 Subject: [PATCH 3/4] Handle unpatched binaries in optee shim ELF loader Mirror the linux shim's parse_trampoline/load_mapped pattern: tolerate UnpatchedBinary errors and reserve trampoline space for runtime patching. --- litebox_shim_optee/src/loader/elf.rs | 35 ++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/litebox_shim_optee/src/loader/elf.rs b/litebox_shim_optee/src/loader/elf.rs index ecef135f9..5bab95963 100644 --- a/litebox_shim_optee/src/loader/elf.rs +++ b/litebox_shim_optee/src/loader/elf.rs @@ -195,9 +195,37 @@ impl<'a> FileAndParsed<'a> { let file = ElfFileInMemory::new(task, elf_buf); let mut parsed = litebox_common_linux::loader::ElfParsedFile::parse(&mut &file) .map_err(ElfLoaderError::ParseError)?; - parsed.parse_trampoline(&mut &file, task.global.platform.get_syscall_entry_point())?; + + let syscall_entry_point = task.global.platform.get_syscall_entry_point(); + + // Try to parse an embedded trampoline. For pre-patched binaries this + // succeeds and load_trampoline() will map it. For unpatched binaries + // (UnpatchedBinary error), the runtime patching during mmap will patch + // code segments as they are mapped. + if syscall_entry_point != 0 { + match parsed.parse_trampoline(&mut &file, syscall_entry_point) { + Ok(()) | Err(litebox_common_linux::loader::ElfParseError::UnpatchedBinary) => {} + Err(e) => return Err(ElfLoaderError::ParseError(e)), + } + } + Ok(Self { file, parsed }) } + + /// Load the ELF into guest memory. + fn load_mapped( + &mut self, + platform: &(impl litebox::platform::RawPointerProvider + litebox::platform::SystemInfoProvider), + ) -> Result { + let syscall_entry_point = self.file.task.global.platform.get_syscall_entry_point(); + let reserve = if syscall_entry_point != 0 && !self.parsed.has_trampoline() { + Some(litebox::mm::linux::DEFAULT_RESERVED_SPACE_SIZE) + } else { + None + }; + let result = self.parsed.load(&mut self.file, &mut &*platform, reserve); + Ok(result?) + } } impl<'a> ElfLoader<'a> { @@ -214,10 +242,7 @@ impl<'a> ElfLoader<'a> { } let task = self.main.file.task; let global = &task.global; - let ldelf_info = - self.main - .parsed - .load(&mut self.main.file, &mut &*global.platform, None)?; + let ldelf_info = self.main.load_mapped(global.platform)?; let mut ta_stack = crate::loader::ta_stack::allocate_stack(task, None).ok_or( ElfLoaderError::MappingError(litebox::mm::linux::MappingError::OutOfMemory), From 78d0cb6b89b8be86cd4671f7521e0ff35d8909d1 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Sat, 25 Apr 2026 14:52:14 -0700 Subject: [PATCH 4/4] update comment --- litebox_syscall_rewriter/src/lib.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/litebox_syscall_rewriter/src/lib.rs b/litebox_syscall_rewriter/src/lib.rs index a9ba81a09..d79f5796a 100644 --- a/litebox_syscall_rewriter/src/lib.rs +++ b/litebox_syscall_rewriter/src/lib.rs @@ -724,11 +724,6 @@ fn rel32_bytes(target: u64, base: u64, context: &'static str) -> Result<[u8; 4]> Ok(disp.to_le_bytes()) } -/// Patch a single mapped code segment in-place, returning trampoline stubs and -/// the addresses of any syscall instructions that could not be patched -/// (replaced with `ICEBP; HLT` so they trap instead of escaping to the host -/// kernel). -/// /// This is the runtime counterpart to [`hook_syscalls_in_elf`]. Instead of /// processing a whole ELF file, it operates on a single already-mapped code /// region — the caller is responsible for making the region writable before