From ec58ba70e147590025371cd21baf7b28373a0104 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 1 Apr 2026 22:55:57 -0700 Subject: [PATCH 01/14] Trampoline format: redzone reservation, R11 restart, RIP-relative re-encoding New trampoline format changes for the syscall rewriter: Rewriter (litebox_syscall_rewriter): - Add redzone reservation (LEA RSP,[RSP-0x80]) before syscall callback entry on x86-64, allowing the callback to use the 128-byte red zone - Add R11 restart address (LEA R11,[RIP+disp32]) pointing back to the call-site JMP, enabling SA_RESTART signal re-execution - Re-encode RIP-relative memory operands in pre-syscall instructions when they are copied to the trampoline, using iced_x86::Encoder at the trampoline IP so displacements remain correct - Guard post-syscall instructions with RIP-relative operands by delegating to hook_syscall_before_and_after instead of raw-copying - Append header-only marker (trampoline_size=0) when no syscall instructions are found, so the loader can distinguish checked binaries from unpatched ones - Add 5 inline unit tests for Bun detection and RIP-relative encoding Loader (litebox_common_linux): - Handle trampoline_size==0 as a valid no-op (checked, no syscalls) - Add UnpatchedBinary error variant for binaries missing the magic - Add has_trampoline() accessor Platform/shim (litebox_platform_linux_userland): - Add saved_r11 TLS slot and save R11 on syscall callback entry - Add syscall_callback_redzone entry point that undoes red zone reservation before saving registers - Return syscall_callback_redzone from get_syscall_entry_point() Shim loader (litebox_shim_linux): - Treat UnpatchedBinary as non-fatal in parse_trampoline calls, allowing unpatched binaries to load without a trampoline --- litebox_common_linux/src/loader.rs | 22 +- litebox_platform_linux_userland/src/lib.rs | 34 +- litebox_shim_linux/src/loader/elf.rs | 9 +- litebox_syscall_rewriter/src/lib.rs | 372 +++++++++++++++++- .../snapshots/snapshot_tests__hello-diff.snap | 216 +++++----- 5 files changed, 520 insertions(+), 133 deletions(-) diff --git a/litebox_common_linux/src/loader.rs b/litebox_common_linux/src/loader.rs index 3ae61266e..8d061b93d 100644 --- a/litebox_common_linux/src/loader.rs +++ b/litebox_common_linux/src/loader.rs @@ -128,6 +128,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")] @@ -141,6 +143,7 @@ impl> From> for Errno { | ElfParseError::BadFormat | ElfParseError::BadTrampoline | ElfParseError::BadTrampolineVersion + | ElfParseError::UnpatchedBinary | ElfParseError::BadInterp | ElfParseError::UnsupportedType => Errno::ENOEXEC, ElfParseError::Io(err) => err.into(), @@ -218,6 +221,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). @@ -251,7 +259,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 @@ -267,8 +276,9 @@ 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. When using the syscall rewriter backend + // (syscall_entry_point != 0), all binaries must be patched. + return Err(ElfParseError::UnpatchedBinary); } let (file_offset, vaddr, trampoline_size) = if cfg!(target_pointer_width = "64") { @@ -293,9 +303,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) diff --git a/litebox_platform_linux_userland/src/lib.rs b/litebox_platform_linux_userland/src/lib.rs index c3e60a83a..7babef6ca 100644 --- a/litebox_platform_linux_userland/src/lib.rs +++ b/litebox_platform_linux_userland/src/lib.rs @@ -537,6 +537,8 @@ core::arch::global_asm!( " .section .tbss .align 8 +saved_r11: + .quad 0 scratch: .quad 0 host_sp: @@ -651,6 +653,10 @@ syscall_callback: // expectations of `interrupt_signal_handler`. mov BYTE PTR gs:in_guest@tpoff, 0 + // Save guest R11 (syscall call-site address from rewriter trampoline) + // before it is clobbered by the fsbase/gsbase save sequence below. + mov gs:saved_r11@tpoff, r11 + // Restore host fs base. rdfsbase r11 mov gs:guest_fsbase@tpoff, r11 @@ -660,6 +666,25 @@ syscall_callback: // Switch to the top of the guest context. mov r11, rsp mov rsp, fs:guest_context_top@tpoff + jmp .Lsyscall_save_regs + + .globl syscall_callback_redzone +syscall_callback_redzone: + // Same as syscall_callback, but the trampoline has already reserved + // 128 bytes below RSP to protect the SysV red zone. + mov BYTE PTR gs:in_guest@tpoff, 0 + mov gs:saved_r11@tpoff, r11 + rdfsbase r11 + mov gs:guest_fsbase@tpoff, r11 + rdgsbase r11 + wrfsbase r11 + + // The trampoline lowered RSP by 128 bytes with LEA, so recover the + // architectural guest stack pointer before saving pt_regs. + lea r11, [rsp + 128] + mov rsp, fs:guest_context_top@tpoff + +.Lsyscall_save_regs: // TODO: save float and vector registers (xsave or fxsave) // Save caller-saved registers @@ -678,7 +703,7 @@ syscall_callback: push r8 // pt_regs->r8 push r9 // pt_regs->r9 push r10 // pt_regs->r10 - push [rsp + 88] // pt_regs->r11 = rflags + push QWORD PTR gs:saved_r11@tpoff // pt_regs->r11 (syscall call-site from rewriter) push rbx // pt_regs->bx push rbp // pt_regs->bp push r12 // pt_regs->r12 @@ -1967,6 +1992,7 @@ impl litebox::platform::StdioProvider for LinuxUserland { unsafe extern "C" { // Defined in asm blocks above fn syscall_callback() -> isize; + fn syscall_callback_redzone() -> isize; fn exception_callback(); fn interrupt_callback(); fn switch_to_guest_start(); @@ -2047,7 +2073,7 @@ impl ThreadContext<'_> { impl litebox::platform::SystemInfoProvider for LinuxUserland { fn get_syscall_entry_point(&self) -> usize { - syscall_callback as *const () as usize + syscall_callback_redzone as *const () as usize } fn get_vdso_address(&self) -> Option { @@ -2714,7 +2740,9 @@ unsafe fn interrupt_signal_handler( // FUTURE: handle trampoline code, too. This is somewhat less important // because it's probably fine for the shim to observe a guest context that // is inside the trampoline. - if ip == syscall_callback as *const () as usize { + if ip == syscall_callback as *const () as usize + || ip == syscall_callback_redzone as *const () as usize + { // No need to clear `in_guest` or set interrupt; the syscall handler will // clear `in_guest` and call into the shim. return; diff --git a/litebox_shim_linux/src/loader/elf.rs b/litebox_shim_linux/src/loader/elf.rs index 0d62030a8..63a9a5d1e 100644 --- a/litebox_shim_linux/src/loader/elf.rs +++ b/litebox_shim_linux/src/loader/elf.rs @@ -10,12 +10,12 @@ use litebox::{ platform::{RawConstPointer as _, SystemInfoProvider as _}, utils::{ReinterpretSignedExt, TruncateExt}, }; -use litebox_common_linux::{MapFlags, errno::Errno, loader::ElfParsedFile}; +use litebox_common_linux::{errno::Errno, loader::ElfParsedFile, MapFlags}; use thiserror::Error; use crate::{ - MutPtr, loader::auxv::{AuxKey, AuxVec}, + MutPtr, }; use super::stack::UserStack; @@ -172,7 +172,10 @@ 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())?; + match parsed.parse_trampoline(&mut &file, task.global.platform.get_syscall_entry_point()) { + Ok(()) | Err(litebox_common_linux::loader::ElfParseError::UnpatchedBinary) => {} + Err(err) => return Err(ElfLoaderError::ParseError(err)), + } Ok(Self { file, parsed }) } } diff --git a/litebox_syscall_rewriter/src/lib.rs b/litebox_syscall_rewriter/src/lib.rs index ebe4b74a4..7a0ff6eaa 100644 --- a/litebox_syscall_rewriter/src/lib.rs +++ b/litebox_syscall_rewriter/src/lib.rs @@ -199,6 +199,7 @@ pub fn hook_syscalls_in_elf( } // Patch syscalls in-place in buf let mut skipped_addrs = Vec::new(); + let mut syscall_insns_found = false; for s in &text_sections { let section_data = section_slice_mut(buf, s)?; match hook_syscalls_in_section( @@ -211,12 +212,62 @@ pub fn hook_syscalls_in_elf( dl_sysinfo_int80, &mut trampoline_data, ) { - Ok(addrs) => skipped_addrs.extend(addrs), + Ok(addrs) => { + skipped_addrs.extend(addrs); + syscall_insns_found = true; + } Err(Error::NoSyscallInstructionsFound) => {} Err(e) => return Err(e), } } + if !syscall_insns_found { + // No syscall instructions found. Append a header-only marker so the + // loader can distinguish "checked by rewriter, nothing to patch" from + // "never processed." The trampoline_size=0 sentinel tells the loader + // to skip trampoline mapping entirely. + // Use the original input (not `buf`) to avoid emitting the phdr + // alignment fixup that was only needed for the `object` crate parser. + let mut out = input_binary.to_vec(); + if arch == Arch::X86_64 { + let header = TrampolineHeader64 { + magic: *TRAMPOLINE_MAGIC, + file_offset: 0, + vaddr: 0, + trampoline_size: 0, + }; + out.extend_from_slice(header.as_bytes()); + } else { + let header = TrampolineHeader32 { + magic: *TRAMPOLINE_MAGIC, + file_offset: 0, + vaddr: 0, + trampoline_size: 0, + }; + out.extend_from_slice(header.as_bytes()); + } + return Ok(out); + } + + // Patch fork → vfork: overwrite the first bytes of __libc_fork with a + // JMP to __libc_vfork. This prevents glibc's fork wrapper from running + // post-fork handlers that corrupt shared state under vfork semantics. + if let Some((fork_file_offset, fork_patch_end, rel32)) = fork_to_vfork_patch { + #[allow(clippy::cast_possible_truncation)] + let off = fork_file_offset as usize; + #[allow(clippy::cast_possible_truncation)] + let patch_end = fork_patch_end as usize; + if off + 5 <= buf.len() && patch_end <= buf.len() && off + 5 <= patch_end { + buf[off] = 0xE9; // JMP rel32 + buf[off + 1..off + 5].copy_from_slice(&rel32.to_le_bytes()); + } else { + return Err(Error::ParseError(format!( + "fork→vfork patch range {off:#x}..{patch_end:#x} is invalid for buffer length {}", + buf.len(), + ))); + } + } + // Build output: [patched ELF][padding to page boundary][trampoline code][header] let mut out = buf.to_vec(); let remain = out.len() % 0x1000; @@ -434,22 +485,94 @@ fn hook_syscalls_in_section( let replace_start = replace_start.unwrap(); let replace_len = usize::try_from(replace_end - replace_start).unwrap(); + let copied_presyscall_insts_have_ip_rel_mem = arch == Arch::X86_64 + && instruction_slice_has_ip_rel_memory_operand( + instructions + .iter() + .take(i) + .skip_while(|prev_inst| prev_inst.ip() < replace_start), + ); + let target_addr = checked_add_u64( trampoline_base_addr, trampoline_data.len() as u64, "syscall trampoline target", )?; - // Copy the original instructions to the trampoline + // Copy the pre-syscall instructions to the trampoline. + // When any instruction has a RIP-relative memory operand, we + // re-encode them so the displacement targets the same absolute + // address from the new trampoline location. if replace_start < inst.ip() { - trampoline_data.extend_from_slice( - §ion_data[usize::try_from(replace_start - section_base_addr).unwrap() - ..usize::try_from(inst.ip() - section_base_addr).unwrap()], - ); + if copied_presyscall_insts_have_ip_rel_mem { + let mut reencoded = Vec::new(); + let mut ok = true; + let mut encoder = iced_x86::Encoder::new(64); + for pre_inst in instructions + .iter() + .take(i) + .skip_while(|p| p.ip() < replace_start) + { + let tramp_ip = target_addr + reencoded.len() as u64; + if encoder.encode(pre_inst, tramp_ip).is_err() { + ok = false; + break; + } + let bytes = encoder.take_buffer(); + if bytes.len() != pre_inst.len() { + ok = false; + break; + } + reencoded.extend_from_slice(&bytes); + } + if !ok { + match hook_syscall_and_after( + arch, + control_transfer_targets, + section_base_addr, + section_data, + trampoline_base_addr, + syscall_entry_addr, + trampoline_data, + &instructions, + i, + ) { + Ok(()) => {} + Err(Error::InsufficientBytesBeforeOrAfter(_)) => { + replace_with_ud2(section_data, section_base_addr, inst); + skipped_addrs.push(inst.ip()); + } + Err(e) => return Err(e), + } + continue; + } + trampoline_data.extend_from_slice(&reencoded); + } else { + trampoline_data.extend_from_slice( + §ion_data[usize::try_from(replace_start - section_base_addr).unwrap() + ..usize::try_from(inst.ip() - section_base_addr).unwrap()], + ); + } } let return_addr = inst.next_ip(); if arch == Arch::X86_64 { + // Reserve the SysV red zone before entering the shim so async + // guest signal delivery / interrupt handling cannot clobber + // stack locals parked below the architectural RSP. + // LEA RSP, [RSP - 0x80] = 48 8D 64 24 80 + trampoline_data.extend_from_slice(&[0x48, 0x8D, 0x64, 0x24, 0x80]); + + // Put the address of the original JMP (call-site) into R11 so + // that SA_RESTART can rewind ctx.rip to re-enter the trampoline. + // The real `syscall` instruction clobbers R11 with RFLAGS, so + // this register is free from the guest's perspective. + // LEA R11, [RIP + disp32] = 4C 8D 1D + let r11_disp = i64::try_from(replace_start).unwrap() + - i64::try_from(trampoline_base_addr + trampoline_data.len() as u64 + 7).unwrap(); + trampoline_data.extend_from_slice(&[0x4C, 0x8D, 0x1D]); // LEA R11, [RIP + disp32] + trampoline_data.extend_from_slice(&(i32::try_from(r11_disp).unwrap().to_le_bytes())); + // Put jump back location into rcx. let jmp_back_base = checked_add_u64( trampoline_base_addr, @@ -483,8 +606,8 @@ fn hook_syscalls_in_section( trampoline_data.extend_from_slice(&[0xE8, 0x0, 0x0, 0x0, 0x0]); // CALL next instruction trampoline_data.push(0x58); // POP EAX (effectively store IP in EAX) trampoline_data.extend_from_slice(&[0xFF, 0x90]); // CALL [EAX + offset] - // EAX = trampoline_base_addr + (trampoline_data.len() - 3) - // We want: EAX + offset = syscall_entry_addr + // EAX = trampoline_base_addr + (trampoline_data.len() - 3) + // We want: EAX + offset = syscall_entry_addr let call_base = checked_add_u64( trampoline_base_addr, trampoline_data.len() as u64 - 3, @@ -591,8 +714,8 @@ fn fixup_phdr_alignment(buf: &mut [u8]) { return; }; - if old_end > buf.len() || new_end > buf.len() { - return; // corrupt phdr table or not enough room + if new_end > buf.len() { + return; // not enough room } // Only relocate when the overwritten bytes are padding. Otherwise this would corrupt the file @@ -638,6 +761,94 @@ fn fixup_phdr_alignment(buf: &mut [u8]) { } } +/// Find fork and vfork symbols in the ELF and compute the patch needed to +/// redirect fork -> vfork. Returns `Some((fork_file_offset, jmp_rel32))` if +/// both symbols are found, or `None` if this binary doesn't export fork. +fn find_fork_vfork_patch( + file: &object::File<'_>, + text_sections: &[TextSectionInfo], +) -> Option<(u64, u64, i32)> { + use object::ObjectSymbol as _; + + let mut fork_vaddr = None; + let mut vfork_vaddr = None; + + // Restrict this rewrite to libc-specific symbols. Plain `fork`/`vfork` names may belong to + // arbitrary DSOs or user code, and retargeting them would silently change unrelated behavior. + for sym in file.dynamic_symbols() { + if sym.kind() != object::SymbolKind::Text { + continue; + } + let Ok(name) = sym.name() else { continue }; + match name { + "__libc_fork" if fork_vaddr.is_none() => { + fork_vaddr = Some(sym.address()); + } + "__libc_vfork" | "__vfork" if vfork_vaddr.is_none() => { + vfork_vaddr = Some(sym.address()); + } + _ => {} + } + } + + for sym in file.symbols() { + if sym.kind() != object::SymbolKind::Text { + continue; + } + let Ok(name) = sym.name() else { continue }; + match name { + "__libc_fork" if fork_vaddr.is_none() => { + fork_vaddr = Some(sym.address()); + } + "__libc_vfork" | "__vfork" if vfork_vaddr.is_none() => { + vfork_vaddr = Some(sym.address()); + } + _ => {} + } + } + + let fork_vaddr = fork_vaddr?; + let vfork_vaddr = vfork_vaddr?; + if fork_vaddr == 0 || vfork_vaddr == 0 { + return None; + } + + // Convert fork's vaddr to a file offset using the text sections. + let (fork_file_offset, fork_patch_end) = text_sections.iter().find_map(|s| { + let section_end = s.vaddr + s.size; + if fork_vaddr >= s.vaddr + && fork_vaddr < section_end + && fork_vaddr + .checked_add(5) + .is_some_and(|end| end <= section_end) + { + let file_offset = s.file_offset + (fork_vaddr - s.vaddr); + let file_end = s.file_offset + s.size; + Some((file_offset, file_end)) + } else { + None + } + })?; + + // Compute the relative offset for a JMP rel32 instruction. + // JMP rel32 encodes: target = rip_after_jmp + rel32 + // rip_after_jmp = fork_vaddr + 5 (size of JMP rel32 instruction) + let rel32 = i64::try_from(vfork_vaddr) + .ok()? + .checked_sub(i64::try_from(fork_vaddr).ok()? + 5)?; + let rel32 = i32::try_from(rel32).ok()?; + + Some((fork_file_offset, fork_patch_end, rel32)) +} + +/// Check if the input binary has the Bun footer marker near the end. +fn has_bun_footer_marker(input_binary: &[u8]) -> bool { + let window_len = input_binary.len().min(256); + input_binary[input_binary.len().saturating_sub(window_len)..] + .windows(BUN_FOOTER_MARKER.len()) + .any(|window| window == BUN_FOOTER_MARKER) +} + /// Replace an unpatchable syscall instruction with `ICEBP; HLT` (`F1 F4`) so /// that reaching it traps instead of silently escaping to the host kernel. /// @@ -939,6 +1150,26 @@ fn hook_syscall_and_after( } let replace_end = replace_end.unwrap(); + let copied_postsyscall_insts_have_ip_rel_mem = arch == Arch::X86_64 + && instruction_slice_has_ip_rel_memory_operand( + instructions + .iter() + .skip(inst_index + 1) + .take_while(|next_inst| next_inst.ip() < replace_end), + ); + if copied_postsyscall_insts_have_ip_rel_mem { + return hook_syscall_before_and_after( + arch, + control_transfer_targets, + section_base_addr, + section_data, + trampoline_base_addr, + syscall_entry_addr, + trampoline_data, + instructions, + inst_index, + ); + } let target_addr = checked_add_u64( trampoline_base_addr, @@ -947,6 +1178,20 @@ fn hook_syscall_and_after( )?; if arch == Arch::X86_64 { + // Reserve the SysV red zone before entering the shim so async guest + // signal delivery / interrupt handling cannot clobber stack locals + // parked below the architectural RSP. + // LEA RSP, [RSP - 0x80] = 48 8D 64 24 80 + trampoline_data.extend_from_slice(&[0x48, 0x8D, 0x64, 0x24, 0x80]); + + // Put the address of the original JMP (call-site) into R11 so + // that SA_RESTART can rewind ctx.rip to re-enter the trampoline. + // LEA R11, [RIP + disp32] = 4C 8D 1D + let r11_disp = i64::try_from(replace_start).unwrap() + - i64::try_from(trampoline_base_addr + trampoline_data.len() as u64 + 7).unwrap(); + trampoline_data.extend_from_slice(&[0x4C, 0x8D, 0x1D]); // LEA R11, [RIP + disp32] + trampoline_data.extend_from_slice(&(i32::try_from(r11_disp).unwrap().to_le_bytes())); + // Put jump back location into rcx, via lea rcx, [next instruction] trampoline_data.extend_from_slice(&[0x48, 0x8D, 0x0D]); // LEA RCX, [RIP + disp32] trampoline_data.extend_from_slice(&6u32.to_le_bytes()); @@ -970,8 +1215,8 @@ fn hook_syscall_and_after( trampoline_data.extend_from_slice(&[0xE8, 0x0, 0x0, 0x0, 0x0]); // CALL next instruction trampoline_data.push(0x58); // POP EAX (effectively store IP in EAX) trampoline_data.extend_from_slice(&[0xFF, 0x90]); // CALL [EAX + offset] - // EAX = trampoline_base_addr + (trampoline_data.len() - 3) - // We want: EAX + offset = syscall_entry_addr + // EAX = trampoline_base_addr + (trampoline_data.len() - 3) + // We want: EAX + offset = syscall_entry_addr let call_base = checked_add_u64( trampoline_base_addr, trampoline_data.len() as u64, @@ -1028,6 +1273,14 @@ fn hook_syscall_and_after( Ok(()) } +fn instruction_slice_has_ip_rel_memory_operand<'a>( + instructions: impl IntoIterator, +) -> bool { + instructions + .into_iter() + .any(iced_x86::Instruction::is_ip_rel_memory_operand) +} + #[allow(clippy::too_many_arguments)] fn hook_syscall_before_and_after( arch: Arch, @@ -1114,8 +1367,8 @@ fn hook_syscall_before_and_after( trampoline_data.extend_from_slice(&[0xE8, 0x0, 0x0, 0x0, 0x0]); // CALL next instruction trampoline_data.push(0x58); // POP EAX (effectively store IP in EAX) trampoline_data.extend_from_slice(&[0xFF, 0x90]); // CALL [EAX + offset] - // EAX = trampoline_base_addr + (trampoline_data.len() - 3) - // We want: EAX + offset = syscall_entry_addr + // EAX = trampoline_base_addr + (trampoline_data.len() - 3) + // We want: EAX + offset = syscall_entry_addr let call_base = checked_add_u64( trampoline_base_addr, trampoline_data.len() as u64, @@ -1169,3 +1422,94 @@ fn hook_syscall_before_and_after( Ok(()) } + +#[cfg(test)] +mod tests { + use super::{has_bun_footer_marker, patch_code_segment, BUN_FOOTER_MARKER}; + + #[test] + fn detects_bun_footer_marker_near_end() { + let mut bytes = vec![0u8; 512]; + let offset = bytes.len() - BUN_FOOTER_MARKER.len() - 8; + bytes[offset..offset + BUN_FOOTER_MARKER.len()].copy_from_slice(BUN_FOOTER_MARKER); + assert!(has_bun_footer_marker(&bytes)); + } + + #[test] + fn ignores_missing_bun_footer_marker() { + let bytes = vec![0u8; 512]; + assert!(!has_bun_footer_marker(&bytes)); + } + + #[test] + fn patch_code_segment_relocates_rip_relative_presyscall_to_trampoline() { + let mut code = vec![ + 0x48, 0x8D, 0x35, 0x10, 0x00, 0x00, 0x00, // lea rsi, [rip + 0x10] @ 0x1000 + 0x0F, 0x05, // syscall @ 0x1007 + 0x31, 0xC0, // xor eax, eax + 0xBA, 0x01, 0x00, 0x00, 0x00, // mov edx, 1 + ]; + + let trampoline = patch_code_segment(&mut code, 0x1000, 0x8000, 0x9000, &mut Vec::new()) + .expect("patch_code_segment should succeed"); + + assert!(!trampoline.is_empty()); + // The lea + syscall region (9 bytes starting at 0x1000) should now be a + // JMP to the trampoline followed by NOPs. + assert_eq!(code[0], 0xE9, "replace region should start with JMP rel32"); + // The trampoline should contain the re-encoded lea with an adjusted + // RIP-relative displacement targeting the same absolute address. + // Original: lea targets 0x1007 + 0x10 = 0x1017. + // Re-encoded at 0x8000: displacement = 0x1017 - (0x8000 + 7) = -0x6FF0 = 0xFFFF9010 + #[allow(clippy::cast_possible_truncation)] + let expected_disp: i32 = 0x1017_i64.wrapping_sub(0x8000 + 7) as i32; + assert_eq!( + &trampoline[3..7], + &expected_disp.to_le_bytes(), + "re-encoded lea displacement should target the original address" + ); + } + + #[test] + fn patch_code_segment_handles_rip_relative_on_both_sides_of_syscall() { + let mut code = vec![ + 0x48, 0x8D, 0x35, 0x10, 0x00, 0x00, 0x00, // lea rsi, [rip + 0x10] @ 0x1000 + 0x0F, 0x05, // syscall @ 0x1007 + 0x48, 0x8D, 0x3D, 0x10, 0x00, 0x00, 0x00, // lea rdi, [rip + 0x10] + ]; + + let mut skipped = Vec::new(); + let stubs = patch_code_segment(&mut code, 0x1000, 0x8000, 0x9000, &mut skipped) + .expect("patch_code_segment should succeed"); + // The pre-syscall lea is re-encoded in the trampoline; the + // post-syscall lea stays in place (not overwritten). + assert!(!stubs.is_empty(), "should be patched via re-encoding"); + assert_eq!(code[0], 0xE9, "replace region should start with JMP"); + assert!(skipped.is_empty(), "nothing should be skipped"); + } + + #[test] + fn patch_code_segment_patches_all_syscalls_including_rip_relative() { + let mut code = vec![ + // First syscall: patchable (3 nops before = 5 bytes total with syscall) + 0x90, 0x90, 0x90, // nop; nop; nop + 0x0F, 0x05, // syscall @ offset 3 + 0xC3, // ret + // Second syscall: RIP-relative before, now patchable via re-encoding + 0x48, 0x8D, 0x35, 0x10, 0x00, 0x00, 0x00, // lea rsi, [rip+0x10] + 0x0F, 0x05, // syscall @ offset 13 + 0x48, 0x8D, 0x3D, 0x10, 0x00, 0x00, 0x00, // lea rdi, [rip+0x10] + ]; + + let mut skipped = Vec::new(); + let stubs = patch_code_segment(&mut code, 0x1000, 0x8000, 0x9000, &mut skipped).unwrap(); + + assert!(!stubs.is_empty(), "both syscalls should be patched"); + assert_eq!(code[0], 0xE9, "first syscall site should be a JMP"); + assert_eq!( + code[6], 0xE9, + "second syscall site (lea start) should be a JMP" + ); + assert!(skipped.is_empty(), "nothing should be skipped"); + } +} diff --git a/litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-diff.snap b/litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-diff.snap index 9f933eb4d..aebaab30d 100644 --- a/litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-diff.snap +++ b/litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-diff.snap @@ -24,7 +24,7 @@ expression: diff - 401e78: 31 ff xor %edi,%edi - 401e7a: 89 d0 mov %edx,%eax - 401e7c: 0f 05 syscall -+ 401e78: ++ 401e78: + 401e7d: 90 nop 401e7e: eb f8 jmp 401e78 <__libc_start_call_main+0x88> 401e80: 31 c0 xor %eax,%eax @@ -35,7 +35,7 @@ expression: diff 403ee0: bf 01 50 00 00 mov $0x5001,%edi - 403ee5: b8 9e 00 00 00 mov $0x9e,%eax - 403eea: 0f 05 syscall -+ 403ee5: ++ 403ee5: + 403eea: 90 nop + 403eeb: 90 nop 403eec: 44 89 ef mov %r13d,%edi @@ -47,7 +47,7 @@ expression: diff 4043ce: 48 89 36 mov %rsi,(%rsi) - 4043d1: 48 89 76 10 mov %rsi,0x10(%rsi) - 4043d5: 0f 05 syscall -+ 4043d1: ++ 4043d1: + 4043d6: 90 nop 4043d7: 85 c0 test %eax,%eax 4043d9: 74 24 je 4043ff <__libc_setup_tls+0x1df> @@ -56,7 +56,7 @@ expression: diff 4043e5: b8 01 00 00 00 mov $0x1,%eax - 4043ea: 48 8d 35 c7 d1 07 00 lea 0x7d1c7(%rip),%rsi # 4815b8 - 4043f1: 0f 05 syscall -+ 4043ea: ++ 4043ea: + 4043ef: 90 nop + 4043f0: 90 nop + 4043f1: 90 nop @@ -64,7 +64,7 @@ expression: diff 4043f3: bf 7f 00 00 00 mov $0x7f,%edi - 4043f8: b8 e7 00 00 00 mov $0xe7,%eax - 4043fd: 0f 05 syscall -+ 4043f8: ++ 4043f8: + 4043fd: 90 nop + 4043fe: 90 nop 4043ff: e8 dc ba 01 00 call 41fee0 <__tls_init_tp> @@ -76,7 +76,7 @@ expression: diff 4044ba: b8 01 00 00 00 mov $0x1,%eax - 4044bf: 48 8d 35 f2 d0 07 00 lea 0x7d0f2(%rip),%rsi # 4815b8 - 4044c6: 0f 05 syscall -+ 4044bf: ++ 4044bf: + 4044c4: 90 nop + 4044c5: 90 nop + 4044c6: 90 nop @@ -84,7 +84,7 @@ expression: diff 4044c8: bf 7f 00 00 00 mov $0x7f,%edi - 4044cd: b8 e7 00 00 00 mov $0xe7,%eax - 4044d2: 0f 05 syscall -+ 4044cd: ++ 4044cd: + 4044d2: 90 nop + 4044d3: 90 nop 4044d4: e9 70 fe ff ff jmp 404349 <__libc_setup_tls+0x129> @@ -96,7 +96,7 @@ expression: diff 40a3e7: bf 02 00 00 00 mov $0x2,%edi - 40a3ec: 44 89 c8 mov %r9d,%eax - 40a3ef: 0f 05 syscall -+ 40a3ec: ++ 40a3ec: 40a3f1: 48 83 f8 fc cmp $0xfffffffffffffffc,%rax 40a3f5: 74 e9 je 40a3e0 <__libc_message_impl+0x150> 40a3f7: 45 31 c9 xor %r9d,%r9d @@ -106,7 +106,7 @@ expression: diff 40a5cf: be 80 00 00 00 mov $0x80,%esi - 40a5d4: b8 ca 00 00 00 mov $0xca,%eax - 40a5d9: 0f 05 syscall -+ 40a5d4: ++ 40a5d4: + 40a5d9: 90 nop + 40a5da: 90 nop 40a5db: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -118,7 +118,7 @@ expression: diff 40a635: b8 ca 00 00 00 mov $0xca,%eax - 40a63a: 40 80 f6 80 xor $0x80,%sil - 40a63e: 0f 05 syscall -+ 40a63a: ++ 40a63a: + 40a63f: 90 nop 40a640: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax 40a646: 76 d6 jbe 40a61e <__lll_lock_wait+0xe> @@ -129,7 +129,7 @@ expression: diff 40a67c: be 81 00 00 00 mov $0x81,%esi - 40a681: b8 ca 00 00 00 mov $0xca,%eax - 40a686: 0f 05 syscall -+ 40a681: ++ 40a681: + 40a686: 90 nop + 40a687: 90 nop 40a688: c3 ret @@ -141,7 +141,7 @@ expression: diff 40a69b: ba 01 00 00 00 mov $0x1,%edx - 40a6a0: b8 ca 00 00 00 mov $0xca,%eax - 40a6a5: 0f 05 syscall -+ 40a6a0: ++ 40a6a0: + 40a6a5: 90 nop + 40a6a6: 90 nop 40a6a7: c3 ret @@ -153,7 +153,7 @@ expression: diff 40bbdb: c6 05 3e 4c 0a 00 01 movb $0x1,0xa4c3e(%rip) # 4b0820 <__malloc_initialized> - 40bbe2: b8 3e 01 00 00 mov $0x13e,%eax - 40bbe7: 0f 05 syscall -+ 40bbe2: ++ 40bbe2: + 40bbe7: 90 nop + 40bbe8: 90 nop 40bbe9: 48 8d 5d d0 lea -0x30(%rbp),%rbx @@ -165,7 +165,7 @@ expression: diff 4181de: 66 90 xchg %ax,%ax - 4181e0: b8 e4 00 00 00 mov $0xe4,%eax - 4181e5: 0f 05 syscall -+ 4181e0: ++ 4181e0: + 4181e5: 90 nop + 4181e6: 90 nop 4181e7: 85 c0 test %eax,%eax @@ -177,7 +177,7 @@ expression: diff 418249: 89 d0 mov %edx,%eax - 41824b: 0f 05 syscall - 41824d: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax -+ 41824b: ++ 41824b: + 418250: 90 nop + 418251: 90 nop + 418252: 90 nop @@ -190,7 +190,7 @@ expression: diff 418260: f3 0f 1e fa endbr64 - 418264: b8 05 00 00 00 mov $0x5,%eax - 418269: 0f 05 syscall -+ 418264: ++ 418264: + 418269: 90 nop + 41826a: 90 nop 41826b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -202,7 +202,7 @@ expression: diff 418290: f3 0f 1e fa endbr64 - 418294: b8 03 00 00 00 mov $0x3,%eax - 418299: 0f 05 syscall -+ 418294: ++ 418294: + 418299: 90 nop + 41829a: 90 nop 41829b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -214,7 +214,7 @@ expression: diff 4182f9: 74 25 je 418320 <__fcntl64_nocancel+0x60> - 4182fb: b8 48 00 00 00 mov $0x48,%eax - 418300: 0f 05 syscall -+ 4182fb: ++ 4182fb: + 418300: 90 nop + 418301: 90 nop 418302: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -226,7 +226,7 @@ expression: diff 418324: be 10 00 00 00 mov $0x10,%esi - 418329: b8 48 00 00 00 mov $0x48,%eax - 41832e: 0f 05 syscall -+ 418329: ++ 418329: + 41832e: 90 nop + 41832f: 90 nop 418330: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -238,7 +238,7 @@ expression: diff 41837e: 74 20 je 4183a0 <__fcntl64_nocancel_adjusted+0x40> - 418380: b8 48 00 00 00 mov $0x48,%eax - 418385: 0f 05 syscall -+ 418380: ++ 418380: + 418385: 90 nop + 418386: 90 nop 418387: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -250,7 +250,7 @@ expression: diff 4183a4: be 10 00 00 00 mov $0x10,%esi - 4183a9: b8 48 00 00 00 mov $0x48,%eax - 4183ae: 0f 05 syscall -+ 4183a9: ++ 4183a9: + 4183ae: 90 nop + 4183af: 90 nop 4183b0: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -262,7 +262,7 @@ expression: diff 41841a: 48 89 fe mov %rdi,%rsi - 41841d: bf 9c ff ff ff mov $0xffffff9c,%edi - 418422: 0f 05 syscall -+ 41841d: ++ 41841d: + 418422: 90 nop + 418423: 90 nop 418424: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -275,7 +275,7 @@ expression: diff - 418480: f3 0f 1e fa endbr64 - 418484: 31 c0 xor %eax,%eax - 418486: 0f 05 syscall -+ 418480: ++ 418480: + 418485: 90 nop + 418486: 90 nop + 418487: 90 nop @@ -288,7 +288,7 @@ expression: diff 4184b0: f3 0f 1e fa endbr64 - 4184b4: b8 0c 00 00 00 mov $0xc,%eax - 4184b9: 0f 05 syscall -+ 4184b4: ++ 4184b4: + 4184b9: 90 nop + 4184ba: 90 nop 4184bb: 48 89 05 96 83 09 00 mov %rax,0x98396(%rip) # 4b0858 <__curbrk> @@ -300,7 +300,7 @@ expression: diff 41871a: 48 8d 95 f0 ef ff ff lea -0x1010(%rbp),%rdx - 418721: b8 cc 00 00 00 mov $0xcc,%eax - 418726: 0f 05 syscall -+ 418721: ++ 418721: + 418726: 90 nop + 418727: 90 nop 418728: 85 c0 test %eax,%eax @@ -312,7 +312,7 @@ expression: diff 418b40: f3 0f 1e fa endbr64 - 418b44: b8 1c 00 00 00 mov $0x1c,%eax - 418b49: 0f 05 syscall -+ 418b44: ++ 418b44: + 418b49: 90 nop + 418b4a: 90 nop 418b4b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax @@ -324,7 +324,7 @@ expression: diff 418b92: 48 89 df mov %rbx,%rdi - 418b95: b8 09 00 00 00 mov $0x9,%eax - 418b9a: 0f 05 syscall -+ 418b95: ++ 418b95: + 418b9a: 90 nop + 418b9b: 90 nop 418b9c: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -336,7 +336,7 @@ expression: diff 418bed: b8 09 00 00 00 mov $0x9,%eax - 418bf2: 41 83 ca 40 or $0x40,%r10d - 418bf6: 0f 05 syscall -+ 418bf2: ++ 418bf2: + 418bf7: 90 nop 418bf8: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax 418bfe: 76 a4 jbe 418ba4 <__mmap64+0x34> @@ -347,7 +347,7 @@ expression: diff 418c30: f3 0f 1e fa endbr64 - 418c34: b8 0a 00 00 00 mov $0xa,%eax - 418c39: 0f 05 syscall -+ 418c34: ++ 418c34: + 418c39: 90 nop + 418c3a: 90 nop 418c3b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax @@ -359,7 +359,7 @@ expression: diff 418c60: f3 0f 1e fa endbr64 - 418c64: b8 0b 00 00 00 mov $0xb,%eax - 418c69: 0f 05 syscall -+ 418c64: ++ 418c64: + 418c69: 90 nop + 418c6a: 90 nop 418c6b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax @@ -371,7 +371,7 @@ expression: diff 418d47: 45 31 c0 xor %r8d,%r8d - 418d4a: b8 19 00 00 00 mov $0x19,%eax - 418d4f: 0f 05 syscall -+ 418d4a: ++ 418d4a: + 418d4f: 90 nop + 418d50: 90 nop 418d51: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -383,7 +383,7 @@ expression: diff 418e23: bf 41 4d 56 53 mov $0x53564d41,%edi - 418e28: b8 9d 00 00 00 mov $0x9d,%eax - 418e2d: 0f 05 syscall -+ 418e28: ++ 418e28: + 418e2d: 90 nop + 418e2e: 90 nop 418e2f: 83 f8 ea cmp $0xffffffea,%eax @@ -395,7 +395,7 @@ expression: diff 418e50: f3 0f 1e fa endbr64 - 418e54: b8 63 00 00 00 mov $0x63,%eax - 418e59: 0f 05 syscall -+ 418e54: ++ 418e54: + 418e59: 90 nop + 418e5a: 90 nop 418e5b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax @@ -407,7 +407,7 @@ expression: diff 41e494: 48 8d 9d e0 ef ff ff lea -0x1020(%rbp),%rbx - 41e49b: 48 89 da mov %rbx,%rdx - 41e49e: 0f 05 syscall -+ 41e49b: ++ 41e49b: 41e4a0: 85 c0 test %eax,%eax 41e4a2: 7e 5c jle 41e500 <_dl_get_origin+0xa0> 41e4a4: 0f b6 95 e0 ef ff ff movzbl -0x1020(%rbp),%edx @@ -417,7 +417,7 @@ expression: diff 41e6db: 48 8d b5 d0 f6 ff ff lea -0x930(%rbp),%rsi - 41e6e2: b8 14 00 00 00 mov $0x14,%eax - 41e6e7: 0f 05 syscall -+ 41e6e2: ++ 41e6e2: + 41e6e7: 90 nop + 41e6e8: 90 nop 41e6e9: 48 81 c4 38 09 00 00 add $0x938,%rsp @@ -429,7 +429,7 @@ expression: diff 41ff24: 48 8d bb d0 02 00 00 lea 0x2d0(%rbx),%rdi - 41ff2b: b8 da 00 00 00 mov $0xda,%eax - 41ff30: 0f 05 syscall -+ 41ff2b: ++ 41ff2b: + 41ff30: 90 nop + 41ff31: 90 nop 41ff32: 89 83 d0 02 00 00 mov %eax,0x2d0(%rbx) @@ -441,7 +441,7 @@ expression: diff 41ff81: 66 0f 6c c0 punpcklqdq %xmm0,%xmm0 - 41ff85: 0f 11 83 d8 02 00 00 movups %xmm0,0x2d8(%rbx) - 41ff8c: 0f 05 syscall -+ 41ff85: ++ 41ff85: + 41ff8a: 90 nop + 41ff8b: 90 nop + 41ff8c: 90 nop @@ -455,7 +455,7 @@ expression: diff 41fff4: 48 89 df mov %rbx,%rdi - 41fff7: b8 4e 01 00 00 mov $0x14e,%eax - 41fffc: 0f 05 syscall -+ 41fff7: ++ 41fff7: + 41fffc: 90 nop + 41fffd: 90 nop 41fffe: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -467,7 +467,7 @@ expression: diff 421344: bf 02 50 00 00 mov $0x5002,%edi - 421349: b8 9e 00 00 00 mov $0x9e,%eax - 42134e: 0f 05 syscall -+ 421349: ++ 421349: + 42134e: 90 nop + 42134f: 90 nop 421350: 89 c7 mov %eax,%edi @@ -479,7 +479,7 @@ expression: diff 4213a5: 48 89 e5 mov %rsp,%rbp - 4213a8: 48 8d 75 f8 lea -0x8(%rbp),%rsi - 4213ac: 0f 05 syscall -+ 4213a8: ++ 4213a8: + 4213ad: 90 nop 4213ae: 48 85 c0 test %rax,%rax 4213b1: 74 15 je 4213c8 <_dl_cet_setup_features+0x38> @@ -491,7 +491,7 @@ expression: diff - 4213f7: bf 03 50 00 00 mov $0x5003,%edi - 4213fc: 89 d0 mov %edx,%eax - 4213fe: 0f 05 syscall -+ 4213f7: ++ 4213f7: + 4213fc: 90 nop + 4213fd: 90 nop + 4213fe: 90 nop @@ -506,13 +506,13 @@ expression: diff - 421455: 31 ff xor %edi,%edi - 421457: 89 f0 mov %esi,%eax - 421459: 0f 05 syscall -+ 421455: ++ 421455: + 42145a: 90 nop 42145b: 48 89 c2 mov %rax,%rdx - 42145e: 48 8d 3c 18 lea (%rax,%rbx,1),%rdi - 421462: 89 f0 mov %esi,%eax - 421464: 0f 05 syscall -+ 42145e: ++ 42145e: + 421463: 90 nop + 421464: 90 nop + 421465: 90 nop @@ -525,7 +525,7 @@ expression: diff 421481: 48 89 de mov %rbx,%rsi - 421484: b8 09 00 00 00 mov $0x9,%eax - 421489: 0f 05 syscall -+ 421484: ++ 421484: + 421489: 90 nop + 42148a: 90 nop 42148b: 31 d2 xor %edx,%edx @@ -537,7 +537,7 @@ expression: diff 444c16: 48 8d 35 b3 0a 04 00 lea 0x40ab3(%rip),%rsi # 4856d0 - 444c1d: b8 0e 00 00 00 mov $0xe,%eax - 444c22: 0f 05 syscall -+ 444c1d: ++ 444c1d: + 444c22: 90 nop + 444c23: 90 nop 444c24: 31 c0 xor %eax,%eax @@ -549,7 +549,7 @@ expression: diff 444c63: bf 02 00 00 00 mov $0x2,%edi - 444c68: b8 0e 00 00 00 mov $0xe,%eax - 444c6d: 0f 05 syscall -+ 444c68: ++ 444c68: + 444c6d: 90 nop + 444c6e: 90 nop 444c6f: 48 8b 45 d8 mov -0x28(%rbp),%rax @@ -561,7 +561,7 @@ expression: diff 444ca8: 89 de mov %ebx,%esi - 444caa: b8 ea 00 00 00 mov $0xea,%eax - 444caf: 0f 05 syscall -+ 444caa: ++ 444caa: + 444caf: 90 nop + 444cb0: 90 nop 444cb1: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -572,7 +572,7 @@ expression: diff 444cbe: 66 90 xchg %ax,%ax - 444cc0: b8 ba 00 00 00 mov $0xba,%eax - 444cc5: 0f 05 syscall -+ 444cc0: ++ 444cc0: + 444cc5: 90 nop + 444cc6: 90 nop 444cc7: 89 c3 mov %eax,%ebx @@ -582,7 +582,7 @@ expression: diff 444cd3: 89 c7 mov %eax,%edi - 444cd5: b8 ea 00 00 00 mov $0xea,%eax - 444cda: 0f 05 syscall -+ 444cd5: ++ 444cd5: + 444cda: 90 nop + 444cdb: 90 nop 444cdc: 89 c3 mov %eax,%ebx @@ -594,7 +594,7 @@ expression: diff 444d78: 4c 89 fa mov %r15,%rdx - 444d7b: 48 8d 35 4e 09 04 00 lea 0x4094e(%rip),%rsi # 4856d0 - 444d82: 0f 05 syscall -+ 444d7b: ++ 444d7b: + 444d80: 90 nop + 444d81: 90 nop + 444d82: 90 nop @@ -608,7 +608,7 @@ expression: diff 444dc4: bf 02 00 00 00 mov $0x2,%edi - 444dc9: b8 0e 00 00 00 mov $0xe,%eax - 444dce: 0f 05 syscall -+ 444dc9: ++ 444dc9: + 444dce: 90 nop + 444dcf: 90 nop 444dd0: 48 8b 45 c8 mov -0x38(%rbp),%rax @@ -620,7 +620,7 @@ expression: diff 444e08: 89 de mov %ebx,%esi - 444e0a: b8 ea 00 00 00 mov $0xea,%eax - 444e0f: 0f 05 syscall -+ 444e0a: ++ 444e0a: + 444e0f: 90 nop + 444e10: 90 nop 444e11: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -630,7 +630,7 @@ expression: diff 444e1e: eb 8a jmp 444daa <__pthread_kill+0x8a> - 444e20: b8 ba 00 00 00 mov $0xba,%eax - 444e25: 0f 05 syscall -+ 444e20: ++ 444e20: + 444e25: 90 nop + 444e26: 90 nop 444e27: 89 c3 mov %eax,%ebx @@ -640,7 +640,7 @@ expression: diff 444e33: 89 c7 mov %eax,%edi - 444e35: b8 ea 00 00 00 mov $0xea,%eax - 444e3a: 0f 05 syscall -+ 444e35: ++ 444e35: + 444e3a: 90 nop + 444e3b: 90 nop 444e3c: 41 89 c6 mov %eax,%r14d @@ -652,7 +652,7 @@ expression: diff 445107: f7 d6 not %esi - 445109: 81 e6 80 00 00 00 and $0x80,%esi - 44510f: 0f 05 syscall -+ 445109: ++ 445109: + 44510e: 90 nop + 44510f: 90 nop + 445110: 90 nop @@ -665,7 +665,7 @@ expression: diff 4452e4: 48 89 df mov %rbx,%rdi - 4452e7: b8 ca 00 00 00 mov $0xca,%eax - 4452ec: 0f 05 syscall -+ 4452e7: ++ 4452e7: + 4452ec: 90 nop + 4452ed: 90 nop 4452ee: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -677,7 +677,7 @@ expression: diff 4454ff: be 07 00 00 00 mov $0x7,%esi - 445504: b8 ca 00 00 00 mov $0xca,%eax - 445509: 0f 05 syscall -+ 445504: ++ 445504: + 445509: 90 nop + 44550a: 90 nop 44550b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -689,7 +689,7 @@ expression: diff 445aa9: 81 e6 80 00 00 00 and $0x80,%esi - 445aaf: 40 80 f6 81 xor $0x81,%sil - 445ab3: 0f 05 syscall -+ 445aaf: ++ 445aaf: + 445ab4: 90 nop 445ab5: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax 445abb: 0f 87 0e 02 00 00 ja 445ccf <__pthread_mutex_unlock_full+0x3bf> @@ -700,7 +700,7 @@ expression: diff 445cfd: 4c 89 c7 mov %r8,%rdi - 445d00: b8 ca 00 00 00 mov $0xca,%eax - 445d05: 0f 05 syscall -+ 445d00: ++ 445d00: + 445d05: 90 nop + 445d06: 90 nop 445d07: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -712,7 +712,7 @@ expression: diff 445d29: 4c 89 c7 mov %r8,%rdi - 445d2c: b8 ca 00 00 00 mov $0xca,%eax - 445d31: 0f 05 syscall -+ 445d2c: ++ 445d2c: + 445d31: 90 nop + 445d32: 90 nop 445d33: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -724,7 +724,7 @@ expression: diff 44600f: 48 89 df mov %rbx,%rdi - 446012: b8 ca 00 00 00 mov $0xca,%eax - 446017: 0f 05 syscall -+ 446012: ++ 446012: + 446017: 90 nop + 446018: 90 nop 446019: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -736,7 +736,7 @@ expression: diff 4460b0: 48 89 df mov %rbx,%rdi - 4460b3: b8 ca 00 00 00 mov $0xca,%eax - 4460b8: 0f 05 syscall -+ 4460b3: ++ 4460b3: + 4460b8: 90 nop + 4460b9: 90 nop 4460ba: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -748,7 +748,7 @@ expression: diff 446142: be 81 00 00 00 mov $0x81,%esi - 446147: b8 ca 00 00 00 mov $0xca,%eax - 44614c: 0f 05 syscall -+ 446147: ++ 446147: + 44614c: 90 nop + 44614d: 90 nop 44614e: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -760,7 +760,7 @@ expression: diff 4462e3: c1 e6 07 shl $0x7,%esi - 4462e6: 40 80 f6 81 xor $0x81,%sil - 4462ea: 0f 05 syscall -+ 4462e6: ++ 4462e6: + 4462eb: 90 nop 4462ec: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax 4462f2: 0f 86 2e ff ff ff jbe 446226 <___pthread_rwlock_rdlock+0x46> @@ -771,7 +771,7 @@ expression: diff 446437: 40 80 f6 81 xor $0x81,%sil - 44643b: b8 ca 00 00 00 mov $0xca,%eax - 446440: 0f 05 syscall -+ 44643b: ++ 44643b: + 446440: 90 nop + 446441: 90 nop 446442: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -783,7 +783,7 @@ expression: diff 44648a: b8 ca 00 00 00 mov $0xca,%eax - 44648f: 40 80 f6 81 xor $0x81,%sil - 446493: 0f 05 syscall -+ 44648f: ++ 44648f: + 446494: 90 nop 446495: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax 44649b: 76 83 jbe 446420 <___pthread_rwlock_unlock+0x50> @@ -794,7 +794,7 @@ expression: diff 446511: 40 80 f6 81 xor $0x81,%sil - 446515: b8 ca 00 00 00 mov $0xca,%eax - 44651a: 0f 05 syscall -+ 446515: ++ 446515: + 44651a: 90 nop + 44651b: 90 nop 44651c: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -806,7 +806,7 @@ expression: diff 446577: b8 ca 00 00 00 mov $0xca,%eax - 44657c: 40 80 f6 81 xor $0x81,%sil - 446580: 0f 05 syscall -+ 44657c: ++ 44657c: + 446581: 90 nop 446582: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax 446588: 0f 86 6c ff ff ff jbe 4464fa <___pthread_rwlock_unlock+0x12a> @@ -817,7 +817,7 @@ expression: diff 446855: 40 80 f6 81 xor $0x81,%sil - 446859: b8 ca 00 00 00 mov $0xca,%eax - 44685e: 0f 05 syscall -+ 446859: ++ 446859: + 44685e: 90 nop + 44685f: 90 nop 446860: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -829,7 +829,7 @@ expression: diff 446880: 40 80 f6 81 xor $0x81,%sil - 446884: b8 ca 00 00 00 mov $0xca,%eax - 446889: 0f 05 syscall -+ 446884: ++ 446884: + 446889: 90 nop + 44688a: 90 nop 44688b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -841,7 +841,7 @@ expression: diff 446924: 40 80 f6 81 xor $0x81,%sil - 446928: b8 ca 00 00 00 mov $0xca,%eax - 44692d: 0f 05 syscall -+ 446928: ++ 446928: + 44692d: 90 nop + 44692e: 90 nop 44692f: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -853,7 +853,7 @@ expression: diff 446a0b: 41 ba 08 00 00 00 mov $0x8,%r10d - 446a11: b8 0e 00 00 00 mov $0xe,%eax - 446a16: 0f 05 syscall -+ 446a11: ++ 446a11: + 446a16: 90 nop + 446a17: 90 nop 446a18: 89 c2 mov %eax,%edx @@ -865,7 +865,7 @@ expression: diff 45ba2c: 48 0f 47 d0 cmova %rax,%rdx - 45ba30: b8 d9 00 00 00 mov $0xd9,%eax - 45ba35: 0f 05 syscall -+ 45ba30: ++ 45ba30: + 45ba35: 90 nop + 45ba36: 90 nop 45ba37: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -877,7 +877,7 @@ expression: diff 45bb50: f3 0f 1e fa endbr64 - 45bb54: b8 27 00 00 00 mov $0x27,%eax - 45bb59: 0f 05 syscall -+ 45bb54: ++ 45bb54: + 45bb59: 90 nop + 45bb5a: 90 nop 45bb5b: c3 ret @@ -889,7 +889,7 @@ expression: diff 45bba0: f3 0f 1e fa endbr64 - 45bba4: b8 8f 00 00 00 mov $0x8f,%eax - 45bba9: 0f 05 syscall -+ 45bba4: ++ 45bba4: + 45bba9: 90 nop + 45bbaa: 90 nop 45bbab: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax @@ -901,7 +901,7 @@ expression: diff 45bbd0: f3 0f 1e fa endbr64 - 45bbd4: b8 91 00 00 00 mov $0x91,%eax - 45bbd9: 0f 05 syscall -+ 45bbd4: ++ 45bbd4: + 45bbd9: 90 nop + 45bbda: 90 nop 45bbdb: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax @@ -913,7 +913,7 @@ expression: diff 45bc00: f3 0f 1e fa endbr64 - 45bc04: b8 92 00 00 00 mov $0x92,%eax - 45bc09: 0f 05 syscall -+ 45bc04: ++ 45bc04: + 45bc09: 90 nop + 45bc0a: 90 nop 45bc0b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax @@ -925,7 +925,7 @@ expression: diff 45bc30: f3 0f 1e fa endbr64 - 45bc34: b8 93 00 00 00 mov $0x93,%eax - 45bc39: 0f 05 syscall -+ 45bc34: ++ 45bc34: + 45bc39: 90 nop + 45bc3a: 90 nop 45bc3b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax @@ -937,7 +937,7 @@ expression: diff 45bc60: f3 0f 1e fa endbr64 - 45bc64: b8 90 00 00 00 mov $0x90,%eax - 45bc69: 0f 05 syscall -+ 45bc64: ++ 45bc64: + 45bc69: 90 nop + 45bc6a: 90 nop 45bc6b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax @@ -949,7 +949,7 @@ expression: diff 45bd0d: 48 8b bd 08 ff ff ff mov -0xf8(%rbp),%rdi - 45bd14: b8 4f 00 00 00 mov $0x4f,%eax - 45bd19: 0f 05 syscall -+ 45bd14: ++ 45bd14: + 45bd19: 90 nop + 45bd1a: 90 nop 45bd1b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -961,7 +961,7 @@ expression: diff 45c510: f3 0f 1e fa endbr64 - 45c514: b8 08 00 00 00 mov $0x8,%eax - 45c519: 0f 05 syscall -+ 45c514: ++ 45c514: + 45c519: 90 nop + 45c51a: 90 nop 45c51b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -973,7 +973,7 @@ expression: diff 45c5a9: bf 9c ff ff ff mov $0xffffff9c,%edi - 45c5ae: b8 01 01 00 00 mov $0x101,%eax - 45c5b3: 0f 05 syscall -+ 45c5ae: ++ 45c5ae: + 45c5b3: 90 nop + 45c5b4: 90 nop 45c5b5: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -985,7 +985,7 @@ expression: diff 45c619: bf 9c ff ff ff mov $0xffffff9c,%edi - 45c61e: b8 01 01 00 00 mov $0x101,%eax - 45c623: 0f 05 syscall -+ 45c61e: ++ 45c61e: + 45c623: 90 nop + 45c624: 90 nop 45c625: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -997,7 +997,7 @@ expression: diff 45c6b9: 74 51 je 45c70c <__libc_openat64+0x8c> - 45c6bb: b8 01 01 00 00 mov $0x101,%eax - 45c6c0: 0f 05 syscall -+ 45c6bb: ++ 45c6bb: + 45c6c0: 90 nop + 45c6c1: 90 nop 45c6c2: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -1009,7 +1009,7 @@ expression: diff 45c72d: 8b 7d a8 mov -0x58(%rbp),%edi - 45c730: b8 01 01 00 00 mov $0x101,%eax - 45c735: 0f 05 syscall -+ 45c730: ++ 45c730: + 45c735: 90 nop + 45c736: 90 nop 45c737: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -1021,7 +1021,7 @@ expression: diff 45c79d: 31 c0 xor %eax,%eax - 45c79f: 0f 05 syscall - 45c7a1: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax -+ 45c79f: ++ 45c79f: + 45c7a4: 90 nop + 45c7a5: 90 nop + 45c7a6: 90 nop @@ -1035,7 +1035,7 @@ expression: diff - 45c7d3: 8b 7d f8 mov -0x8(%rbp),%edi - 45c7d6: 31 c0 xor %eax,%eax - 45c7d8: 0f 05 syscall -+ 45c7d3: ++ 45c7d3: + 45c7d8: 90 nop + 45c7d9: 90 nop 45c7da: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -1047,7 +1047,7 @@ expression: diff 45c85b: 74 13 je 45c870 <__libc_write+0x20> - 45c85d: b8 01 00 00 00 mov $0x1,%eax - 45c862: 0f 05 syscall -+ 45c85d: ++ 45c85d: + 45c862: 90 nop + 45c863: 90 nop 45c864: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -1059,7 +1059,7 @@ expression: diff 45c893: 8b 7d f8 mov -0x8(%rbp),%edi - 45c896: b8 01 00 00 00 mov $0x1,%eax - 45c89b: 0f 05 syscall -+ 45c896: ++ 45c896: + 45c89b: 90 nop + 45c89c: 90 nop 45c89d: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -1071,7 +1071,7 @@ expression: diff 45c920: 74 26 je 45c948 <__openat64_nocancel+0x58> - 45c922: b8 01 01 00 00 mov $0x101,%eax - 45c927: 0f 05 syscall -+ 45c922: ++ 45c922: + 45c927: 90 nop + 45c928: 90 nop 45c929: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -1083,7 +1083,7 @@ expression: diff 45c984: 49 89 ca mov %rcx,%r10 - 45c987: b8 11 00 00 00 mov $0x11,%eax - 45c98c: 0f 05 syscall -+ 45c987: ++ 45c987: + 45c98c: 90 nop + 45c98d: 90 nop 45c98e: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -1095,7 +1095,7 @@ expression: diff 45c9c0: f3 0f 1e fa endbr64 - 45c9c4: b8 01 00 00 00 mov $0x1,%eax - 45c9c9: 0f 05 syscall -+ 45c9c4: ++ 45c9c4: + 45c9c9: 90 nop + 45c9ca: 90 nop 45c9cb: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -1107,7 +1107,7 @@ expression: diff 45ca13: 48 8d 55 d0 lea -0x30(%rbp),%rdx - 45ca17: b8 10 00 00 00 mov $0x10,%eax - 45ca1c: 0f 05 syscall -+ 45ca17: ++ 45ca17: + 45ca1c: 90 nop + 45ca1d: 90 nop 45ca1e: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -1120,7 +1120,7 @@ expression: diff - 45cabb: b8 2e 01 00 00 mov $0x12e,%eax - 45cac0: 31 ff xor %edi,%edi - 45cac2: 0f 05 syscall -+ 45cabb: ++ 45cabb: + 45cac0: 90 nop + 45cac1: 90 nop + 45cac2: 90 nop @@ -1134,7 +1134,7 @@ expression: diff 45ffa0: 48 8d 78 1c lea 0x1c(%rax),%rdi - 45ffa4: b8 ca 00 00 00 mov $0xca,%eax - 45ffa9: 0f 05 syscall -+ 45ffa4: ++ 45ffa4: + 45ffa9: 90 nop + 45ffaa: 90 nop 45ffab: 48 8d 3d 6e ab 04 00 lea 0x4ab6e(%rip),%rdi # 4aab20 <_dl_load_lock> @@ -1146,7 +1146,7 @@ expression: diff 46306a: be 80 00 00 00 mov $0x80,%esi - 46306f: 44 89 c8 mov %r9d,%eax - 463072: 0f 05 syscall -+ 46306f: ++ 46306f: 463074: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax 46307a: 76 dc jbe 463058 <__thread_gscope_wait+0x88> 46307c: 83 f8 f5 cmp $0xfffffff5,%eax @@ -1156,7 +1156,7 @@ expression: diff 46310a: be 80 00 00 00 mov $0x80,%esi - 46310f: 44 89 c8 mov %r9d,%eax - 463112: 0f 05 syscall -+ 46310f: ++ 46310f: 463114: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax 46311a: 76 dc jbe 4630f8 <__thread_gscope_wait+0x128> 46311c: 83 f8 f5 cmp $0xfffffff5,%eax @@ -1166,7 +1166,7 @@ expression: diff 00000000004669d0 <__restore_rt>: - 4669d0: 48 c7 c0 0f 00 00 00 mov $0xf,%rax - 4669d7: 0f 05 syscall -+ 4669d0: ++ 4669d0: + 4669d5: 90 nop + 4669d6: 90 nop + 4669d7: 90 nop @@ -1180,7 +1180,7 @@ expression: diff 466aad: 41 ba 08 00 00 00 mov $0x8,%r10d - 466ab3: b8 0d 00 00 00 mov $0xd,%eax - 466ab8: 0f 05 syscall -+ 466ab3: ++ 466ab3: + 466ab8: 90 nop + 466ab9: 90 nop 466aba: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -1192,7 +1192,7 @@ expression: diff 46cb16: be 80 00 00 00 mov $0x80,%esi - 46cb1b: 44 89 c0 mov %r8d,%eax - 46cb1e: 0f 05 syscall -+ 46cb1b: ++ 46cb1b: 46cb20: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax 46cb26: 77 0d ja 46cb35 <__pthread_disable_asynccancel+0x65> 46cb28: 8b 0f mov (%rdi),%ecx @@ -1202,7 +1202,7 @@ expression: diff 46ccdf: 44 31 c6 xor %r8d,%esi - 46cce2: 45 31 c0 xor %r8d,%r8d - 46cce5: 0f 05 syscall -+ 46cce2: ++ 46cce2: 46cce7: 85 c0 test %eax,%eax 46cce9: 7f 27 jg 46cd12 <__futex_abstimed_wait64+0x62> 46cceb: 83 f8 ea cmp $0xffffffea,%eax @@ -1212,7 +1212,7 @@ expression: diff 46cd89: 44 89 e2 mov %r12d,%edx - 46cd8c: b8 ca 00 00 00 mov $0xca,%eax - 46cd91: 0f 05 syscall -+ 46cd8c: ++ 46cd8c: + 46cd91: 90 nop + 46cd92: 90 nop 46cd93: 48 89 c3 mov %rax,%rbx @@ -1224,7 +1224,7 @@ expression: diff 46ce17: 44 89 e2 mov %r12d,%edx - 46ce1a: b8 ca 00 00 00 mov $0xca,%eax - 46ce1f: 0f 05 syscall -+ 46ce1a: ++ 46ce1a: + 46ce1f: 90 nop + 46ce20: 90 nop 46ce21: 44 89 ef mov %r13d,%edi @@ -1236,7 +1236,7 @@ expression: diff 46ce6c: 31 d2 xor %edx,%edx - 46ce6e: b8 ca 00 00 00 mov $0xca,%eax - 46ce73: 0f 05 syscall -+ 46ce6e: ++ 46ce6e: + 46ce73: 90 nop + 46ce74: 90 nop 46ce75: 83 f8 da cmp $0xffffffda,%eax @@ -1248,7 +1248,7 @@ expression: diff 46f344: 41 89 ca mov %ecx,%r10d - 46f347: b8 06 01 00 00 mov $0x106,%eax - 46f34c: 0f 05 syscall -+ 46f347: ++ 46f347: + 46f34c: 90 nop + 46f34d: 90 nop 46f34e: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -1260,7 +1260,7 @@ expression: diff 472975: 48 8d 78 1c lea 0x1c(%rax),%rdi - 472979: b8 ca 00 00 00 mov $0xca,%eax - 47297e: 0f 05 syscall -+ 472979: ++ 472979: + 47297e: 90 nop + 47297f: 90 nop 472980: eb 8c jmp 47290e <_dl_fixup+0x10e> @@ -1272,7 +1272,7 @@ expression: diff 476c10: 48 8d 78 1c lea 0x1c(%rax),%rdi - 476c14: b8 ca 00 00 00 mov $0xca,%eax - 476c19: 0f 05 syscall -+ 476c14: ++ 476c14: + 476c19: 90 nop + 476c1a: 90 nop 476c1b: 48 83 7d 98 00 cmpq $0x0,-0x68(%rbp) @@ -1284,7 +1284,7 @@ expression: diff 476e4a: 48 8d 78 1c lea 0x1c(%rax),%rdi - 476e4e: b8 ca 00 00 00 mov $0xca,%eax - 476e53: 0f 05 syscall -+ 476e4e: ++ 476e4e: + 476e53: 90 nop + 476e54: 90 nop 476e55: 48 83 7d 98 00 cmpq $0x0,-0x68(%rbp) From 6818dd7a91d3761d9823de9657fdc35746bc21ad Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 2 Apr 2026 06:26:31 -0700 Subject: [PATCH 02/14] Fix CI: 32-bit build, Windows redzone callback, rustfmt - Gate syscall_callback_redzone behind #[cfg(target_arch = "x86_64")] on Linux since the asm symbol only exists in the x86_64 asm block, fixing the i686 linker error. - Add syscall_callback_redzone entry point to the Windows platform so the new trampoline format (with redzone reservation) works correctly on the Windows emulator. Uses mov+add to SCRATCH to avoid clobbering rax. - Fix rustfmt import ordering in litebox_shim_linux/src/loader/elf.rs. --- litebox_platform_linux_userland/src/lib.rs | 18 ++++++++++++++---- litebox_platform_windows_userland/src/lib.rs | 19 ++++++++++++++++++- litebox_shim_linux/src/loader/elf.rs | 4 ++-- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/litebox_platform_linux_userland/src/lib.rs b/litebox_platform_linux_userland/src/lib.rs index 7babef6ca..4c06e3c8a 100644 --- a/litebox_platform_linux_userland/src/lib.rs +++ b/litebox_platform_linux_userland/src/lib.rs @@ -1992,6 +1992,7 @@ impl litebox::platform::StdioProvider for LinuxUserland { unsafe extern "C" { // Defined in asm blocks above fn syscall_callback() -> isize; + #[cfg(target_arch = "x86_64")] fn syscall_callback_redzone() -> isize; fn exception_callback(); fn interrupt_callback(); @@ -2073,7 +2074,14 @@ impl ThreadContext<'_> { impl litebox::platform::SystemInfoProvider for LinuxUserland { fn get_syscall_entry_point(&self) -> usize { - syscall_callback_redzone as *const () as usize + #[cfg(target_arch = "x86_64")] + { + syscall_callback_redzone as *const () as usize + } + #[cfg(target_arch = "x86")] + { + syscall_callback as *const () as usize + } } fn get_vdso_address(&self) -> Option { @@ -2740,9 +2748,11 @@ unsafe fn interrupt_signal_handler( // FUTURE: handle trampoline code, too. This is somewhat less important // because it's probably fine for the shim to observe a guest context that // is inside the trampoline. - if ip == syscall_callback as *const () as usize - || ip == syscall_callback_redzone as *const () as usize - { + let is_at_syscall_callback = ip == syscall_callback as *const () as usize; + #[cfg(target_arch = "x86_64")] + let is_at_syscall_callback = + is_at_syscall_callback || ip == syscall_callback_redzone as *const () as usize; + if is_at_syscall_callback { // No need to clear `in_guest` or set interrupt; the syscall handler will // clear `in_guest` and call into the shim. return; diff --git a/litebox_platform_windows_userland/src/lib.rs b/litebox_platform_windows_userland/src/lib.rs index 9d827e057..ed694f3ee 100644 --- a/litebox_platform_windows_userland/src/lib.rs +++ b/litebox_platform_windows_userland/src/lib.rs @@ -562,6 +562,22 @@ syscall_callback: mov BYTE PTR [r11 + {IS_IN_GUEST}], 0 // Set rsp to the top of the guest context. mov QWORD PTR [r11 + {SCRATCH}], rsp + jmp .Lsyscall_callback_common + + .globl syscall_callback_redzone +syscall_callback_redzone: + // Same as syscall_callback, but the trampoline has already reserved + // 128 bytes below RSP to protect the SysV red zone. Recover the + // architectural guest stack pointer. + mov r11d, DWORD PTR [rip + {TLS_INDEX}] + mov r11, QWORD PTR gs:[r11 * 8 + TEB_TLS_SLOTS_OFFSET] + mov BYTE PTR [r11 + {IS_IN_GUEST}], 0 + // Save RSP + 128 to SCRATCH without clobbering any guest registers. + // Use SCRATCH as a temporary: store rsp, then add 128 in-place. + mov QWORD PTR [r11 + {SCRATCH}], rsp + add QWORD PTR [r11 + {SCRATCH}], 128 + +.Lsyscall_callback_common: mov rsp, QWORD PTR [r11 + {GUEST_CONTEXT_TOP}] // TODO: save float and vector registers (xsave or fxsave) @@ -1948,6 +1964,7 @@ impl litebox::mm::allocator::MemoryProvider for WindowsUserland { unsafe extern "C" { // Defined in asm blocks above fn syscall_callback() -> isize; + fn syscall_callback_redzone() -> isize; fn exception_callback() -> isize; fn interrupt_callback(); fn switch_to_guest_start(); @@ -2037,7 +2054,7 @@ impl ThreadContext<'_> { impl litebox::platform::SystemInfoProvider for WindowsUserland { fn get_syscall_entry_point(&self) -> usize { - syscall_callback as *const () as usize + syscall_callback_redzone as *const () as usize } fn get_vdso_address(&self) -> Option { diff --git a/litebox_shim_linux/src/loader/elf.rs b/litebox_shim_linux/src/loader/elf.rs index 63a9a5d1e..59e972b86 100644 --- a/litebox_shim_linux/src/loader/elf.rs +++ b/litebox_shim_linux/src/loader/elf.rs @@ -10,12 +10,12 @@ use litebox::{ platform::{RawConstPointer as _, SystemInfoProvider as _}, utils::{ReinterpretSignedExt, TruncateExt}, }; -use litebox_common_linux::{errno::Errno, loader::ElfParsedFile, MapFlags}; +use litebox_common_linux::{MapFlags, errno::Errno, loader::ElfParsedFile}; use thiserror::Error; use crate::{ - loader::auxv::{AuxKey, AuxVec}, MutPtr, + loader::auxv::{AuxKey, AuxVec}, }; use super::stack::UserStack; From adbfd9acbcf0658739b6d95ddf1a13e10a34aa03 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 2 Apr 2026 07:43:41 -0700 Subject: [PATCH 03/14] Runtime ELF patching and rtld_audit removal Add runtime syscall patching in the shim's mmap hook: when an ELF segment with PROT_EXEC is mapped, patch syscall instructions in-place and set up a trampoline region. The loader also patches the main binary at load time when it lacks a trampoline. Remove rtld_audit entirely: gut build.rs, remove the audit .so injection from the runner, and remove the REQUIRE_RTLD_AUDIT global. Supporting changes: - Add ReadAt impl for &[u8] in litebox_common_linux - Hook finalize_elf_patch into sys_close to mprotect trampolines RX - Add elf_patch_cache on GlobalState and suppress_elf_runtime_patch on Task - Update ratchet test (runner has zero globals now) --- Cargo.lock | 1 + dev_tests/src/ratchet.rs | 1 - litebox_common_linux/src/loader.rs | 18 + litebox_runner_linux_userland/build.rs | 44 +- litebox_runner_linux_userland/src/lib.rs | 52 +- litebox_shim_linux/Cargo.toml | 1 + litebox_shim_linux/src/lib.rs | 9 + litebox_shim_linux/src/loader/elf.rs | 185 ++++++- litebox_shim_linux/src/syscalls/file.rs | 4 + litebox_shim_linux/src/syscalls/mm.rs | 538 ++++++++++++++++++++- litebox_shim_linux/src/syscalls/process.rs | 1 + 11 files changed, 747 insertions(+), 107 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1466b96ea..4b4a073c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1685,6 +1685,7 @@ dependencies = [ "litebox", "litebox_common_linux", "litebox_platform_multiplex", + "litebox_syscall_rewriter", "once_cell", "ringbuf", "seq-macro", diff --git a/dev_tests/src/ratchet.rs b/dev_tests/src/ratchet.rs index 276452d4d..8e6d35034 100644 --- a/dev_tests/src/ratchet.rs +++ b/dev_tests/src/ratchet.rs @@ -40,7 +40,6 @@ fn ratchet_globals() -> Result<()> { ("litebox_platform_lvbs/", 23), ("litebox_platform_multiplex/", 1), ("litebox_platform_windows_userland/", 8), - ("litebox_runner_linux_userland/", 1), ("litebox_runner_lvbs/", 5), ("litebox_runner_snp/", 1), ("litebox_shim_linux/", 1), diff --git a/litebox_common_linux/src/loader.rs b/litebox_common_linux/src/loader.rs index 8d061b93d..6b1fcc88b 100644 --- a/litebox_common_linux/src/loader.rs +++ b/litebox_common_linux/src/loader.rs @@ -579,6 +579,24 @@ pub trait ReadAt { fn size(&mut self) -> Result; } +impl ReadAt for &[u8] { + type Error = Errno; + + fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), Self::Error> { + let offset: usize = offset.truncate(); + let end = offset.checked_add(buf.len()).ok_or(Errno::ENODATA)?; + if end > self.len() { + return Err(Errno::ENODATA); + } + buf.copy_from_slice(&self[offset..end]); + Ok(()) + } + + fn size(&mut self) -> Result { + Ok(self.len() as u64) + } +} + pub trait MapMemory { type Error; diff --git a/litebox_runner_linux_userland/build.rs b/litebox_runner_linux_userland/build.rs index 3360e452a..f189226e4 100644 --- a/litebox_runner_linux_userland/build.rs +++ b/litebox_runner_linux_userland/build.rs @@ -1,48 +1,6 @@ // 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"); + // rtld_audit has been removed; nothing to build. } diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 291a53a1f..6f56152b1 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -89,9 +89,6 @@ pub enum InterceptionBackend { Rewriter, } -static REQUIRE_RTLD_AUDIT: core::sync::atomic::AtomicBool = - core::sync::atomic::AtomicBool::new(false); - struct MmappedFile { data: &'static [u8], abs_path: PathBuf, @@ -130,14 +127,14 @@ pub fn run(cli_args: CliArgs) -> Result<()> { ) } - // --program-from-tar loads pre-rewritten binaries that depend on litebox_rtld_audit.so, - // which is only injected by the rewriter backend. + // --program-from-tar loads pre-rewritten binaries that require the rewriter + // backend's runtime trampoline setup. if cli_args.program_from_tar && !matches!(cli_args.interception_backend, InterceptionBackend::Rewriter) { anyhow::bail!( "--program-from-tar requires --interception-backend=rewriter \ - (the packaged binary is pre-rewritten and needs the audit library)" + (the packaged binary is pre-rewritten and needs the rewriter runtime)" ); } @@ -306,34 +303,10 @@ pub fn run(cli_args: CliArgs) -> Result<()> { } }); - // When using the rewriter backend, automatically include litebox_rtld_audit.so - // in the filesystem so tests and users don't need to include it in tar files + // When using the rewriter backend, the shim's mmap hook handles + // syscall patching at runtime — no audit library needed. match cli_args.interception_backend { - InterceptionBackend::Rewriter => { - #[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"); - }); - } - InterceptionBackend::Seccomp => { - // No need to include rtld_audit.so for seccomp backend - } + InterceptionBackend::Rewriter | InterceptionBackend::Seccomp => {} } let tar_ro = litebox::fs::tar_ro::FileSystem::new(litebox, tar_data.into()); @@ -396,7 +369,7 @@ pub fn run(cli_args: CliArgs) -> Result<()> { match cli_args.interception_backend { InterceptionBackend::Seccomp => platform.enable_seccomp_based_syscall_interception(), InterceptionBackend::Rewriter => { - REQUIRE_RTLD_AUDIT.store(true, core::sync::atomic::Ordering::SeqCst); + // Runtime patching is handled by the shim's mmap hook — nothing to do here. } } @@ -478,13 +451,6 @@ fn pin_thread_to_cpu(cpu: usize) { } } -fn fixup_env(envp: &mut Vec) { - // Enable the audit library to load trampoline code for rewritten binaries. - if REQUIRE_RTLD_AUDIT.load(core::sync::atomic::Ordering::SeqCst) { - 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()); - } - } +fn fixup_env(_envp: &mut Vec) { + // No-op: rtld_audit has been removed; runtime patching is handled by the shim. } diff --git a/litebox_shim_linux/Cargo.toml b/litebox_shim_linux/Cargo.toml index 94d889a7f..ff0b4ea4e 100644 --- a/litebox_shim_linux/Cargo.toml +++ b/litebox_shim_linux/Cargo.toml @@ -16,6 +16,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 2834f7b72..38f96b535 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -200,6 +200,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) } @@ -257,6 +258,7 @@ impl LinuxShim { fs: Arc::new(syscalls::file::FsState::new()).into(), files: files.into(), signals: syscalls::signal::SignalState::new_process(), + suppress_elf_runtime_patch: Cell::new(false), }, }; entrypoints.task.load_program( @@ -1059,6 +1061,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 { @@ -1082,6 +1086,9 @@ struct Task { files: RefCell>>, /// Signal state signals: syscalls::signal::SignalState, + /// Suppresses runtime ELF patching in `do_mmap_file` while the ELF loader + /// is actively loading a binary (prevents double-mapping the trampoline). + suppress_elf_runtime_patch: Cell, } impl Drop for Task { @@ -1121,6 +1128,7 @@ mod test_utils { fs: Arc::new(syscalls::file::FsState::new()).into(), files: files.into(), signals: syscalls::signal::SignalState::new_process(), + suppress_elf_runtime_patch: Cell::new(false), global: self, } } @@ -1145,6 +1153,7 @@ mod test_utils { fs: self.fs.clone(), files: self.files.clone(), signals: self.signals.clone_for_new_task(), + suppress_elf_runtime_patch: Cell::new(false), }; Some(task) } diff --git a/litebox_shim_linux/src/loader/elf.rs b/litebox_shim_linux/src/loader/elf.rs index 59e972b86..6cbebaf98 100644 --- a/litebox_shim_linux/src/loader/elf.rs +++ b/litebox_shim_linux/src/loader/elf.rs @@ -7,10 +7,14 @@ use alloc::{ffi::CString, vec::Vec}; use litebox::{ fs::{Mode, OFlags}, mm::linux::{CreatePagesFlags, MappingError, PAGE_SIZE}, - platform::{RawConstPointer as _, SystemInfoProvider as _}, + platform::{RawConstPointer as _, RawMutPointer as _, SystemInfoProvider as _}, utils::{ReinterpretSignedExt, TruncateExt}, }; -use litebox_common_linux::{MapFlags, errno::Errno, loader::ElfParsedFile}; +use litebox_common_linux::{ + MapFlags, + errno::Errno, + loader::{ElfParsedFile, ReadAt as _}, +}; use thiserror::Error; use crate::{ @@ -148,6 +152,79 @@ impl litebox_common_linux::loader::MapMemory for ElfFile<'_, FS> { } } +/// A [`MapMemory`](litebox_common_linux::loader::MapMemory) wrapper that reads +/// file-backed data from an in-memory buffer instead of from a file descriptor. +/// Used when the loader has patched the ELF binary on the fly (e.g. syscall +/// rewriting of the dynamic linker). +/// +/// `reserve`, `map_zero`, and `protect` are delegated to the underlying +/// [`ElfFile`]; `map_file` is replaced by `map_zero` + a memory copy from the +/// patched buffer. +struct PatchedMapper<'a, 'b, FS: ShimFS> { + inner: &'b mut ElfFile<'a, FS>, + data: &'b [u8], +} + +impl litebox_common_linux::loader::MapMemory for PatchedMapper<'_, '_, FS> { + type Error = Errno; + + fn reserve(&mut self, len: usize, align: usize) -> Result { + self.inner.reserve(len, align) + } + + fn map_file( + &mut self, + address: usize, + len: usize, + offset: u64, + prot: &litebox_common_linux::loader::Protection, + ) -> Result<(), Self::Error> { + // Allocate anonymous RW pages, copy from the in-memory buffer, then + // apply the requested protection. + self.inner.map_zero( + address, + len, + &litebox_common_linux::loader::Protection { + read: true, + write: true, + execute: false, + }, + )?; + + let offset: usize = offset.truncate(); + if offset < self.data.len() { + let end = core::cmp::min(offset + len, self.data.len()); + let src = &self.data[offset..end]; + let dest = MutPtr::::from_usize(address); + dest.copy_from_slice(0, src).ok_or(Errno::EFAULT)?; + } + + // Set final permissions if different from the writable mapping above. + if !prot.write || prot.execute { + self.inner.protect(address, len, prot)?; + } + Ok(()) + } + + fn map_zero( + &mut self, + address: usize, + len: usize, + prot: &litebox_common_linux::loader::Protection, + ) -> Result<(), Self::Error> { + self.inner.map_zero(address, len, prot) + } + + fn protect( + &mut self, + address: usize, + len: usize, + prot: &litebox_common_linux::loader::Protection, + ) -> Result<(), Self::Error> { + self.inner.protect(address, len, prot) + } +} + /// Struct to hold the information needed to start the program /// (entry point and user stack top). pub struct ElfLoadInfo { @@ -165,6 +242,9 @@ pub(crate) struct ElfLoader<'a, FS: ShimFS> { struct FileAndParsed<'a, FS: ShimFS> { file: ElfFile<'a, FS>, parsed: ElfParsedFile, + /// When the rewriter backend is active and the binary was not pre-patched, + /// the loader patches it on the fly and loads from this in-memory copy. + patched_data: Option>, } impl<'a, FS: ShimFS> FileAndParsed<'a, FS> { @@ -172,11 +252,91 @@ 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)?; - match parsed.parse_trampoline(&mut &file, task.global.platform.get_syscall_entry_point()) { - Ok(()) | Err(litebox_common_linux::loader::ElfParseError::UnpatchedBinary) => {} - Err(err) => return Err(ElfLoaderError::ParseError(err)), - } - Ok(Self { file, parsed }) + + let syscall_entry_point = task.global.platform.get_syscall_entry_point(); + let trampoline_result = parsed.parse_trampoline(&mut &file, syscall_entry_point); + + // If the rewriter backend is active (syscall_entry_point != 0) and the + // binary lacks a trampoline, patch it on the fly so that both the main + // program and the dynamic linker are covered. + let patched_data = if syscall_entry_point != 0 && trampoline_result.is_err() { + let size: usize = (&mut &file) + .size() + .map_err(ElfLoaderError::OpenError)? + .truncate(); + let mut buf = alloc::vec![0u8; size]; + (&mut &file) + .read_at(0, &mut buf) + .map_err(ElfLoaderError::OpenError)?; + + let mut skipped_addrs = alloc::vec::Vec::new(); + match litebox_syscall_rewriter::hook_syscalls_in_elf(&buf, None, &mut skipped_addrs) { + Ok(patched) => { + if !skipped_addrs.is_empty() { + litebox::log_println!( + task.global.platform, + "warning: {} unpatchable syscall instruction(s) (addresses: {:?})", + skipped_addrs.len(), + skipped_addrs, + ); + } + // Re-parse the patched binary and extract its trampoline. + parsed = + litebox_common_linux::loader::ElfParsedFile::parse(&mut patched.as_slice()) + .map_err(ElfLoaderError::ParseError)?; + parsed + .parse_trampoline(&mut patched.as_slice(), syscall_entry_point) + .map_err(ElfLoaderError::ParseError)?; + Some(patched) + } + Err(_) => { + // Patching failed (e.g. ET_REL, no .text). Proceed without + // a trampoline — the binary may simply have no syscalls. + None + } + } + } else { + None + }; + + Ok(Self { + file, + parsed, + patched_data, + }) + } + + /// Load the ELF into guest memory, choosing the right mapper depending on + /// whether the binary was patched in memory. + fn load_mapped( + &mut self, + platform: &(impl litebox::platform::RawPointerProvider + litebox::platform::SystemInfoProvider), + ) -> Result { + // Suppress runtime ELF patching (maybe_patch_exec_segment) when the + // loader will map the trampoline itself via load_trampoline(). Without + // this, both paths would map the same region — the second MAP_FIXED + // destroys the first mapping. + // + // Only suppress when using the ElfFile mapper (which routes through + // do_mmap_file → maybe_patch_exec_segment) AND the loader actually + // has a trampoline to map. When patched_data is None and there's no + // trampoline (e.g. the rewriter declined the binary), the runtime + // fallback must remain enabled. + let has_loader_trampoline = self.patched_data.is_some() || self.parsed.has_trampoline(); + let suppress = has_loader_trampoline && self.patched_data.is_none(); + self.file.task.suppress_elf_runtime_patch.set(suppress); + let result = if let Some(ref data) = self.patched_data { + let mut mapper = PatchedMapper { + inner: &mut self.file, + data, + }; + self.parsed.load(&mut mapper, &mut &*platform) + } else { + self.parsed.load(&mut self.file, &mut &*platform) + }; + self.file.task.suppress_elf_runtime_patch.set(false); + + Ok(result?) } } @@ -207,18 +367,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 03bf151ad..d1f219579 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -536,6 +536,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); }; diff --git a/litebox_shim_linux/src/syscalls/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index ce6c3513c..453039cba 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -4,10 +4,11 @@ //! 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 litebox::{ mm::linux::{MappingError, PAGE_SIZE, PageRange}, platform::{ - PageManagementProvider, RawConstPointer, RawMutPointer, + PageManagementProvider, RawConstPointer, RawMutPointer, SystemInfoProvider, page_mgmt::{FixedAddressBehavior, MemoryRegionPermissions}, }, }; @@ -17,6 +18,39 @@ use crate::MutPtr; use crate::ShimFS; use crate::Task; +/// 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 { + /// Base virtual address of the ELF (recorded from first mmap at offset 0). + pub _base_addr: usize, + /// 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, + /// For pre-patched binaries: virtual address offset of the trampoline in the ELF. + pub _trampoline_vaddr: 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 path of the ELF (from the fd path table, if available). + #[allow(dead_code)] + pub file_path: Option, +} + +/// 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 +110,42 @@ 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. + // Suppressed during ELF loader's load() sequence because the loader + // maps the trampoline itself via load_trampoline(). Running both + // paths would double-map the trampoline, with the second MAP_FIXED + // destroying the first mapping. + if !self.suppress_elf_runtime_patch.get() { + 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 offset == 0 { + // First mmap at offset 0: record the base address for later patching. + 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 +416,472 @@ 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. + #[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]; + if self.sys_read(fd, &mut ehdr_buf, Some(0)).is_err() { + return; // Not readable, 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()); + + // Read program headers to find max PT_LOAD end + let phdrs_size = e_phentsize * e_phnum; + if phdrs_size == 0 || phdrs_size > 0x10000 { + return; // Sanity check + } + let mut phdrs_buf = alloc::vec![0u8; phdrs_size]; + if self.sys_read(fd, &mut phdrs_buf, Some(e_phoff)).is_err() { + 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 != 1 { + // PT_LOAD = 1 + 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 end = p_vaddr + p_memsz; + 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 == 3 { + // 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 == 3 { + 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 { + _base_addr: base_addr, + pre_patched, + trampoline_file_offset: tramp_file_offset, + trampoline_file_size: tramp_file_size as usize, + _trampoline_vaddr: tramp_vaddr as usize, + trampoline_addr: trampoline_vaddr, + trampoline_cursor: 0, + trampoline_mapped: false, + trampoline_mapped_len: 0, + runtime_patches_committed: false, + file_path: None, + }); + } + + /// 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) { + let Ok(stat) = self.sys_fstat(fd) else { + return (false, 0, 0, 0); + }; + let file_size = stat.st_size; + if file_size < 32 { + return (false, 0, 0, 0); + } + let mut tail = [0u8; 32]; + if self.sys_read(fd, &mut tail, Some(file_size - 32)).is_err() { + return (false, 0, 0, 0); + } + if &tail[0..8] != litebox_syscall_rewriter::TRAMPOLINE_MAGIC { + return (false, 0, 0, 0); + } + // Parse header: magic(8) | file_offset(8) | vaddr(8) | size(8) + 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)] + 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. + if offset == 0 { + self.init_elf_patch_state(fd, mapped_addr.as_usize()); + } + + 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. + 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, + ) + .or_else(|_| { + self.do_mmap_anonymous( + Some(tramp_addr), + tramp_len, + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + MapFlags::MAP_ANONYMOUS | MapFlags::MAP_PRIVATE, + ) + }); + 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, + ) + .or_else(|_| { + self.do_mmap_anonymous( + Some(tramp_addr), + PAGE_SIZE, + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + MapFlags::MAP_ANONYMOUS | MapFlags::MAP_PRIVATE, + ) + }); + let actual_addr = match actual_addr { + Ok(ptr) => ptr.as_usize(), + Err(_) => return true, + }; + + // Verify the trampoline is within JMP rel32 range (+-2GB) of the code. + let distance = actual_addr.abs_diff(addr_usize); + if distance > 0x7FFF_0000 { + 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() + { + 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; + } + + 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() + { + return true; + } + + // Make the code segment writable for in-place patching. + if self + .sys_mprotect( + mapped_addr, + len, + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + ) + .is_err() + { + return true; + } + + // Read the mapped code into a buffer, patch it, write back. + let Some(code_owned) = mapped_addr.to_owned_slice(len) else { + 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 mut skipped_addrs = alloc::vec::Vec::new(); + let patch_result = litebox_syscall_rewriter::patch_code_segment( + &mut code_buf, + code_vaddr, + trampoline_write_vaddr, + syscall_entry_addr, + &mut skipped_addrs, + ); + if !skipped_addrs.is_empty() { + litebox::log_println!( + self.global.platform, + "warning: {} syscall instruction(s) could not be patched (addresses: {:?})", + skipped_addrs.len(), + skipped_addrs, + ); + } + match patch_result { + Ok(stubs) if !stubs.is_empty() => { + let Some(new_cursor) = state.trampoline_cursor.checked_add(stubs.len()) else { + 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, + ) + .is_err() + { + 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() { + 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() { + 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(_) => { + 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_linux/src/syscalls/process.rs b/litebox_shim_linux/src/syscalls/process.rs index 70f878cde..419afb09c 100644 --- a/litebox_shim_linux/src/syscalls/process.rs +++ b/litebox_shim_linux/src/syscalls/process.rs @@ -770,6 +770,7 @@ impl Task { fs: fs.into(), files: self.files.clone(), // TODO: !CLONE_FILES support signals: self.signals.clone_for_new_task(), + suppress_elf_runtime_patch: core::cell::Cell::new(false), }, }), ) From 0b2c4326bbedd82e4d4e92b22b03d170850057f2 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 2 Apr 2026 07:53:07 -0700 Subject: [PATCH 04/14] Fix Windows CI: suppress dead_code warning for syscall_callback extern --- litebox_platform_windows_userland/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/litebox_platform_windows_userland/src/lib.rs b/litebox_platform_windows_userland/src/lib.rs index ed694f3ee..d6d22b702 100644 --- a/litebox_platform_windows_userland/src/lib.rs +++ b/litebox_platform_windows_userland/src/lib.rs @@ -1963,6 +1963,7 @@ impl litebox::mm::allocator::MemoryProvider for WindowsUserland { unsafe extern "C" { // Defined in asm blocks above + #[allow(dead_code)] // Referenced from inline asm, not directly from Rust fn syscall_callback() -> isize; fn syscall_callback_redzone() -> isize; fn exception_callback() -> isize; From d7ee8e8eb30b33bc066b4d01a62f41f3b4b0988e Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 2 Apr 2026 08:03:08 -0700 Subject: [PATCH 05/14] Fix Windows CI: remove rtld_audit.so from Windows test (incompatible with new trampoline format) --- litebox_runner_linux_on_windows_userland/src/lib.rs | 10 +++------- .../tests/loader.rs | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/litebox_runner_linux_on_windows_userland/src/lib.rs b/litebox_runner_linux_on_windows_userland/src/lib.rs index c5afcbc71..063c7197e 100644 --- a/litebox_runner_linux_on_windows_userland/src/lib.rs +++ b/litebox_runner_linux_on_windows_userland/src/lib.rs @@ -130,11 +130,7 @@ pub fn run(cli_args: CliArgs) -> Result<()> { } 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()); - } + let _ = envp; + // No environment fixups needed — the shim's mmap hook handles + // syscall patching at runtime without LD_AUDIT. } diff --git a/litebox_runner_linux_on_windows_userland/tests/loader.rs b/litebox_runner_linux_on_windows_userland/tests/loader.rs index e6f470e34..b83fdb056 100644 --- a/litebox_runner_linux_on_windows_userland/tests/loader.rs +++ b/litebox_runner_linux_on_windows_userland/tests/loader.rs @@ -361,7 +361,7 @@ 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")]; + let libs_without_rewrite: [(&str, &str); 0] = []; // Run run_dynamic_linked_prog_with_rewriter( From dc399b67fc30890097bfa58bb9c30a5d9f4c231b Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 2 Apr 2026 12:07:06 -0700 Subject: [PATCH 06/14] Windows: preserve guest R11 across syscall callback via TEB.ArbitraryUserPointer The new trampoline format loads a restart address into R11 (for SA_RESTART) before jumping to the callback. On Windows, the TLS index lookup clobbers R11, so we temporarily stash R11 in the per-thread TEB.ArbitraryUserPointer slot (gs:[0x28]) for the ~20 instructions of inline asm between callback entry and pt_regs save. Also removes the dead syscall_callback entry point (only syscall_callback_redzone is used since get_syscall_entry_point always returns the redzone variant). --- litebox_platform_windows_userland/src/lib.rs | 38 +++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/litebox_platform_windows_userland/src/lib.rs b/litebox_platform_windows_userland/src/lib.rs index d6d22b702..19a5256bd 100644 --- a/litebox_platform_windows_userland/src/lib.rs +++ b/litebox_platform_windows_userland/src/lib.rs @@ -549,31 +549,26 @@ unsafe extern "C-unwind" fn run_thread_arch(thread_ctx: &mut ThreadContext, tls_ jmp .Ldone // This entry point is called from the guest when it issues a syscall - // instruction. + // instruction. The rewriter trampoline has already: + // 1. Reserved 128 bytes below RSP to protect the SysV red zone + // 2. Loaded the call-site restart address into R11 (for SA_RESTART) + // 3. Loaded the return address into RCX // - // At entry, the register context is the guest context with the - // return address in rcx. r11 is an available scratch register (it would - // contain rflags if the syscall instruction had actually been issued). - .globl syscall_callback -syscall_callback: - // Get the TLS state from the TLS slot and clear the in-guest flag. - mov r11d, DWORD PTR [rip + {TLS_INDEX}] - mov r11, QWORD PTR gs:[r11 * 8 + TEB_TLS_SLOTS_OFFSET] - mov BYTE PTR [r11 + {IS_IN_GUEST}], 0 - // Set rsp to the top of the guest context. - mov QWORD PTR [r11 + {SCRATCH}], rsp - jmp .Lsyscall_callback_common - + // All other registers hold guest state. .globl syscall_callback_redzone syscall_callback_redzone: - // Same as syscall_callback, but the trampoline has already reserved - // 128 bytes below RSP to protect the SysV red zone. Recover the - // architectural guest stack pointer. + // Save guest R11 (restart address from rewriter trampoline) into + // TEB.ArbitraryUserPointer (gs:[0x28]) before the TLS index lookup + // clobbers R11. This slot is per-thread and the window is very + // narrow: only ~20 instructions of inline asm with no API calls, + // no Rust code, and no DLL activity, so the ntdll loader (which + // also uses this slot for debugger communication) cannot interfere. + mov gs:[0x28], r11 + // Get the TLS state from the TLS slot and clear the in-guest flag. mov r11d, DWORD PTR [rip + {TLS_INDEX}] mov r11, QWORD PTR gs:[r11 * 8 + TEB_TLS_SLOTS_OFFSET] mov BYTE PTR [r11 + {IS_IN_GUEST}], 0 - // Save RSP + 128 to SCRATCH without clobbering any guest registers. - // Use SCRATCH as a temporary: store rsp, then add 128 in-place. + // Recover the architectural guest stack pointer (RSP + 128) into SCRATCH. mov QWORD PTR [r11 + {SCRATCH}], rsp add QWORD PTR [r11 + {SCRATCH}], 128 @@ -597,7 +592,8 @@ syscall_callback_redzone: push r8 // pt_regs->r8 push r9 // pt_regs->r9 push r10 // pt_regs->r10 - push [rsp + 88] // pt_regs->r11 = rflags + mov r10, gs:[0x28] // recover guest R11 saved at entry + push r10 // pt_regs->r11 = guest R11 (restart addr from rewriter) push rbx // pt_regs->bx push rbp // pt_regs->bp push r12 @@ -1963,8 +1959,6 @@ impl litebox::mm::allocator::MemoryProvider for WindowsUserland { unsafe extern "C" { // Defined in asm blocks above - #[allow(dead_code)] // Referenced from inline asm, not directly from Rust - fn syscall_callback() -> isize; fn syscall_callback_redzone() -> isize; fn exception_callback() -> isize; fn interrupt_callback(); From 0d3b29bb671240ab0a30fcc4316f97eb49c9d89f Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 2 Apr 2026 14:58:36 -0700 Subject: [PATCH 07/14] Remove rtld_audit, fix RFLAGS on Windows, simplify callback dispatch, discriminate rewriter errors - Remove litebox_rtld_audit/ directory entirely (Makefile, rtld_audit.c, .gitignore) - Replace litebox_packager/build.rs with no-op (was building rtld_audit.so) - Remove rtld_audit tar entry from litebox_packager/src/lib.rs - Remove fixup_env and set_load_filter from both Linux and LoW runners - Fix RFLAGS clobber on Windows: use lea+mov instead of mov+add - Simplify is_at_syscall_callback: x86 checks syscall_callback, x86_64 checks syscall_callback_redzone - Discriminate trampoline parse errors: only UnpatchedBinary triggers runtime patching - Discriminate rewriter errors: expected non-fatal vs unexpected with logging - Restore fork-vfork patch error path from PR 1c - Simplify suppress_elf_runtime_patch logic - Clean up rtld_audit references in comments across codebase --- dev_bench/unixbench/prepare_unixbench.py | 4 +- dev_tests/src/boilerplate.rs | 1 - litebox_packager/build.rs | 39 +- litebox_packager/src/lib.rs | 17 - litebox_platform_linux_userland/src/lib.rs | 4 +- litebox_platform_windows_userland/src/lib.rs | 6 +- litebox_rtld_audit/.gitignore | 1 - litebox_rtld_audit/Makefile | 26 -- litebox_rtld_audit/rtld_audit.c | 384 ------------------ .../src/lib.rs | 19 +- .../tests/loader.rs | 32 +- litebox_runner_linux_userland/build.rs | 6 - litebox_runner_linux_userland/src/lib.rs | 7 +- litebox_runner_linux_userland/tests/run.rs | 2 +- litebox_shim_linux/src/loader/elf.rs | 62 ++- 15 files changed, 68 insertions(+), 542 deletions(-) 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/dev_bench/unixbench/prepare_unixbench.py b/dev_bench/unixbench/prepare_unixbench.py index 0d472d505..4eee4e6e1 100644 --- a/dev_bench/unixbench/prepare_unixbench.py +++ b/dev_bench/unixbench/prepare_unixbench.py @@ -61,8 +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 + 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_packager/build.rs b/litebox_packager/build.rs index 77956be92..f189226e4 100644 --- a/litebox_packager/build.rs +++ b/litebox_packager/build.rs @@ -1,43 +1,6 @@ // 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"); + // rtld_audit has been removed; nothing to build. } diff --git a/litebox_packager/src/lib.rs b/litebox_packager/src/lib.rs index adfd1543d..0b7afed8c 100644 --- a/litebox_packager/src/lib.rs +++ b/litebox_packager/src/lib.rs @@ -358,23 +358,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_platform_linux_userland/src/lib.rs b/litebox_platform_linux_userland/src/lib.rs index 4c06e3c8a..777bfeada 100644 --- a/litebox_platform_linux_userland/src/lib.rs +++ b/litebox_platform_linux_userland/src/lib.rs @@ -2748,10 +2748,10 @@ unsafe fn interrupt_signal_handler( // FUTURE: handle trampoline code, too. This is somewhat less important // because it's probably fine for the shim to observe a guest context that // is inside the trampoline. + #[cfg(target_arch = "x86")] let is_at_syscall_callback = ip == syscall_callback as *const () as usize; #[cfg(target_arch = "x86_64")] - let is_at_syscall_callback = - is_at_syscall_callback || ip == syscall_callback_redzone as *const () as usize; + let is_at_syscall_callback = ip == syscall_callback_redzone as *const () as usize; if is_at_syscall_callback { // No need to clear `in_guest` or set interrupt; the syscall handler will // clear `in_guest` and call into the shim. diff --git a/litebox_platform_windows_userland/src/lib.rs b/litebox_platform_windows_userland/src/lib.rs index 19a5256bd..b43f2f790 100644 --- a/litebox_platform_windows_userland/src/lib.rs +++ b/litebox_platform_windows_userland/src/lib.rs @@ -568,9 +568,11 @@ syscall_callback_redzone: mov r11d, DWORD PTR [rip + {TLS_INDEX}] mov r11, QWORD PTR gs:[r11 * 8 + TEB_TLS_SLOTS_OFFSET] mov BYTE PTR [r11 + {IS_IN_GUEST}], 0 - // Recover the architectural guest stack pointer (RSP + 128) into SCRATCH. + // Recover the architectural guest stack pointer (undo the 128-byte + // red zone reservation) and store it in SCRATCH. LEA is used instead + // of ADD to avoid clobbering RFLAGS before pushfq. + lea rsp, [rsp + 128] mov QWORD PTR [r11 + {SCRATCH}], rsp - add QWORD PTR [r11 + {SCRATCH}], 128 .Lsyscall_callback_common: mov rsp, QWORD PTR [r11 + {GUEST_CONTEXT_TOP}] 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 063c7197e..826d42923 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`). @@ -60,7 +60,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. @@ -83,7 +83,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 @@ -128,9 +127,3 @@ pub fn run(cli_args: CliArgs) -> Result<()> { } std::process::exit(program.process.wait()) } - -fn fixup_env(envp: &mut Vec) { - let _ = envp; - // No environment fixups needed — the shim's mmap hook handles - // syscall patching at runtime without LD_AUDIT. -} diff --git a/litebox_runner_linux_on_windows_userland/tests/loader.rs b/litebox_runner_linux_on_windows_userland/tests/loader.rs index b83fdb056..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: [(&str, &str); 0] = []; - // 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 f189226e4..000000000 --- a/litebox_runner_linux_userland/build.rs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -fn main() { - // rtld_audit has been removed; nothing to build. -} diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 6f56152b1..cbfba14d6 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -224,7 +224,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); @@ -330,7 +330,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)); @@ -450,7 +449,3 @@ fn pin_thread_to_cpu(cpu: usize) { } } } - -fn fixup_env(_envp: &mut Vec) { - // No-op: rtld_audit has been removed; runtime patching is handled by the shim. -} diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 219d27da1..e53303165 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -206,7 +206,7 @@ fn find_c_test_files(dir: &str) -> Vec { files } -// our rtld_audit does not support x86 yet +// Syscall rewriting does not support x86 yet #[cfg(target_arch = "x86_64")] #[test] fn test_dynamic_lib_with_rewriter() { diff --git a/litebox_shim_linux/src/loader/elf.rs b/litebox_shim_linux/src/loader/elf.rs index 6cbebaf98..8935ebbe3 100644 --- a/litebox_shim_linux/src/loader/elf.rs +++ b/litebox_shim_linux/src/loader/elf.rs @@ -11,15 +11,15 @@ use litebox::{ utils::{ReinterpretSignedExt, TruncateExt}, }; use litebox_common_linux::{ - MapFlags, errno::Errno, loader::{ElfParsedFile, ReadAt as _}, + MapFlags, }; use thiserror::Error; use crate::{ - MutPtr, loader::auxv::{AuxKey, AuxVec}, + MutPtr, }; use super::stack::UserStack; @@ -259,7 +259,15 @@ impl<'a, FS: ShimFS> FileAndParsed<'a, FS> { // If the rewriter backend is active (syscall_entry_point != 0) and the // binary lacks a trampoline, patch it on the fly so that both the main // program and the dynamic linker are covered. - let patched_data = if syscall_entry_point != 0 && trampoline_result.is_err() { + // + // Only attempt runtime patching for UnpatchedBinary — other errors + // (BadTrampolineVersion, BadTrampoline, Io) indicate a corrupt or + // incompatible pre-patched binary that should not be re-patched. + let patched_data = if syscall_entry_point != 0 + && matches!( + trampoline_result, + Err(litebox_common_linux::loader::ElfParseError::UnpatchedBinary) + ) { let size: usize = (&mut &file) .size() .map_err(ElfLoaderError::OpenError)? @@ -289,12 +297,39 @@ impl<'a, FS: ShimFS> FileAndParsed<'a, FS> { .map_err(ElfLoaderError::ParseError)?; Some(patched) } - Err(_) => { - // Patching failed (e.g. ET_REL, no .text). Proceed without - // a trampoline — the binary may simply have no syscalls. + Err( + litebox_syscall_rewriter::Error::UnsupportedBunExecutable + | litebox_syscall_rewriter::Error::UnsupportedObjectFile + | litebox_syscall_rewriter::Error::NoTextSectionFound + | litebox_syscall_rewriter::Error::NoSyscallInstructionsFound + | litebox_syscall_rewriter::Error::AlreadyHooked, + ) => { + // These are expected non-fatal cases: + // - BUN: can't be statically patched but the runtime mmap + // hook will patch code segments as they are mapped. + // - Object files / no .text / no syscalls / already hooked: + // nothing to patch. + None + } + Err(e) => { + // Unexpected rewriter failure (parse error, disassembly + // failure, etc.). Proceed without a trampoline — the + // runtime mmap hook may still patch individual segments. + litebox::log_println!( + task.global.platform, + "warning: syscall rewriter failed: {}; \ + falling back to runtime patching", + e + ); None } } + } else if syscall_entry_point != 0 { + // Rewriter is active but trampoline_result is an error other than + // UnpatchedBinary (e.g. BadTrampolineVersion, BadTrampoline, Io). + // Propagate the error rather than silently proceeding. + trampoline_result.map_err(ElfLoaderError::ParseError)?; + None } else { None }; @@ -317,14 +352,13 @@ impl<'a, FS: ShimFS> FileAndParsed<'a, FS> { // this, both paths would map the same region — the second MAP_FIXED // destroys the first mapping. // - // Only suppress when using the ElfFile mapper (which routes through - // do_mmap_file → maybe_patch_exec_segment) AND the loader actually - // has a trampoline to map. When patched_data is None and there's no - // trampoline (e.g. the rewriter declined the binary), the runtime - // fallback must remain enabled. - let has_loader_trampoline = self.patched_data.is_some() || self.parsed.has_trampoline(); - let suppress = has_loader_trampoline && self.patched_data.is_none(); - self.file.task.suppress_elf_runtime_patch.set(suppress); + // When patched_data is Some the PatchedMapper path doesn't go through + // do_mmap_file so the flag is a no-op, but setting it is harmless and + // keeps the logic simple. + self.file + .task + .suppress_elf_runtime_patch + .set(self.patched_data.is_some() || self.parsed.has_trampoline()); let result = if let Some(ref data) = self.patched_data { let mut mapper = PatchedMapper { inner: &mut self.file, From 68c634fc360fef9e837d87ee3b8ac56e94af9832 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 2 Apr 2026 20:07:04 -0700 Subject: [PATCH 08/14] Clean up rewriter: remove unit tests, revert bun footer to suffix check, add x86_64 comment, add post-syscall RIP-relative comment, fix formatting --- litebox_shim_linux/src/loader/elf.rs | 4 +- litebox_shim_linux/src/syscalls/mm.rs | 2 + litebox_syscall_rewriter/src/lib.rs | 103 ++------------------------ 3 files changed, 12 insertions(+), 97 deletions(-) diff --git a/litebox_shim_linux/src/loader/elf.rs b/litebox_shim_linux/src/loader/elf.rs index 8935ebbe3..b9a345483 100644 --- a/litebox_shim_linux/src/loader/elf.rs +++ b/litebox_shim_linux/src/loader/elf.rs @@ -11,15 +11,15 @@ use litebox::{ utils::{ReinterpretSignedExt, TruncateExt}, }; use litebox_common_linux::{ + MapFlags, errno::Errno, loader::{ElfParsedFile, ReadAt as _}, - MapFlags, }; use thiserror::Error; use crate::{ - loader::auxv::{AuxKey, AuxVec}, MutPtr, + loader::auxv::{AuxKey, AuxVec}, }; use super::stack::UserStack; diff --git a/litebox_shim_linux/src/syscalls/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index 453039cba..d20583b3f 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -424,6 +424,8 @@ impl Task { /// 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. diff --git a/litebox_syscall_rewriter/src/lib.rs b/litebox_syscall_rewriter/src/lib.rs index 7a0ff6eaa..bd7cc143a 100644 --- a/litebox_syscall_rewriter/src/lib.rs +++ b/litebox_syscall_rewriter/src/lib.rs @@ -843,10 +843,8 @@ fn find_fork_vfork_patch( /// Check if the input binary has the Bun footer marker near the end. fn has_bun_footer_marker(input_binary: &[u8]) -> bool { - let window_len = input_binary.len().min(256); - input_binary[input_binary.len().saturating_sub(window_len)..] - .windows(BUN_FOOTER_MARKER.len()) - .any(|window| window == BUN_FOOTER_MARKER) + input_binary.len() >= BUN_FOOTER_MARKER.len() + && input_binary[input_binary.len() - BUN_FOOTER_MARKER.len()..] == *BUN_FOOTER_MARKER } /// Replace an unpatchable syscall instruction with `ICEBP; HLT` (`F1 F4`) so @@ -1150,6 +1148,12 @@ fn hook_syscall_and_after( } let replace_end = replace_end.unwrap(); + // This function copies post-syscall instructions to the trampoline as raw + // bytes (no re-encoding). That only works for position-independent + // instructions. If any post-syscall instruction has a RIP-relative memory + // operand, the raw bytes would reference the wrong address from the + // trampoline's location, so fall back to hook_syscall_before_and_after + // which re-encodes both sides with corrected displacements. let copied_postsyscall_insts_have_ip_rel_mem = arch == Arch::X86_64 && instruction_slice_has_ip_rel_memory_operand( instructions @@ -1422,94 +1426,3 @@ fn hook_syscall_before_and_after( Ok(()) } - -#[cfg(test)] -mod tests { - use super::{has_bun_footer_marker, patch_code_segment, BUN_FOOTER_MARKER}; - - #[test] - fn detects_bun_footer_marker_near_end() { - let mut bytes = vec![0u8; 512]; - let offset = bytes.len() - BUN_FOOTER_MARKER.len() - 8; - bytes[offset..offset + BUN_FOOTER_MARKER.len()].copy_from_slice(BUN_FOOTER_MARKER); - assert!(has_bun_footer_marker(&bytes)); - } - - #[test] - fn ignores_missing_bun_footer_marker() { - let bytes = vec![0u8; 512]; - assert!(!has_bun_footer_marker(&bytes)); - } - - #[test] - fn patch_code_segment_relocates_rip_relative_presyscall_to_trampoline() { - let mut code = vec![ - 0x48, 0x8D, 0x35, 0x10, 0x00, 0x00, 0x00, // lea rsi, [rip + 0x10] @ 0x1000 - 0x0F, 0x05, // syscall @ 0x1007 - 0x31, 0xC0, // xor eax, eax - 0xBA, 0x01, 0x00, 0x00, 0x00, // mov edx, 1 - ]; - - let trampoline = patch_code_segment(&mut code, 0x1000, 0x8000, 0x9000, &mut Vec::new()) - .expect("patch_code_segment should succeed"); - - assert!(!trampoline.is_empty()); - // The lea + syscall region (9 bytes starting at 0x1000) should now be a - // JMP to the trampoline followed by NOPs. - assert_eq!(code[0], 0xE9, "replace region should start with JMP rel32"); - // The trampoline should contain the re-encoded lea with an adjusted - // RIP-relative displacement targeting the same absolute address. - // Original: lea targets 0x1007 + 0x10 = 0x1017. - // Re-encoded at 0x8000: displacement = 0x1017 - (0x8000 + 7) = -0x6FF0 = 0xFFFF9010 - #[allow(clippy::cast_possible_truncation)] - let expected_disp: i32 = 0x1017_i64.wrapping_sub(0x8000 + 7) as i32; - assert_eq!( - &trampoline[3..7], - &expected_disp.to_le_bytes(), - "re-encoded lea displacement should target the original address" - ); - } - - #[test] - fn patch_code_segment_handles_rip_relative_on_both_sides_of_syscall() { - let mut code = vec![ - 0x48, 0x8D, 0x35, 0x10, 0x00, 0x00, 0x00, // lea rsi, [rip + 0x10] @ 0x1000 - 0x0F, 0x05, // syscall @ 0x1007 - 0x48, 0x8D, 0x3D, 0x10, 0x00, 0x00, 0x00, // lea rdi, [rip + 0x10] - ]; - - let mut skipped = Vec::new(); - let stubs = patch_code_segment(&mut code, 0x1000, 0x8000, 0x9000, &mut skipped) - .expect("patch_code_segment should succeed"); - // The pre-syscall lea is re-encoded in the trampoline; the - // post-syscall lea stays in place (not overwritten). - assert!(!stubs.is_empty(), "should be patched via re-encoding"); - assert_eq!(code[0], 0xE9, "replace region should start with JMP"); - assert!(skipped.is_empty(), "nothing should be skipped"); - } - - #[test] - fn patch_code_segment_patches_all_syscalls_including_rip_relative() { - let mut code = vec![ - // First syscall: patchable (3 nops before = 5 bytes total with syscall) - 0x90, 0x90, 0x90, // nop; nop; nop - 0x0F, 0x05, // syscall @ offset 3 - 0xC3, // ret - // Second syscall: RIP-relative before, now patchable via re-encoding - 0x48, 0x8D, 0x35, 0x10, 0x00, 0x00, 0x00, // lea rsi, [rip+0x10] - 0x0F, 0x05, // syscall @ offset 13 - 0x48, 0x8D, 0x3D, 0x10, 0x00, 0x00, 0x00, // lea rdi, [rip+0x10] - ]; - - let mut skipped = Vec::new(); - let stubs = patch_code_segment(&mut code, 0x1000, 0x8000, 0x9000, &mut skipped).unwrap(); - - assert!(!stubs.is_empty(), "both syscalls should be patched"); - assert_eq!(code[0], 0xE9, "first syscall site should be a JMP"); - assert_eq!( - code[6], 0xE9, - "second syscall site (lea start) should be a JMP" - ); - assert!(skipped.is_empty(), "nothing should be skipped"); - } -} From 231691d6470d09105a0b2c457c4248d1a80d440e Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 2 Apr 2026 20:54:54 -0700 Subject: [PATCH 09/14] Fix signal handler to check both callback entry points, add short-read guards, remove unused ElfPatchState fields --- litebox_platform_linux_userland/src/lib.rs | 3 ++- litebox_shim_linux/src/syscalls/mm.rs | 25 ++++++++-------------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/litebox_platform_linux_userland/src/lib.rs b/litebox_platform_linux_userland/src/lib.rs index 777bfeada..871c4ccdd 100644 --- a/litebox_platform_linux_userland/src/lib.rs +++ b/litebox_platform_linux_userland/src/lib.rs @@ -2751,7 +2751,8 @@ unsafe fn interrupt_signal_handler( #[cfg(target_arch = "x86")] let is_at_syscall_callback = ip == syscall_callback as *const () as usize; #[cfg(target_arch = "x86_64")] - let is_at_syscall_callback = ip == syscall_callback_redzone as *const () as usize; + let is_at_syscall_callback = ip == syscall_callback_redzone as *const () as usize + || ip == syscall_callback as *const () as usize; if is_at_syscall_callback { // No need to clear `in_guest` or set interrupt; the syscall handler will // clear `in_guest` and call into the shim. diff --git a/litebox_shim_linux/src/syscalls/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index d20583b3f..ab6069b24 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -23,15 +23,11 @@ use crate::Task; /// Tracks base address and trampoline write cursor for each ELF file that /// has executable segments mapped via `do_mmap_file()`. pub(crate) struct ElfPatchState { - /// Base virtual address of the ELF (recorded from first mmap at offset 0). - pub _base_addr: usize, /// 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, - /// For pre-patched binaries: virtual address offset of the trampoline in the ELF. - pub _trampoline_vaddr: usize, /// Start address of the trampoline region (runtime). pub trampoline_addr: usize, /// Current write position within the trampoline (byte offset from `trampoline_addr`). @@ -43,9 +39,6 @@ pub(crate) struct ElfPatchState { /// Whether any runtime-generated stubs were successfully linked from code /// in this fd to the trampoline. pub runtime_patches_committed: bool, - /// File path of the ELF (from the fd path table, if available). - #[allow(dead_code)] - pub file_path: Option, } /// Per-process collection of ELF patching state, keyed by fd number. @@ -435,8 +428,9 @@ impl Task { // Read the ELF header (first 64 bytes covers both 32-bit and 64-bit). let mut ehdr_buf = [0u8; 64]; - if self.sys_read(fd, &mut ehdr_buf, Some(0)).is_err() { - return; // Not readable, skip + 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 @@ -456,8 +450,9 @@ impl Task { return; // Sanity check } let mut phdrs_buf = alloc::vec![0u8; phdrs_size]; - if self.sys_read(fd, &mut phdrs_buf, Some(e_phoff)).is_err() { - return; + 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) @@ -509,17 +504,14 @@ impl Task { // Insert under lock (re-check for races). let mut cache = self.global.elf_patch_cache.lock(); cache.entry(fd).or_insert(ElfPatchState { - _base_addr: base_addr, pre_patched, trampoline_file_offset: tramp_file_offset, trampoline_file_size: tramp_file_size as usize, - _trampoline_vaddr: tramp_vaddr as usize, trampoline_addr: trampoline_vaddr, trampoline_cursor: 0, trampoline_mapped: false, trampoline_mapped_len: 0, runtime_patches_committed: false, - file_path: None, }); } @@ -534,8 +526,9 @@ impl Task { return (false, 0, 0, 0); } let mut tail = [0u8; 32]; - if self.sys_read(fd, &mut tail, Some(file_size - 32)).is_err() { - return (false, 0, 0, 0); + match self.sys_read(fd, &mut tail, Some(file_size - 32)) { + Ok(n) if n == tail.len() => {} + _ => return (false, 0, 0, 0), } if &tail[0..8] != litebox_syscall_rewriter::TRAMPOLINE_MAGIC { return (false, 0, 0, 0); From 7e79881950d245850d6f0f8ebcaf0a53161a16e6 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 2 Apr 2026 21:19:26 -0700 Subject: [PATCH 10/14] Fix integration tests: replace OUT_DIR with CARGO_TARGET_TMPDIR Deleting litebox_runner_linux_userland/build.rs (rtld_audit removal) also removed Cargo's OUT_DIR env var from integration tests. Replace the three call sites with env!("CARGO_TARGET_TMPDIR"), a compile-time macro available since Rust 1.68 that requires no build.rs. --- litebox_runner_linux_userland/tests/common/mod.rs | 2 +- litebox_runner_linux_userland/tests/loader.rs | 2 +- litebox_runner_linux_userland/tests/run.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/litebox_runner_linux_userland/tests/common/mod.rs b/litebox_runner_linux_userland/tests/common/mod.rs index e9b6a9810..3f761f64a 100644 --- a/litebox_runner_linux_userland/tests/common/mod.rs +++ b/litebox_runner_linux_userland/tests/common/mod.rs @@ -80,7 +80,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 9850ba843..2ff79f97c 100644 --- a/litebox_runner_linux_userland/tests/loader.rs +++ b/litebox_runner_linux_userland/tests/loader.rs @@ -234,7 +234,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 e53303165..3da964a4f 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -32,7 +32,7 @@ impl Runner { Backend::Rewriter => "rewriter", Backend::Seccomp => "seccomp", }; - let dir_path = PathBuf::from(std::env::var_os("OUT_DIR").unwrap()); + let dir_path = PathBuf::from(env!("CARGO_TARGET_TMPDIR")); let path = match backend { Backend::Seccomp => target.to_path_buf(), Backend::Rewriter => { From f1349cc252c44bf8bc870ea9acd5810ebd0ab67a Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Sat, 4 Apr 2026 21:14:32 -0700 Subject: [PATCH 11/14] Run cargo fmt --all (comment alignment after rebase) --- litebox_syscall_rewriter/src/lib.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/litebox_syscall_rewriter/src/lib.rs b/litebox_syscall_rewriter/src/lib.rs index bd7cc143a..1b723f7ce 100644 --- a/litebox_syscall_rewriter/src/lib.rs +++ b/litebox_syscall_rewriter/src/lib.rs @@ -606,8 +606,8 @@ fn hook_syscalls_in_section( trampoline_data.extend_from_slice(&[0xE8, 0x0, 0x0, 0x0, 0x0]); // CALL next instruction trampoline_data.push(0x58); // POP EAX (effectively store IP in EAX) trampoline_data.extend_from_slice(&[0xFF, 0x90]); // CALL [EAX + offset] - // EAX = trampoline_base_addr + (trampoline_data.len() - 3) - // We want: EAX + offset = syscall_entry_addr + // EAX = trampoline_base_addr + (trampoline_data.len() - 3) + // We want: EAX + offset = syscall_entry_addr let call_base = checked_add_u64( trampoline_base_addr, trampoline_data.len() as u64 - 3, @@ -1219,8 +1219,8 @@ fn hook_syscall_and_after( trampoline_data.extend_from_slice(&[0xE8, 0x0, 0x0, 0x0, 0x0]); // CALL next instruction trampoline_data.push(0x58); // POP EAX (effectively store IP in EAX) trampoline_data.extend_from_slice(&[0xFF, 0x90]); // CALL [EAX + offset] - // EAX = trampoline_base_addr + (trampoline_data.len() - 3) - // We want: EAX + offset = syscall_entry_addr + // EAX = trampoline_base_addr + (trampoline_data.len() - 3) + // We want: EAX + offset = syscall_entry_addr let call_base = checked_add_u64( trampoline_base_addr, trampoline_data.len() as u64, @@ -1371,8 +1371,8 @@ fn hook_syscall_before_and_after( trampoline_data.extend_from_slice(&[0xE8, 0x0, 0x0, 0x0, 0x0]); // CALL next instruction trampoline_data.push(0x58); // POP EAX (effectively store IP in EAX) trampoline_data.extend_from_slice(&[0xFF, 0x90]); // CALL [EAX + offset] - // EAX = trampoline_base_addr + (trampoline_data.len() - 3) - // We want: EAX + offset = syscall_entry_addr + // EAX = trampoline_base_addr + (trampoline_data.len() - 3) + // We want: EAX + offset = syscall_entry_addr let call_base = checked_add_u64( trampoline_base_addr, trampoline_data.len() as u64, From 9d900b09017d74f8bc38a047de2afac40fe26588 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 8 Apr 2026 17:10:06 +0000 Subject: [PATCH 12/14] Address PR #739 review comments - Use EINVAL instead of ENODATA for trampoline parse failures (loader.rs) - Handle UnpatchedBinary as non-fatal in OptEE ELF loader (optee/elf.rs) - Document R11 restart-address contract in rewriter (lib.rs) - Replace unchecked arithmetic with checked_add_u64 in rewriter (lib.rs) - Rename saved_r11 to saved_restart_addr in Linux userland TLS (lib.rs) - Store RFLAGS from stack ([rsp+88]) instead of TLS in Linux/Windows userland pt_regs->r11 (lib.rs) - Save R11 restart address to TlsState on Windows userland (lib.rs) - Add cleanup-leak TODO comment in PatchedMapper::map_file (elf.rs) - Restore trampoline RX on mprotect failure path (mm.rs) - Make check_trampoline_magic pointer-width aware (mm.rs) - Validate e_phentsize before parsing program headers (mm.rs) - Clarify elf_patch_cache lock scope comment (mm.rs) - Finalize ELF patch for implicitly-closed fd in dup2/dup3 (file.rs) --- litebox_common_linux/src/loader.rs | 4 +- litebox_platform_linux_userland/src/lib.rs | 15 +++++--- litebox_platform_windows_userland/src/lib.rs | 13 +++++-- litebox_shim_linux/src/loader/elf.rs | 6 +++ litebox_shim_linux/src/syscalls/file.rs | 8 +++- litebox_shim_linux/src/syscalls/mm.rs | 40 ++++++++++++++++---- litebox_shim_optee/src/loader/elf.rs | 8 +++- litebox_syscall_rewriter/src/lib.rs | 26 +++++++++++-- 8 files changed, 95 insertions(+), 25 deletions(-) diff --git a/litebox_common_linux/src/loader.rs b/litebox_common_linux/src/loader.rs index 6b1fcc88b..1f714402e 100644 --- a/litebox_common_linux/src/loader.rs +++ b/litebox_common_linux/src/loader.rs @@ -584,9 +584,9 @@ impl ReadAt for &[u8] { fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), Self::Error> { let offset: usize = offset.truncate(); - let end = offset.checked_add(buf.len()).ok_or(Errno::ENODATA)?; + let end = offset.checked_add(buf.len()).ok_or(Errno::EINVAL)?; if end > self.len() { - return Err(Errno::ENODATA); + return Err(Errno::EINVAL); } buf.copy_from_slice(&self[offset..end]); Ok(()) diff --git a/litebox_platform_linux_userland/src/lib.rs b/litebox_platform_linux_userland/src/lib.rs index 871c4ccdd..8c9d57b02 100644 --- a/litebox_platform_linux_userland/src/lib.rs +++ b/litebox_platform_linux_userland/src/lib.rs @@ -537,7 +537,7 @@ core::arch::global_asm!( " .section .tbss .align 8 -saved_r11: +saved_restart_addr: .quad 0 scratch: .quad 0 @@ -653,9 +653,12 @@ syscall_callback: // expectations of `interrupt_signal_handler`. mov BYTE PTR gs:in_guest@tpoff, 0 - // Save guest R11 (syscall call-site address from rewriter trampoline) - // before it is clobbered by the fsbase/gsbase save sequence below. - mov gs:saved_r11@tpoff, r11 + // Save guest R11 (syscall call-site restart address from the rewriter + // trampoline) to TLS before it is clobbered by the fsbase/gsbase save + // sequence below. This value is not placed in pt_regs (which holds + // RFLAGS in the r11 slot per the kernel ABI); instead it is kept in + // TLS for future SA_RESTART support. + mov gs:saved_restart_addr@tpoff, r11 // Restore host fs base. rdfsbase r11 @@ -673,7 +676,7 @@ syscall_callback_redzone: // Same as syscall_callback, but the trampoline has already reserved // 128 bytes below RSP to protect the SysV red zone. mov BYTE PTR gs:in_guest@tpoff, 0 - mov gs:saved_r11@tpoff, r11 + mov gs:saved_restart_addr@tpoff, r11 rdfsbase r11 mov gs:guest_fsbase@tpoff, r11 rdgsbase r11 @@ -703,7 +706,7 @@ syscall_callback_redzone: push r8 // pt_regs->r8 push r9 // pt_regs->r9 push r10 // pt_regs->r10 - push QWORD PTR gs:saved_r11@tpoff // pt_regs->r11 (syscall call-site from rewriter) + push [rsp + 88] // pt_regs->r11 = rflags (matching real syscall ABI) push rbx // pt_regs->bx push rbp // pt_regs->bp push r12 // pt_regs->r12 diff --git a/litebox_platform_windows_userland/src/lib.rs b/litebox_platform_windows_userland/src/lib.rs index b43f2f790..61f9ff977 100644 --- a/litebox_platform_windows_userland/src/lib.rs +++ b/litebox_platform_windows_userland/src/lib.rs @@ -414,6 +414,10 @@ struct TlsState { host_bp: Cell<*mut u128>, guest_context_top: Cell<*mut litebox_common_linux::PtRegs>, scratch: Cell, + /// Syscall call-site restart address from the rewriter trampoline, + /// saved here for future SA_RESTART support. Not stored in pt_regs + /// (which holds RFLAGS in the r11 slot per the kernel ABI). + saved_restart_addr: Cell, is_in_guest: Cell, interrupt: Cell, continue_context: @@ -433,6 +437,7 @@ impl TlsState { host_bp: Cell::new(core::ptr::null_mut()), guest_context_top: core::ptr::null_mut::().into(), scratch: 0.into(), + saved_restart_addr: 0.into(), is_in_guest: false.into(), interrupt: false.into(), continue_context: Box::default(), @@ -557,7 +562,7 @@ unsafe extern "C-unwind" fn run_thread_arch(thread_ctx: &mut ThreadContext, tls_ // All other registers hold guest state. .globl syscall_callback_redzone syscall_callback_redzone: - // Save guest R11 (restart address from rewriter trampoline) into + // Save guest R11 (restart address from rewriter trampoline) on the // TEB.ArbitraryUserPointer (gs:[0x28]) before the TLS index lookup // clobbers R11. This slot is per-thread and the window is very // narrow: only ~20 instructions of inline asm with no API calls, @@ -568,6 +573,8 @@ syscall_callback_redzone: mov r11d, DWORD PTR [rip + {TLS_INDEX}] mov r11, QWORD PTR gs:[r11 * 8 + TEB_TLS_SLOTS_OFFSET] mov BYTE PTR [r11 + {IS_IN_GUEST}], 0 + // Move the restart address from the stack into the TLS field. + pop QWORD PTR [r11 + {SAVED_RESTART_ADDR}] // Recover the architectural guest stack pointer (undo the 128-byte // red zone reservation) and store it in SCRATCH. LEA is used instead // of ADD to avoid clobbering RFLAGS before pushfq. @@ -594,8 +601,7 @@ syscall_callback_redzone: push r8 // pt_regs->r8 push r9 // pt_regs->r9 push r10 // pt_regs->r10 - mov r10, gs:[0x28] // recover guest R11 saved at entry - push r10 // pt_regs->r11 = guest R11 (restart addr from rewriter) + push [rsp + 88] // pt_regs->r11 = rflags (matching real syscall ABI) push rbx // pt_regs->bx push rbp // pt_regs->bp push r12 @@ -660,6 +666,7 @@ interrupt_callback: HOST_BP = const core::mem::offset_of!(TlsState, host_bp), GUEST_CONTEXT_TOP = const core::mem::offset_of!(TlsState, guest_context_top), SCRATCH = const core::mem::offset_of!(TlsState, scratch), + SAVED_RESTART_ADDR = const core::mem::offset_of!(TlsState, saved_restart_addr), IS_IN_GUEST = const core::mem::offset_of!(TlsState, is_in_guest), ); } diff --git a/litebox_shim_linux/src/loader/elf.rs b/litebox_shim_linux/src/loader/elf.rs index b9a345483..6dd022173 100644 --- a/litebox_shim_linux/src/loader/elf.rs +++ b/litebox_shim_linux/src/loader/elf.rs @@ -181,6 +181,12 @@ impl litebox_common_linux::loader::MapMemory for PatchedMapper<'_, ' ) -> Result<(), Self::Error> { // Allocate anonymous RW pages, copy from the in-memory buffer, then // apply the requested protection. + // + // TODO: if the copy or protect step fails, the pages allocated by + // map_zero are leaked because the MapMemory trait has no unmap + // method, and no caller cleans up partially-mapped segments either. + // Add an `unmap` method to MapMemory and clean up the reserved + // region on failure in ElfParsedFile::load(). self.inner.map_zero( address, len, diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index d1f219579..b0877001d 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -2016,8 +2016,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 ab6069b24..6b5352fe1 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -444,6 +444,12 @@ impl Task { 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 phdrs_size = e_phentsize * e_phnum; if phdrs_size == 0 || phdrs_size > 0x10000 { @@ -518,26 +524,40 @@ impl Task { /// 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) { + let header_size: usize = if cfg!(target_pointer_width = "64") { + 32 // TrampolineHeader64: magic(8) + file_offset(8) + vaddr(8) + size(8) + } else { + 20 // TrampolineHeader32: magic(8) + file_offset(4) + vaddr(4) + size(4) + }; let Ok(stat) = self.sys_fstat(fd) else { return (false, 0, 0, 0); }; let file_size = stat.st_size; - if file_size < 32 { + if file_size < header_size { return (false, 0, 0, 0); } - let mut tail = [0u8; 32]; - match self.sys_read(fd, &mut tail, Some(file_size - 32)) { + let mut tail = [0u8; 32]; // max header size + let tail = &mut tail[..header_size]; + match self.sys_read(fd, tail, Some(file_size - header_size)) { Ok(n) if n == tail.len() => {} _ => return (false, 0, 0, 0), } if &tail[0..8] != litebox_syscall_rewriter::TRAMPOLINE_MAGIC { return (false, 0, 0, 0); } - // Parse header: magic(8) | file_offset(8) | vaddr(8) | size(8) - 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) + if cfg!(target_pointer_width = "64") { + // Parse 64-bit header: magic(8) | file_offset(8) | vaddr(8) | size(8) + 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) + } else { + // Parse 32-bit header: magic(8) | file_offset(4) | vaddr(4) | size(4) + let file_offset = u64::from(u32::from_le_bytes(tail[8..12].try_into().unwrap())); + let vaddr = u64::from(u32::from_le_bytes(tail[12..16].try_into().unwrap())); + let trampoline_size = u64::from(u32::from_le_bytes(tail[16..20].try_into().unwrap())); + (true, file_offset, vaddr, trampoline_size) + } } /// Patch an executable segment in-place after it has been mapped. @@ -565,6 +585,9 @@ impl Task { 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 @@ -728,6 +751,7 @@ impl Task { ) .is_err() { + restore_trampoline_rx(self, state); return true; } diff --git a/litebox_shim_optee/src/loader/elf.rs b/litebox_shim_optee/src/loader/elf.rs index 47859eb09..8b8a828ef 100644 --- a/litebox_shim_optee/src/loader/elf.rs +++ b/litebox_shim_optee/src/loader/elf.rs @@ -195,7 +195,13 @@ 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())?; + match parsed.parse_trampoline(&mut &file, task.global.platform.get_syscall_entry_point()) { + Ok(()) | Err(litebox_common_linux::loader::ElfParseError::UnpatchedBinary) => { + // Unpatched binary is expected in the LVBS scenario where not + // all binaries are rewritten. Proceed without a trampoline. + } + Err(e) => return Err(ElfLoaderError::ParseError(e)), + } Ok(Self { file, parsed }) } } diff --git a/litebox_syscall_rewriter/src/lib.rs b/litebox_syscall_rewriter/src/lib.rs index 1b723f7ce..f38d44d21 100644 --- a/litebox_syscall_rewriter/src/lib.rs +++ b/litebox_syscall_rewriter/src/lib.rs @@ -567,9 +567,18 @@ fn hook_syscalls_in_section( // that SA_RESTART can rewind ctx.rip to re-enter the trampoline. // The real `syscall` instruction clobbers R11 with RFLAGS, so // this register is free from the guest's perspective. + // + // CONTRACT: R11 carries the call-site restart address from this + // point until the platform callback saves it to a dedicated TLS + // variable (saved_restart_addr). The platform MUST preserve R11 + // before any clobbering instructions (fsbase swap, TLS lookup). // LEA R11, [RIP + disp32] = 4C 8D 1D - let r11_disp = i64::try_from(replace_start).unwrap() - - i64::try_from(trampoline_base_addr + trampoline_data.len() as u64 + 7).unwrap(); + let r11_rip = checked_add_u64( + trampoline_base_addr, + trampoline_data.len() as u64 + 7, + "x86_64 trampoline R11 displacement base", + )?; + let r11_disp = i64::try_from(replace_start).unwrap() - i64::try_from(r11_rip).unwrap(); trampoline_data.extend_from_slice(&[0x4C, 0x8D, 0x1D]); // LEA R11, [RIP + disp32] trampoline_data.extend_from_slice(&(i32::try_from(r11_disp).unwrap().to_le_bytes())); @@ -1190,9 +1199,18 @@ fn hook_syscall_and_after( // Put the address of the original JMP (call-site) into R11 so // that SA_RESTART can rewind ctx.rip to re-enter the trampoline. + // + // CONTRACT: R11 carries the call-site restart address from this + // point until the platform callback saves it to a dedicated TLS + // variable (saved_restart_addr). The platform MUST preserve R11 + // before any clobbering instructions (fsbase swap, TLS lookup). // LEA R11, [RIP + disp32] = 4C 8D 1D - let r11_disp = i64::try_from(replace_start).unwrap() - - i64::try_from(trampoline_base_addr + trampoline_data.len() as u64 + 7).unwrap(); + let r11_rip = checked_add_u64( + trampoline_base_addr, + trampoline_data.len() as u64 + 7, + "x86_64 trampoline R11 displacement base", + )?; + let r11_disp = i64::try_from(replace_start).unwrap() - i64::try_from(r11_rip).unwrap(); trampoline_data.extend_from_slice(&[0x4C, 0x8D, 0x1D]); // LEA R11, [RIP + disp32] trampoline_data.extend_from_slice(&(i32::try_from(r11_disp).unwrap().to_le_bytes())); From 8ea375612993c9ec6e76c809df6ecb433d453f4e Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 8 Apr 2026 18:03:35 +0000 Subject: [PATCH 13/14] Fix rebase conflicts: adapt pr1b code to pr1c API changes - Add fork_to_vfork_patch computation to metadata extraction block - Rename replace_with_ud2 -> replace_with_trap (pr1c rename) - Adapt hook_syscalls_in_elf callers to (Vec, Vec) return type - Adapt patch_code_segment callers to 4-arg signature - Update error variant names (UnsupportedExecutable, UnsupportedObjectFile) - Remove dead has_bun_footer_marker (pr1c uses ends_with directly) - Run cargo fmt --- litebox_platform_windows_userland/src/lib.rs | 7 ++++-- litebox_shim_linux/src/loader/elf.rs | 9 ++++---- litebox_shim_linux/src/syscalls/mm.rs | 24 ++++++++++++-------- litebox_syscall_rewriter/src/lib.rs | 22 ++++++++++-------- 4 files changed, 36 insertions(+), 26 deletions(-) diff --git a/litebox_platform_windows_userland/src/lib.rs b/litebox_platform_windows_userland/src/lib.rs index 61f9ff977..0284e8b6c 100644 --- a/litebox_platform_windows_userland/src/lib.rs +++ b/litebox_platform_windows_userland/src/lib.rs @@ -562,7 +562,7 @@ unsafe extern "C-unwind" fn run_thread_arch(thread_ctx: &mut ThreadContext, tls_ // All other registers hold guest state. .globl syscall_callback_redzone syscall_callback_redzone: - // Save guest R11 (restart address from rewriter trampoline) on the + // Save guest R11 (restart address from rewriter trampoline) to // TEB.ArbitraryUserPointer (gs:[0x28]) before the TLS index lookup // clobbers R11. This slot is per-thread and the window is very // narrow: only ~20 instructions of inline asm with no API calls, @@ -573,7 +573,10 @@ syscall_callback_redzone: mov r11d, DWORD PTR [rip + {TLS_INDEX}] mov r11, QWORD PTR gs:[r11 * 8 + TEB_TLS_SLOTS_OFFSET] mov BYTE PTR [r11 + {IS_IN_GUEST}], 0 - // Move the restart address from the stack into the TLS field. + // Recover the restart address from the TEB slot and store it in TLS. + // We use SCRATCH as a temporary since all guest GPRs must be preserved + // and RSP modifications would break the stack pointer recovery below. + push QWORD PTR gs:[0x28] pop QWORD PTR [r11 + {SAVED_RESTART_ADDR}] // Recover the architectural guest stack pointer (undo the 128-byte // red zone reservation) and store it in SCRATCH. LEA is used instead diff --git a/litebox_shim_linux/src/loader/elf.rs b/litebox_shim_linux/src/loader/elf.rs index 6dd022173..36f6933aa 100644 --- a/litebox_shim_linux/src/loader/elf.rs +++ b/litebox_shim_linux/src/loader/elf.rs @@ -283,9 +283,8 @@ impl<'a, FS: ShimFS> FileAndParsed<'a, FS> { .read_at(0, &mut buf) .map_err(ElfLoaderError::OpenError)?; - let mut skipped_addrs = alloc::vec::Vec::new(); - match litebox_syscall_rewriter::hook_syscalls_in_elf(&buf, None, &mut skipped_addrs) { - Ok(patched) => { + match litebox_syscall_rewriter::hook_syscalls_in_elf(&buf, None) { + Ok((patched, skipped_addrs)) => { if !skipped_addrs.is_empty() { litebox::log_println!( task.global.platform, @@ -304,8 +303,8 @@ impl<'a, FS: ShimFS> FileAndParsed<'a, FS> { Some(patched) } Err( - litebox_syscall_rewriter::Error::UnsupportedBunExecutable - | litebox_syscall_rewriter::Error::UnsupportedObjectFile + litebox_syscall_rewriter::Error::UnsupportedExecutable(_) + | litebox_syscall_rewriter::Error::UnsupportedObjectFile(_) | litebox_syscall_rewriter::Error::NoTextSectionFound | litebox_syscall_rewriter::Error::NoSyscallInstructionsFound | litebox_syscall_rewriter::Error::AlreadyHooked, diff --git a/litebox_shim_linux/src/syscalls/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index 6b5352fe1..42826a538 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -772,22 +772,26 @@ impl Task { let trampoline_write_vaddr = (state.trampoline_addr + state.trampoline_cursor) as u64; let syscall_entry_addr = state.trampoline_addr as u64; - let mut skipped_addrs = alloc::vec::Vec::new(); let patch_result = litebox_syscall_rewriter::patch_code_segment( &mut code_buf, code_vaddr, trampoline_write_vaddr, syscall_entry_addr, - &mut skipped_addrs, ); - if !skipped_addrs.is_empty() { - litebox::log_println!( - self.global.platform, - "warning: {} syscall instruction(s) could not be patched (addresses: {:?})", - skipped_addrs.len(), - skipped_addrs, - ); - } + let patch_result = match patch_result { + Ok((stubs, addrs)) => { + if !addrs.is_empty() { + litebox::log_println!( + self.global.platform, + "warning: {} syscall instruction(s) could not be patched (addresses: {:?})", + addrs.len(), + addrs, + ); + } + 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 { diff --git a/litebox_syscall_rewriter/src/lib.rs b/litebox_syscall_rewriter/src/lib.rs index f38d44d21..c26b28557 100644 --- a/litebox_syscall_rewriter/src/lib.rs +++ b/litebox_syscall_rewriter/src/lib.rs @@ -153,7 +153,14 @@ pub fn hook_syscalls_in_elf( fixup_phdr_alignment(buf); // Parse the ELF and extract all metadata we need, then drop the borrow so we can mutate buf. - let (arch, dl_sysinfo_int80, text_sections, control_transfer_targets, trampoline_base_addr) = { + let ( + arch, + dl_sysinfo_int80, + text_sections, + control_transfer_targets, + trampoline_base_addr, + fork_to_vfork_patch, + ) = { let file = object::File::parse(&*buf).map_err(|e| Error::ParseError(e.to_string()))?; let arch = match file { @@ -178,12 +185,15 @@ pub fn hook_syscalls_in_elf( let trampoline_base_addr = find_addr_for_trampoline_code(&file)?; + let fork_to_vfork_patch = find_fork_vfork_patch(&file, &text_sections); + ( arch, dl_sysinfo_int80, text_sections, control_transfer_targets, trampoline_base_addr, + fork_to_vfork_patch, ) }; @@ -246,7 +256,7 @@ pub fn hook_syscalls_in_elf( }; out.extend_from_slice(header.as_bytes()); } - return Ok(out); + return Ok((out, skipped_addrs)); } // Patch fork → vfork: overwrite the first bytes of __libc_fork with a @@ -539,7 +549,7 @@ fn hook_syscalls_in_section( ) { Ok(()) => {} Err(Error::InsufficientBytesBeforeOrAfter(_)) => { - replace_with_ud2(section_data, section_base_addr, inst); + replace_with_trap(section_data, section_base_addr, inst); skipped_addrs.push(inst.ip()); } Err(e) => return Err(e), @@ -850,12 +860,6 @@ fn find_fork_vfork_patch( Some((fork_file_offset, fork_patch_end, rel32)) } -/// Check if the input binary has the Bun footer marker near the end. -fn has_bun_footer_marker(input_binary: &[u8]) -> bool { - input_binary.len() >= BUN_FOOTER_MARKER.len() - && input_binary[input_binary.len() - BUN_FOOTER_MARKER.len()..] == *BUN_FOOTER_MARKER -} - /// Replace an unpatchable syscall instruction with `ICEBP; HLT` (`F1 F4`) so /// that reaching it traps instead of silently escaping to the host kernel. /// From 3d61f12c53a04c401243c01f953bc3543f4ec760 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 9 Apr 2026 03:53:38 +0000 Subject: [PATCH 14/14] Skip syscall rewriting when seccomp backend is active get_syscall_entry_point() now returns 0 when seccomp_interception_enabled is set, preventing unnecessary binary patching of syscall instructions when the systrap/seccomp backend is handling interception via SIGSYS. --- litebox_platform_linux_userland/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/litebox_platform_linux_userland/src/lib.rs b/litebox_platform_linux_userland/src/lib.rs index 8c9d57b02..2cbfce994 100644 --- a/litebox_platform_linux_userland/src/lib.rs +++ b/litebox_platform_linux_userland/src/lib.rs @@ -2077,6 +2077,16 @@ impl ThreadContext<'_> { impl litebox::platform::SystemInfoProvider for LinuxUserland { fn get_syscall_entry_point(&self) -> usize { + // When the seccomp/systrap backend is active, syscall instructions are + // trapped via SIGSYS — no binary rewriting needed. + #[cfg(feature = "systrap_backend")] + if self + .seccomp_interception_enabled + .load(std::sync::atomic::Ordering::SeqCst) + { + return 0; + } + #[cfg(target_arch = "x86_64")] { syscall_callback_redzone as *const () as usize