From 371960996e5d45f741ae05b9a68cb2730dddbb40 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 1 Apr 2026 20:30:16 -0700 Subject: [PATCH 01/26] Syscall rewriter: interface changes, no_std support, and bug fixes Add skipped_addrs output parameter to hook_syscalls_in_elf for reporting unpatchable syscall locations. Add no_std/alloc support with BTreeSet replacing HashSet for deterministic iteration. Add Bun executable detection (UnsupportedBunExecutable error), ET_REL object file rejection, phdr alignment fixup, fork-to-vfork patching, UD2 replacement for unpatchable syscalls, found_any tracking to move NoSyscallInstructionsFound to inner function, and println! removal. Add patch_code_segment public API for in-place code patching. Add unit tests for new functionality and update snapshot tests with address normalization. Update all callers (runner, optee, packager) to 3-arg API. --- Cargo.lock | 1 - litebox_packager/src/lib.rs | 20 +- litebox_runner_linux_userland/src/lib.rs | 15 +- .../src/lib.rs | 28 +- litebox_syscall_rewriter/Cargo.toml | 21 +- litebox_syscall_rewriter/src/lib.rs | 413 +++++++++++++++--- litebox_syscall_rewriter/src/main.rs | 9 + .../tests/snapshot_tests.rs | 63 ++- .../snapshot_tests__hello-32-diff.snap | 203 ++++----- .../snapshots/snapshot_tests__hello-diff.snap | 219 +++++----- 10 files changed, 712 insertions(+), 280 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ca5e7a9a3..1466b96ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1723,7 +1723,6 @@ dependencies = [ "clap", "iced-x86", "insta", - "memmap2", "object", "similar", "tempfile", diff --git a/litebox_packager/src/lib.rs b/litebox_packager/src/lib.rs index 223d027cf..0b5490a6a 100644 --- a/litebox_packager/src/lib.rs +++ b/litebox_packager/src/lib.rs @@ -569,8 +569,17 @@ fn rewrite_elf(data: &[u8], path: &Path, verbose: bool) -> anyhow::Result { + if !skipped_addrs.is_empty() { + eprintln!( + " warning: {} has {} unpatchable syscall instruction(s) at {:?}", + path.display(), + skipped_addrs.len(), + skipped_addrs, + ); + } if verbose { eprintln!(" {} (rewritten)", path.display()); } @@ -610,6 +619,15 @@ fn rewrite_elf(data: &[u8], path: &Path, verbose: bool) -> anyhow::Result { + if verbose { + eprintln!( + " warning: {} is a Bun-packaged executable, using as-is", + path.display() + ); + } + Ok(data.to_vec()) + } Err(e) => Err(e).with_context(|| format!("failed to rewrite {}", path.display())), } } diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index f15d1f7ab..28521c370 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -188,9 +188,18 @@ pub fn run(cli_args: CliArgs) -> Result<()> { .collect(); let file = mmapped_file(&prog)?; let data = if cli_args.rewrite_syscalls { - litebox_syscall_rewriter::hook_syscalls_in_elf(file.data, None) - .unwrap() - .into() + let mut skipped_addrs = Vec::new(); + let rewritten = + litebox_syscall_rewriter::hook_syscalls_in_elf(file.data, None, &mut skipped_addrs) + .unwrap(); + if !skipped_addrs.is_empty() { + eprintln!( + "warning: program has {} unpatchable syscall instruction(s) at {:?}", + skipped_addrs.len(), + skipped_addrs, + ); + } + rewritten.into() } else { let data = file.data.into(); cow_eligible_regions.push(file); diff --git a/litebox_runner_optee_on_linux_userland/src/lib.rs b/litebox_runner_optee_on_linux_userland/src/lib.rs index 4cc021dea..6a31e3926 100644 --- a/litebox_runner_optee_on_linux_userland/src/lib.rs +++ b/litebox_runner_optee_on_linux_userland/src/lib.rs @@ -67,7 +67,19 @@ pub fn run(cli_args: CliArgs) -> Result<()> { let ldelf = PathBuf::from(&cli_args.ldelf); let data = std::fs::read(ldelf).unwrap(); if cli_args.rewrite_syscalls { - litebox_syscall_rewriter::hook_syscalls_in_elf(&data, None).unwrap() + let mut skipped_addrs = Vec::new(); + let rewritten = + litebox_syscall_rewriter::hook_syscalls_in_elf(&data, None, &mut skipped_addrs) + .unwrap(); + if !skipped_addrs.is_empty() { + eprintln!( + "warning: {} has {} unpatchable syscall instruction(s) at {:?}", + cli_args.ldelf, + skipped_addrs.len(), + skipped_addrs, + ); + } + rewritten } else { data } @@ -77,7 +89,19 @@ pub fn run(cli_args: CliArgs) -> Result<()> { let prog = PathBuf::from(&cli_args.program); let data = std::fs::read(prog).unwrap(); if cli_args.rewrite_syscalls { - litebox_syscall_rewriter::hook_syscalls_in_elf(&data, None).unwrap() + let mut skipped_addrs = Vec::new(); + let rewritten = + litebox_syscall_rewriter::hook_syscalls_in_elf(&data, None, &mut skipped_addrs) + .unwrap(); + if !skipped_addrs.is_empty() { + eprintln!( + "warning: {} has {} unpatchable syscall instruction(s) at {:?}", + cli_args.program, + skipped_addrs.len(), + skipped_addrs, + ); + } + rewritten } else { data } diff --git a/litebox_syscall_rewriter/Cargo.toml b/litebox_syscall_rewriter/Cargo.toml index 2831d6b2c..3d88f9027 100644 --- a/litebox_syscall_rewriter/Cargo.toml +++ b/litebox_syscall_rewriter/Cargo.toml @@ -3,14 +3,23 @@ name = "litebox_syscall_rewriter" version = "0.1.0" edition = "2024" +[features] +default = ["std", "anyhow", "clap"] +std = [] + [dependencies] -anyhow = "1.0" -clap = { version = "4.5.32", features = ["derive"] } -iced-x86 = "1.21" -memmap2 = "0.9" -object = { version = "0.36.7", default-features = false, features = ["elf", "read", "std"] } +iced-x86 = { version = "1.21", default-features = false, features = ["no_std", "decoder", "encoder", "instr_info"] } +object = { version = "0.36.7", default-features = false, features = ["elf", "read_core"] } thiserror = { version = "2.0.6", default-features = false } -zerocopy = { version = "0.8", features = ["derive"] } +zerocopy = { version = "0.8", default-features = false, features = ["derive"] } + +# Binary-only dependencies +anyhow = { version = "1.0", optional = true } +clap = { version = "4.5.32", features = ["derive"], optional = true } + +[[bin]] +name = "litebox_syscall_rewriter" +required-features = ["std", "anyhow", "clap"] [lints] workspace = true diff --git a/litebox_syscall_rewriter/src/lib.rs b/litebox_syscall_rewriter/src/lib.rs index 1440bb401..f6de6503e 100644 --- a/litebox_syscall_rewriter/src/lib.rs +++ b/litebox_syscall_rewriter/src/lib.rs @@ -14,7 +14,14 @@ //! //! This crate currently only supports x86-64 (i.e., amd64) ELFs. -use std::collections::HashSet; +#![cfg_attr(not(feature = "std"), no_std)] +extern crate alloc; + +use alloc::collections::BTreeSet; +use alloc::format; +use alloc::string::{String, ToString}; +use alloc::vec; +use alloc::vec::Vec; use object::read::elf::{ElfFile, ProgramHeader as _}; use object::read::{Object as _, ObjectSection as _, ObjectSymbol as _}; @@ -29,6 +36,8 @@ pub enum Error { ParseError(String), #[error("unsupported executable")] UnsupportedObjectFile, + #[error("unsupported Bun-packaged executable")] + UnsupportedBunExecutable, #[error("executable is already hooked with trampoline")] AlreadyHooked, #[error("no .text section found")] @@ -43,7 +52,9 @@ pub enum Error { TrampolineAddressTooLarge, } -type Result = std::result::Result; +type Result = core::result::Result; + +const BUN_FOOTER_MARKER: &[u8] = b"\n---- Bun! ----\n"; /// The magic bytes used to identify the trampoline data. /// This is checked by the loader to verify that the trampoline is valid. @@ -95,7 +106,31 @@ struct TextSectionInfo { /// - trampoline size (8 bytes for 64-bit, 4 bytes for 32-bit) /// /// This layout allows loaders to read just the last 32/20 bytes to get the metadata. -pub fn hook_syscalls_in_elf(input_binary: &[u8], trampoline: Option) -> Result> { +/// +/// `skipped_addrs` receives the virtual addresses of any `syscall` +/// instructions that could not be patched (replaced with `UD2` so they +/// trap instead of escaping to the host kernel). +pub fn hook_syscalls_in_elf( + input_binary: &[u8], + trampoline: Option, + skipped_addrs: &mut Vec, +) -> Result> { + if has_bun_footer_marker(input_binary) { + return Err(Error::UnsupportedBunExecutable); + } + + // Relocatable object files (.o) must not be patched: they are linker + // input, not executable code. Rewriting instructions or appending + // trampoline data would corrupt the object file for the linker. + // Check the ELF e_type field (bytes 16..18) before doing any work. + if input_binary.len() >= 18 { + let e_type = u16::from_le_bytes([input_binary[16], input_binary[17]]); + if e_type == 1 { + // ET_REL — relocatable object file + return Err(Error::UnsupportedObjectFile); + } + } + // Make a single mutable, 8-byte-aligned copy of the input binary. This serves as both the // parse buffer (object::File::parse requires 8-byte alignment) and the output buffer for // in-place patching. We use a Vec to guarantee alignment, then view it as bytes. @@ -104,8 +139,20 @@ pub fn hook_syscalls_in_elf(input_binary: &[u8], trampoline: Option) -> Res buf[..input_binary.len()].copy_from_slice(input_binary); let buf = &mut buf[..input_binary.len()]; + // Some ELF files (e.g. Node.js SEA binaries) have a program header table at an offset that + // is not 8-byte aligned, which the `object` crate rejects. Fix this by relocating the phdr + // table within our mutable copy so it sits at an 8-byte aligned offset. + 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 { @@ -130,12 +177,15 @@ pub fn hook_syscalls_in_elf(input_binary: &[u8], trampoline: Option) -> Res 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, ) }; @@ -151,7 +201,6 @@ pub fn hook_syscalls_in_elf(input_binary: &[u8], trampoline: Option) -> Res } // Patch syscalls in-place in buf - let mut syscall_insns_found = false; for s in &text_sections { let section_data = section_slice_mut(buf, s)?; match hook_syscalls_in_section( @@ -160,19 +209,31 @@ pub fn hook_syscalls_in_elf(input_binary: &[u8], trampoline: Option) -> Res s.vaddr, section_data, trampoline_base_addr, + trampoline_base_addr, // entry point is at offset 0 of trampoline dl_sysinfo_int80, &mut trampoline_data, + skipped_addrs, ) { - Ok(()) => { - syscall_insns_found = true; - } - Err(Error::NoSyscallInstructionsFound) => {} + Ok(()) | Err(Error::NoSyscallInstructionsFound) => {} Err(e) => return Err(e), } } - if !syscall_insns_found { - return Err(Error::NoSyscallInstructionsFound); + // 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, rel32)) = fork_to_vfork_patch { + #[allow(clippy::cast_possible_truncation)] + let off = fork_file_offset as usize; + if off + 5 <= buf.len() { + 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 offset {off:#x} + 5 exceeds buffer length {}", + buf.len() + ))); + } } // Build output: [patched ELF][padding to page boundary][trampoline code][header] @@ -301,17 +362,24 @@ enum Arch { } /// (private) Hook all syscalls in `section`, possibly extending `trampoline_data` to do so. +/// +/// `trampoline_base_addr` is the virtual address corresponding to `trampoline_data[0]`. +/// `syscall_entry_addr` is the address of the 8-byte entry-point value that each trampoline +/// stub jumps to (via `JMP [RIP+disp32]` on x86-64 or `CALL [EAX+disp32]` on x86-32). #[allow(clippy::too_many_arguments)] fn hook_syscalls_in_section( arch: Arch, - control_transfer_targets: &HashSet, + control_transfer_targets: &BTreeSet, section_base_addr: u64, section_data: &mut [u8], trampoline_base_addr: u64, + syscall_entry_addr: u64, dl_sysinfo_int80: Option, trampoline_data: &mut Vec, + skipped_addrs: &mut Vec, ) -> Result<()> { let instructions = decode_section_instructions(arch, section_data, section_base_addr)?; + let mut found_any = false; for (i, inst) in instructions.iter().enumerate() { // Forward search for `syscall` / `int 0x80` / `call DWORD PTR gs:0x10` match arch { @@ -335,6 +403,7 @@ fn hook_syscalls_in_section( } } + found_any = true; let replace_end = inst.next_ip(); let mut replace_start = None; @@ -358,16 +427,26 @@ fn hook_syscalls_in_section( } if replace_start.is_none() { - hook_syscall_and_after( + 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 the unpatchable syscall with UD2 so it traps + // instead of escaping to the host kernel. + replace_with_ud2(section_data, section_base_addr, inst); + skipped_addrs.push(inst.ip()); + } + Err(e) => return Err(e), + } continue; } @@ -394,28 +473,29 @@ fn hook_syscalls_in_section( .extend_from_slice(&(i32::try_from(jmp_back_offset).unwrap().to_le_bytes())); // Add jmp [rip + offset_to_entry_point] - // Entry point is at offset 0 of trampoline_data trampoline_data.extend_from_slice(&[0xFF, 0x25]); - // disp32 points to offset 0 (entry point) from current RIP // RIP after this instruction = trampoline_base_addr + trampoline_data.len() + 4 - // We want: RIP + disp32 = trampoline_base_addr + 0 - // So: disp32 = -(trampoline_data.len() + 4) - let disp32 = -(i32::try_from(trampoline_data.len()).unwrap() + 4); - trampoline_data.extend_from_slice(&disp32.to_le_bytes()); + // We want: RIP + disp32 = syscall_entry_addr + #[allow(clippy::cast_possible_wrap)] + let disp32 = i64::try_from(syscall_entry_addr).unwrap() + - i64::try_from(trampoline_base_addr).unwrap() + - trampoline_data.len() as i64 + - 4; + trampoline_data.extend_from_slice(&(i32::try_from(disp32).unwrap().to_le_bytes())); } else { // For 32-bit, use a different approach to simulate indirect call - // Entry point is at offset 0 of trampoline_data trampoline_data.push(0x50); // PUSH EAX 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] - // The offset should point to the entry at offset 0 - // After PUSH(1) + CALL(5) + POP(1) + opcode(2) = 9 bytes - // EAX = base + (len_before_PUSH + 6) = base + (current_len - 9 + 6) = base + (current_len - 3) - // We want: EAX + offset = base + 0 - // So: offset = -(current_len - 3) - let disp32 = -(i32::try_from(trampoline_data.len()).unwrap() - 3); - trampoline_data.extend_from_slice(&disp32.to_le_bytes()); + // EAX = trampoline_base_addr + (trampoline_data.len() - 3) + // We want: EAX + offset = syscall_entry_addr + #[allow(clippy::cast_possible_wrap)] + let disp32 = i64::try_from(syscall_entry_addr).unwrap() + - i64::try_from(trampoline_base_addr).unwrap() + - trampoline_data.len() as i64 + + 3; + trampoline_data.extend_from_slice(&(i32::try_from(disp32).unwrap().to_le_bytes())); // Note we skip `POP EAX` here as it is done by the callback `syscall_callback` // from litebox_shim_linux/src/lib.rs, which helps reduce the size of the trampoline. @@ -441,7 +521,226 @@ fn hook_syscalls_in_section( } } - Ok(()) + if found_any { + Ok(()) + } else { + Err(Error::NoSyscallInstructionsFound) + } +} + +/// If the ELF64 program header table offset (`e_phoff`) is not 8-byte aligned, shift the table +/// forward by the necessary padding so the `object` crate can parse it. This is needed for +/// binaries like Node.js SEA executables where post-link tools append data and relocate the +/// program headers to a non-aligned offset. +/// +/// The function modifies the buffer in-place: it moves the phdr table contents and updates +/// `e_phoff` in the ELF header. Only ELF64 files are handled (ELF32 requires 4-byte alignment +/// which is always satisfied when `e_phoff` is within a valid file). +fn fixup_phdr_alignment(buf: &mut [u8]) { + // Minimum ELF header size for ELF64 + if buf.len() < 64 { + return; + } + + // Check ELF magic and class (must be ELF64) + if &buf[0..4] != b"\x7fELF" || buf[4] != 2 { + return; + } + + let e_phoff = u64::from_le_bytes(buf[32..40].try_into().unwrap()); + let e_phentsize = u64::from(u16::from_le_bytes(buf[54..56].try_into().unwrap())); + let e_phnum = u64::from(u16::from_le_bytes(buf[56..58].try_into().unwrap())); + + if e_phoff == 0 || e_phnum == 0 || e_phentsize == 0 { + return; + } + + let misalignment = e_phoff % 8; + if misalignment == 0 { + return; // already aligned + } + + let phdr_size = e_phentsize * e_phnum; + let old_start = usize::try_from(e_phoff).expect("e_phoff must fit in usize"); + let old_end = old_start + usize::try_from(phdr_size).expect("phdr_size must fit in usize"); + + // Shift forward to align: new offset is the next 8-byte boundary. + let padding = usize::try_from(8 - misalignment).expect("padding must fit in usize"); + let new_start = old_start + padding; + let new_end = new_start + usize::try_from(phdr_size).expect("phdr_size must fit in usize"); + + if old_end > buf.len() || new_end > buf.len() { + return; // corrupt phdr table or not enough room + } + + // Move the phdr table forward (use copy_within since src and dst overlap). + buf.copy_within(old_start..old_end, new_start); + + // Update e_phoff in the ELF header. + let new_phoff = (e_phoff + padding as u64).to_le_bytes(); + buf[32..40].copy_from_slice(&new_phoff); + + // Also update the PHDR segment's p_offset if present, so it matches. + // PT_PHDR = 6, each Elf64_Phdr is e_phentsize bytes, p_type at offset 0, p_offset at offset 8. + for i in 0..e_phnum { + let entry_off = new_start + + usize::try_from(i).expect("i must fit in usize") + * usize::try_from(e_phentsize).expect("e_phentsize must fit in usize"); + if entry_off + 16 > buf.len() { + break; + } + let p_type = u32::from_le_bytes(buf[entry_off..entry_off + 4].try_into().unwrap()); + if p_type == 6 { + // PT_PHDR — update p_offset to match new location + let p_offset_off = entry_off + 8; + let old_p_offset = + u64::from_le_bytes(buf[p_offset_off..p_offset_off + 8].try_into().unwrap()); + if old_p_offset == e_phoff { + let new_p_offset = (old_p_offset + padding as u64).to_le_bytes(); + buf[p_offset_off..p_offset_off + 8].copy_from_slice(&new_p_offset); + } + // The PHDR segment size should match the phdr table; no change needed. + } + } +} + +/// 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, i32)> { + use object::ObjectSymbol as _; + + // Search both .dynsym and .symtab for fork/vfork. + let mut fork_vaddr = None; + let mut vfork_vaddr = None; + + for sym in file.dynamic_symbols().chain(file.symbols()) { + if sym.kind() != object::SymbolKind::Text { + continue; + } + let Ok(name) = sym.name() else { continue }; + match name { + "fork" | "__libc_fork" if fork_vaddr.is_none() => { + fork_vaddr = Some(sym.address()); + } + "vfork" | "__libc_vfork" | "__vfork" if vfork_vaddr.is_none() => { + vfork_vaddr = Some(sym.address()); + } + _ => {} + } + } + + let fork_vaddr = fork_vaddr?; + let vfork_vaddr = vfork_vaddr?; + + // Convert fork's vaddr to a file offset using the text sections. + let fork_file_offset = text_sections.iter().find_map(|s| { + let section_end = s.vaddr + s.size; + if fork_vaddr >= s.vaddr && fork_vaddr < section_end { + Some(s.file_offset + (fork_vaddr - s.vaddr)) + } 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, rel32)) +} + +/// Check if the input binary has the Bun footer marker at 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 `UD2` (`0F 0B`) so that +/// reaching it triggers SIGILL instead of silently escaping to the host kernel. +/// +/// `syscall` (0F 05) and `int 0x80` (CD 80) are both 2 bytes — same size as +/// `ud2`. For `call DWORD PTR gs:0x10` (7 bytes), the remaining 5 bytes are +/// filled with NOPs. +fn replace_with_ud2(section_data: &mut [u8], section_base_addr: u64, inst: &iced_x86::Instruction) { + let offset = usize::try_from(inst.ip() - section_base_addr).unwrap(); + let len = inst.len(); + // UD2 = 0F 0B + section_data[offset] = 0x0F; + section_data[offset + 1] = 0x0B; + // Fill any remaining bytes (e.g. 7-byte `call gs:0x10`) with NOPs. + for b in &mut section_data[offset + 2..offset + len] { + *b = 0x90; + } +} + +/// Patch a single mapped code segment in-place, returning trampoline stubs. +/// +/// This is the runtime counterpart to [`hook_syscalls_in_elf`]. Instead of +/// processing a whole ELF file, it operates on a single already-mapped code +/// region — the caller is responsible for making the region writable before +/// calling and restoring permissions afterwards. +/// +/// # Arguments +/// +/// * `code` — mutable slice of the mapped code segment. +/// * `code_vaddr` — virtual address of `code[0]` in guest memory. +/// * `trampoline_write_vaddr` — virtual address where the returned stub bytes +/// will be placed by the caller. +/// * `syscall_entry_addr` — address of the 8-byte entry-point value that +/// each stub's indirect jump targets. +/// +/// # Returns +/// +/// The trampoline stub bytes. The caller must copy them to +/// `trampoline_write_vaddr`. Returns an empty `Vec` if no syscall +/// instructions are found in `code`. +/// +/// `skipped_addrs` receives the virtual addresses of any `syscall` +/// instructions that could not be patched (replaced with `UD2` so they +/// trap instead of escaping to the host kernel). +pub fn patch_code_segment( + code: &mut [u8], + code_vaddr: u64, + trampoline_write_vaddr: u64, + syscall_entry_addr: u64, + skipped_addrs: &mut Vec, +) -> Result> { + let arch = Arch::X86_64; // runtime patching is x86-64 only + + // Build control-transfer targets for this segment. + let instructions = decode_section_instructions(arch, code, code_vaddr)?; + let mut control_transfer_targets = BTreeSet::new(); + for inst in &instructions { + let target = inst.near_branch_target(); + if target != 0 { + control_transfer_targets.insert(target); + } + } + + let mut trampoline_data = Vec::new(); + match hook_syscalls_in_section( + arch, + &control_transfer_targets, + code_vaddr, + code, + trampoline_write_vaddr, + syscall_entry_addr, + None, // dl_sysinfo_int80 — not applicable on x86-64 + &mut trampoline_data, + skipped_addrs, + ) { + Ok(()) => Ok(trampoline_data), + Err(Error::NoSyscallInstructionsFound) => Ok(Vec::new()), + Err(e) => Err(e), + } } fn find_addr_for_trampoline_code(file: &object::File<'_>) -> u64 { @@ -485,8 +784,8 @@ fn get_control_transfer_targets( arch: Arch, input_binary: &[u8], text_sections: &[TextSectionInfo], -) -> Result> { - let mut control_transfer_targets = HashSet::new(); +) -> Result> { + let mut control_transfer_targets = BTreeSet::new(); for s in text_sections { let section_data = section_slice(input_binary, s)?; let instructions = decode_section_instructions(arch, section_data, s.vaddr)?; @@ -595,10 +894,11 @@ fn section_slice_mut<'a>(buf: &'a mut [u8], section: &TextSectionInfo) -> Result #[allow(clippy::too_many_arguments)] fn hook_syscall_and_after( arch: Arch, - control_transfer_targets: &HashSet, + control_transfer_targets: &BTreeSet, section_base_addr: u64, section_data: &mut [u8], trampoline_base_addr: u64, + syscall_entry_addr: u64, trampoline_data: &mut Vec, instructions: &[iced_x86::Instruction], inst_index: usize, @@ -613,7 +913,6 @@ fn hook_syscall_and_after( && control_transfer_targets.contains(&next_inst.ip()) { // If the next instruction is a control transfer target, we don't want to cross it - println!("Skipping control transfer target at {:#x}", next_inst.ip()); break; } // Check if the instruction does control transfer @@ -639,6 +938,7 @@ fn hook_syscall_and_after( section_base_addr, section_data, trampoline_base_addr, + syscall_entry_addr, trampoline_data, instructions, inst_index, @@ -654,28 +954,29 @@ fn hook_syscall_and_after( trampoline_data.extend_from_slice(&[0x48, 0x8D, 0x0D]); // LEA RCX, [RIP + disp32] trampoline_data.extend_from_slice(&6u32.to_le_bytes()); // Add jmp [rip + offset_to_entry_point] - // Entry point is at offset 0 of trampoline_data trampoline_data.extend_from_slice(&[0xFF, 0x25]); - // disp32 points to offset 0 (entry point) from current RIP // RIP after this instruction = trampoline_base_addr + trampoline_data.len() + 4 - // We want: RIP + disp32 = trampoline_base_addr + 0 - // So: disp32 = -(trampoline_data.len() + 4) - let disp32 = -(i32::try_from(trampoline_data.len()).unwrap() + 4); - trampoline_data.extend_from_slice(&disp32.to_le_bytes()); + // We want: RIP + disp32 = syscall_entry_addr + #[allow(clippy::cast_possible_wrap)] + let disp32 = i64::try_from(syscall_entry_addr).unwrap() + - i64::try_from(trampoline_base_addr).unwrap() + - trampoline_data.len() as i64 + - 4; + trampoline_data.extend_from_slice(&(i32::try_from(disp32).unwrap().to_le_bytes())); } else { // For 32-bit, use a different approach to simulate indirect call - // Entry point is at offset 0 of trampoline_data trampoline_data.push(0x50); // PUSH EAX 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] - // The offset should point to the entry at offset 0 - // After PUSH(1) + CALL(5) + POP(1) + opcode(2) = 9 bytes - // EAX = base + (len_before_PUSH + 6) = base + (current_len - 9 + 6) = base + (current_len - 3) - // We want: EAX + offset = base + 0 - // So: offset = -(current_len - 3) - let disp32 = -(i32::try_from(trampoline_data.len()).unwrap() - 3); - trampoline_data.extend_from_slice(&disp32.to_le_bytes()); + // EAX = trampoline_base_addr + (trampoline_data.len() - 3) + // We want: EAX + offset = syscall_entry_addr + #[allow(clippy::cast_possible_wrap)] + let disp32 = i64::try_from(syscall_entry_addr).unwrap() + - i64::try_from(trampoline_base_addr).unwrap() + - trampoline_data.len() as i64 + + 3; + trampoline_data.extend_from_slice(&(i32::try_from(disp32).unwrap().to_le_bytes())); // Note we skip `POP EAX` here as it is done by the callback `syscall_callback` // from litebox_shim_linux/src/lib.rs, which helps reduce the size of the trampoline. } @@ -715,10 +1016,11 @@ fn hook_syscall_and_after( #[allow(clippy::too_many_arguments)] fn hook_syscall_before_and_after( arch: Arch, - control_transfer_targets: &HashSet, + control_transfer_targets: &BTreeSet, section_base_addr: u64, section_data: &mut [u8], trampoline_base_addr: u64, + syscall_entry_addr: u64, trampoline_data: &mut Vec, instructions: &[iced_x86::Instruction], inst_index: usize, @@ -793,13 +1095,14 @@ 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] - // The offset should point to the entry at offset 0 - // After PUSH(1) + CALL(5) + POP(1) + opcode(2) = 9 bytes - // EAX = base + (len_before_PUSH + 6) = base + (current_len - 9 + 6) = base + (current_len - 3) - // We want: EAX + offset = base + 0 - // So: offset = -(current_len - 3) - let disp32 = -(i32::try_from(trampoline_data.len()).unwrap() - 3); - trampoline_data.extend_from_slice(&disp32.to_le_bytes()); + // EAX = trampoline_base_addr + (trampoline_data.len() - 3) + // We want: EAX + offset = syscall_entry_addr + #[allow(clippy::cast_possible_wrap)] + let disp32 = i64::try_from(syscall_entry_addr).unwrap() + - i64::try_from(trampoline_base_addr).unwrap() + - trampoline_data.len() as i64 + + 3; + trampoline_data.extend_from_slice(&(i32::try_from(disp32).unwrap().to_le_bytes())); // Note we skip `POP EAX` here as it is done by the callback `syscall_callback` // from litebox_shim_linux/src/lib.rs, which helps reduce the size of the trampoline. diff --git a/litebox_syscall_rewriter/src/main.rs b/litebox_syscall_rewriter/src/main.rs index 7ef8eef14..c60065981 100644 --- a/litebox_syscall_rewriter/src/main.rs +++ b/litebox_syscall_rewriter/src/main.rs @@ -47,10 +47,19 @@ fn main() -> anyhow::Result<()> { let mut input_binary = std::fs::File::open(&cli_args.input_binary)?; let mut input_binary_bytes = vec![]; input_binary.read_to_end(&mut input_binary_bytes)?; + let mut skipped_addrs = Vec::new(); let output_binary = litebox_syscall_rewriter::hook_syscalls_in_elf( &input_binary_bytes, cli_args.trampoline_addr, + &mut skipped_addrs, )?; + if !skipped_addrs.is_empty() { + eprintln!( + "warning: {} unpatchable syscall instruction(s) at {:?}", + skipped_addrs.len(), + skipped_addrs, + ); + } let output_path = cli_args.output_binary.unwrap_or_else(|| { cli_args.input_binary.with_file_name( cli_args diff --git a/litebox_syscall_rewriter/tests/snapshot_tests.rs b/litebox_syscall_rewriter/tests/snapshot_tests.rs index 841dfe86b..d05ab1a09 100644 --- a/litebox_syscall_rewriter/tests/snapshot_tests.rs +++ b/litebox_syscall_rewriter/tests/snapshot_tests.rs @@ -6,6 +6,7 @@ fn objdump(binary: &[u8]) -> String { use std::process::Command; use tempfile::NamedTempFile; + let trampoline_range = trampoline_range(binary); let mut temp_file = NamedTempFile::new().unwrap(); temp_file.write_all(binary).unwrap(); @@ -19,16 +20,74 @@ fn objdump(binary: &[u8]) -> String { String::from_utf8_lossy(&output.stdout) .lines() .filter(|l| !l.contains("/tmp/")) - .map(str::trim_end) + .map(|line| normalize_objdump_line(line, trampoline_range.as_ref())) .collect::>() .join("\n") } +fn trampoline_range(binary: &[u8]) -> Option> { + const MAGIC: &[u8; 8] = litebox_syscall_rewriter::TRAMPOLINE_MAGIC; + + if binary.len() < 20 { + return None; + } + + match binary.get(4).copied() { + Some(2) if binary.len() >= 32 => { + let header = &binary[binary.len() - 32..]; + if &header[..8] != MAGIC { + return None; + } + let vaddr = u64::from_le_bytes(header[16..24].try_into().unwrap()); + let size = u64::from_le_bytes(header[24..32].try_into().unwrap()); + (size != 0).then_some(vaddr..vaddr.checked_add(size)?) + } + Some(1) => { + let header = &binary[binary.len() - 20..]; + if &header[..8] != MAGIC { + return None; + } + let vaddr = u64::from(u32::from_le_bytes(header[12..16].try_into().unwrap())); + let size = u64::from(u32::from_le_bytes(header[16..20].try_into().unwrap())); + (size != 0).then_some(vaddr..vaddr.checked_add(size)?) + } + _ => None, + } +} + +fn normalize_objdump_line(line: &str, trampoline_range: Option<&std::ops::Range>) -> String { + let Some(trampoline_range) = trampoline_range else { + return line.trim_end().to_owned(); + }; + let Some((address, rest)) = line.split_once(':') else { + return line.trim_end().to_owned(); + }; + let tokens: Vec<_> = rest.split_whitespace().collect(); + let Some((mnemonic_idx, mnemonic)) = tokens + .iter() + .enumerate() + .find(|(_, token)| !token.chars().all(|ch| ch.is_ascii_hexdigit())) + else { + return line.trim_end().to_owned(); + }; + if *mnemonic == "jmp" + && let Some(target) = tokens + .get(mnemonic_idx + 1) + .and_then(|token| u64::from_str_radix(token.trim_start_matches("0x"), 16).ok()) + && trampoline_range.contains(&target) + { + let offset = target - trampoline_range.start; + return format!("{address}:\t"); + } + line.trim_end().to_owned() +} + const HELLO_INPUT_64: &[u8] = include_bytes!("hello"); const HELLO_INPUT_32: &[u8] = include_bytes!("hello-32"); fn run_snapshot_test(input: &[u8], snapshot: &str) { - let output = litebox_syscall_rewriter::hook_syscalls_in_elf(input, None).unwrap(); + let output = + litebox_syscall_rewriter::hook_syscalls_in_elf(input, None, &mut Vec::new()).unwrap(); let diff = similar::udiff::unified_diff( similar::Algorithm::Myers, &objdump(input), diff --git a/litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-32-diff.snap b/litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-32-diff.snap index 822b946c6..0fd4d4ae0 100644 --- a/litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-32-diff.snap +++ b/litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-32-diff.snap @@ -1,5 +1,6 @@ --- source: litebox_syscall_rewriter/tests/snapshot_tests.rs +assertion_line: 99 expression: diff --- --- original @@ -9,7 +10,7 @@ expression: diff 8049142: c7 85 d0 39 00 00 01 movl $0x1,0x39d0(%ebp) 8049149: 00 00 00 - 804914c: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 804914c: e9 b3 de 09 00 jmp 80e7004 <_end+0x3bc> ++ 804914c: + 8049151: 90 nop + 8049152: 90 nop 8049153: 8b 85 d0 39 00 00 mov 0x39d0(%ebp),%eax @@ -22,7 +23,7 @@ expression: diff - 8049e00: 89 d0 mov %edx,%eax - 8049e02: cd 80 int $0x80 - 8049e04: eb fa jmp 8049e00 <__libc_start_call_main+0x90> -+ 8049e00: e9 11 d2 09 00 jmp 80e7016 <_end+0x3ce> ++ 8049e00: + 8049e05: 90 nop 8049e06: 31 c0 xor %eax,%eax 8049e08: eb d0 jmp 8049dda <__libc_start_call_main+0x6a> @@ -34,7 +35,7 @@ expression: diff - 804bf8c: c7 44 24 4c 51 00 00 movl $0x51,0x4c(%esp) - 804bf93: 00 - 804bf94: cd 80 int $0x80 -+ 804bf8c: e9 96 b0 09 00 jmp 80e7027 <_end+0x3df> ++ 804bf8c: + 804bf91: 90 nop + 804bf92: 90 nop + 804bf93: 90 nop @@ -48,13 +49,13 @@ expression: diff 804bfab: 8d 8e 20 e6 fc ff lea -0x319e0(%esi),%ecx - 804bfb1: ba 2d 00 00 00 mov $0x2d,%edx - 804bfb6: cd 80 int $0x80 -+ 804bfb1: e9 8b b0 09 00 jmp 80e7041 <_end+0x3f9> ++ 804bfb1: + 804bfb6: 90 nop + 804bfb7: 90 nop 804bfb8: b8 fc 00 00 00 mov $0xfc,%eax - 804bfbd: bb 7f 00 00 00 mov $0x7f,%ebx - 804bfc2: cd 80 int $0x80 -+ 804bfbd: e9 96 b0 09 00 jmp 80e7058 <_end+0x410> ++ 804bfbd: + 804bfc2: 90 nop + 804bfc3: 90 nop 804bfc4: e8 67 ff 00 00 call 805bf30 <__tls_init_tp> @@ -66,13 +67,13 @@ expression: diff 804c082: 8d 8e 20 e6 fc ff lea -0x319e0(%esi),%ecx - 804c088: ba 2d 00 00 00 mov $0x2d,%edx - 804c08d: cd 80 int $0x80 -+ 804c088: e9 e2 af 09 00 jmp 80e706f <_end+0x427> ++ 804c088: + 804c08d: 90 nop + 804c08e: 90 nop 804c08f: b8 fc 00 00 00 mov $0xfc,%eax - 804c094: bb 7f 00 00 00 mov $0x7f,%ebx - 804c099: cd 80 int $0x80 -+ 804c094: e9 ed af 09 00 jmp 80e7086 <_end+0x43e> ++ 804c094: + 804c099: 90 nop + 804c09a: 90 nop 804c09b: e9 46 fe ff ff jmp 804bee6 <__libc_setup_tls+0x126> @@ -83,7 +84,7 @@ expression: diff 8050f61: 8d b4 26 00 00 00 00 lea 0x0(%esi,%eiz,1),%esi 8050f68: b8 92 00 00 00 mov $0x92,%eax - 8050f6d: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 8050f6d: e9 2b 61 09 00 jmp 80e709d <_end+0x455> ++ 8050f6d: + 8050f72: 90 nop + 8050f73: 90 nop 8050f74: 83 f8 fc cmp $0xfffffffc,%eax @@ -94,7 +95,7 @@ expression: diff 805118e: ba 02 00 00 00 mov $0x2,%edx 8051193: 31 f6 xor %esi,%esi - 8051195: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 8051195: e9 15 5f 09 00 jmp 80e70af <_end+0x467> ++ 8051195: + 805119a: 90 nop + 805119b: 90 nop 805119c: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -105,7 +106,7 @@ expression: diff 8051202: 31 f6 xor %esi,%esi 8051204: 80 f1 80 xor $0x80,%cl - 8051207: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 8051207: e9 b5 5e 09 00 jmp 80e70c1 <_end+0x479> ++ 8051207: + 805120c: 90 nop + 805120d: 90 nop 805120e: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -116,7 +117,7 @@ expression: diff 8051251: 31 f6 xor %esi,%esi 8051253: 8b 5c 24 0c mov 0xc(%esp),%ebx - 8051257: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 8051257: e9 77 5e 09 00 jmp 80e70d3 <_end+0x48b> ++ 8051257: + 805125c: 90 nop + 805125d: 90 nop 805125e: 5b pop %ebx @@ -127,7 +128,7 @@ expression: diff 8051282: 8b 5c 24 0c mov 0xc(%esp),%ebx 8051286: 80 f1 81 xor $0x81,%cl - 8051289: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 8051289: e9 57 5e 09 00 jmp 80e70e5 <_end+0x49d> ++ 8051289: + 805128e: 90 nop + 805128f: 90 nop 8051290: 5b pop %ebx @@ -138,7 +139,7 @@ expression: diff 805215c: c6 86 fc 35 00 00 01 movb $0x1,0x35fc(%esi) 8052163: 8d 9e f4 35 00 00 lea 0x35f4(%esi),%ebx - 8052169: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 8052169: e9 89 4f 09 00 jmp 80e70f7 <_end+0x4af> ++ 8052169: + 805216e: 90 nop + 805216f: 90 nop 8052170: 8d 7c 24 0c lea 0xc(%esp),%edi @@ -149,7 +150,7 @@ expression: diff 8059916: b8 93 01 00 00 mov $0x193,%eax 805991b: 89 f9 mov %edi,%ecx - 805991d: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 805991d: e9 e7 d7 08 00 jmp 80e7109 <_end+0x4c1> ++ 805991d: + 8059922: 90 nop + 8059923: 90 nop 8059924: 85 c0 test %eax,%eax @@ -159,7 +160,7 @@ expression: diff 805992d: 8d 4c 24 04 lea 0x4(%esp),%ecx 8059931: b8 09 01 00 00 mov $0x109,%eax - 8059936: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 8059936: e9 e0 d7 08 00 jmp 80e711b <_end+0x4d3> ++ 8059936: + 805993b: 90 nop + 805993c: 90 nop 805993d: 85 c0 test %eax,%eax @@ -170,7 +171,7 @@ expression: diff 8059a70: f4 hlt 8059a71: 89 d0 mov %edx,%eax - 8059a73: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 8059a73: e9 b5 d6 08 00 jmp 80e712d <_end+0x4e5> ++ 8059a73: + 8059a78: 90 nop + 8059a79: 90 nop 8059a7a: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -181,7 +182,7 @@ expression: diff 8059bd0: 31 c0 xor %eax,%eax 8059bd2: b8 7f 01 00 00 mov $0x17f,%eax - 8059bd7: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 8059bd7: e9 63 d5 08 00 jmp 80e713f <_end+0x4f7> ++ 8059bd7: + 8059bdc: 90 nop + 8059bdd: 90 nop 8059bde: 89 c6 mov %eax,%esi @@ -192,7 +193,7 @@ expression: diff 8059dc2: 8d 54 24 2c lea 0x2c(%esp),%edx 8059dc6: b8 2c 01 00 00 mov $0x12c,%eax - 8059dcb: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 8059dcb: e9 81 d3 08 00 jmp 80e7151 <_end+0x509> ++ 8059dcb: + 8059dd0: 90 nop + 8059dd1: 90 nop 8059dd2: 89 c2 mov %eax,%edx @@ -203,7 +204,7 @@ expression: diff 8059f5c: b8 06 00 00 00 mov $0x6,%eax 8059f61: 8b 5c 24 08 mov 0x8(%esp),%ebx - 8059f65: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 8059f65: e9 f9 d1 08 00 jmp 80e7163 <_end+0x51b> ++ 8059f65: + 8059f6a: 90 nop + 8059f6b: 90 nop 8059f6c: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -214,7 +215,7 @@ expression: diff 8059fb9: 8b 5c 24 20 mov 0x20(%esp),%ebx 8059fbd: b8 dd 00 00 00 mov $0xdd,%eax - 8059fc2: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 8059fc2: e9 ae d1 08 00 jmp 80e7175 <_end+0x52d> ++ 8059fc2: + 8059fc7: 90 nop + 8059fc8: 90 nop 8059fc9: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -225,7 +226,7 @@ expression: diff 8059ff0: b8 dd 00 00 00 mov $0xdd,%eax 8059ff5: b9 10 00 00 00 mov $0x10,%ecx - 8059ffa: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 8059ffa: e9 88 d1 08 00 jmp 80e7187 <_end+0x53f> ++ 8059ffa: + 8059fff: 90 nop + 805a000: 90 nop 805a001: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -236,7 +237,7 @@ expression: diff 805a069: 8b 5c 24 20 mov 0x20(%esp),%ebx 805a06d: b8 dd 00 00 00 mov $0xdd,%eax - 805a072: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 805a072: e9 22 d1 08 00 jmp 80e7199 <_end+0x551> ++ 805a072: + 805a077: 90 nop + 805a078: 90 nop 805a079: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -247,7 +248,7 @@ expression: diff 805a0a0: b8 dd 00 00 00 mov $0xdd,%eax 805a0a5: b9 10 00 00 00 mov $0x10,%ecx - 805a0aa: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 805a0aa: e9 fc d0 08 00 jmp 80e71ab <_end+0x563> ++ 805a0aa: + 805a0af: 90 nop + 805a0b0: 90 nop 805a0b1: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -258,7 +259,7 @@ expression: diff 805a118: b8 27 01 00 00 mov $0x127,%eax 805a11d: bb 9c ff ff ff mov $0xffffff9c,%ebx - 805a122: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 805a122: e9 96 d0 08 00 jmp 80e71bd <_end+0x575> ++ 805a122: + 805a127: 90 nop + 805a128: 90 nop 805a129: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -269,7 +270,7 @@ expression: diff 805a176: 8b 54 24 14 mov 0x14(%esp),%edx 805a17a: 8b 5c 24 0c mov 0xc(%esp),%ebx - 805a17e: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 805a17e: e9 4c d0 08 00 jmp 80e71cf <_end+0x587> ++ 805a17e: + 805a183: 90 nop + 805a184: 90 nop 805a185: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -281,7 +282,7 @@ expression: diff 805a30c: b8 2d 00 00 00 mov $0x2d,%eax - 805a311: 8b 5c 24 08 mov 0x8(%esp),%ebx - 805a315: cd 80 int $0x80 -+ 805a311: e9 cb ce 08 00 jmp 80e71e1 <_end+0x599> ++ 805a311: + 805a316: 90 nop 805a317: 89 82 28 36 00 00 mov %eax,0x3628(%edx) 805a31d: 39 d8 cmp %ebx,%eax @@ -291,7 +292,7 @@ expression: diff 805a750: 8d 54 24 0c lea 0xc(%esp),%edx 805a754: b8 f2 00 00 00 mov $0xf2,%eax - 805a759: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 805a759: e9 99 ca 08 00 jmp 80e71f7 <_end+0x5af> ++ 805a759: + 805a75e: 90 nop + 805a75f: 90 nop 805a760: 85 c0 test %eax,%eax @@ -302,7 +303,7 @@ expression: diff 805a959: 8b 5c 24 08 mov 0x8(%esp),%ebx 805a95d: b8 db 00 00 00 mov $0xdb,%eax - 805a962: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 805a962: e9 a2 c8 08 00 jmp 80e7209 <_end+0x5c1> ++ 805a962: + 805a967: 90 nop + 805a968: 90 nop 805a969: 5b pop %ebx @@ -313,7 +314,7 @@ expression: diff 805aa29: 8b 5c 24 08 mov 0x8(%esp),%ebx 805aa2d: b8 7d 00 00 00 mov $0x7d,%eax - 805aa32: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 805aa32: e9 e4 c7 08 00 jmp 80e721b <_end+0x5d3> ++ 805aa32: + 805aa37: 90 nop + 805aa38: 90 nop 805aa39: 5b pop %ebx @@ -324,7 +325,7 @@ expression: diff 805aa56: 8b 5c 24 04 mov 0x4(%esp),%ebx 805aa5a: b8 5b 00 00 00 mov $0x5b,%eax - 805aa5f: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 805aa5f: e9 c9 c7 08 00 jmp 80e722d <_end+0x5e5> ++ 805aa5f: + 805aa64: 90 nop + 805aa65: 90 nop 805aa66: 89 d3 mov %edx,%ebx @@ -335,7 +336,7 @@ expression: diff 805ab29: b8 a3 00 00 00 mov $0xa3,%eax 805ab2e: 8b 5c 24 14 mov 0x14(%esp),%ebx - 805ab32: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 805ab32: e9 08 c7 08 00 jmp 80e723f <_end+0x5f7> ++ 805ab32: + 805ab37: 90 nop + 805ab38: 90 nop 805ab39: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -346,7 +347,7 @@ expression: diff 805abdb: bb 41 4d 56 53 mov $0x53564d41,%ebx 805abe0: 31 c9 xor %ecx,%ecx - 805abe2: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 805abe2: e9 6a c6 08 00 jmp 80e7251 <_end+0x609> ++ 805abe2: + 805abe7: 90 nop + 805abe8: 90 nop 805abe9: 83 f8 ea cmp $0xffffffea,%eax @@ -357,7 +358,7 @@ expression: diff 805ac02: 8b 5c 24 04 mov 0x4(%esp),%ebx 805ac06: b8 74 00 00 00 mov $0x74,%eax - 805ac0b: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 805ac0b: e9 53 c6 08 00 jmp 80e7263 <_end+0x61b> ++ 805ac0b: + 805ac10: 90 nop + 805ac11: 90 nop 805ac12: 89 d3 mov %edx,%ebx @@ -368,7 +369,7 @@ expression: diff 805bf5e: 8d 5e 68 lea 0x68(%esi),%ebx 805bf61: b8 02 01 00 00 mov $0x102,%eax - 805bf66: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 805bf66: e9 0a b3 08 00 jmp 80e7275 <_end+0x62d> ++ 805bf66: + 805bf6b: 90 nop + 805bf6c: 90 nop 805bf6d: 89 46 68 mov %eax,0x68(%esi) @@ -379,7 +380,7 @@ expression: diff 805bfa0: b9 0c 00 00 00 mov $0xc,%ecx 805bfa5: 89 5e 6c mov %ebx,0x6c(%esi) - 805bfa8: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 805bfa8: e9 da b2 08 00 jmp 80e7287 <_end+0x63f> ++ 805bfa8: + 805bfad: 90 nop + 805bfae: 90 nop 805bfaf: 6a 00 push $0x0 @@ -390,7 +391,7 @@ expression: diff 805bfda: 31 d2 xor %edx,%edx 805bfdc: be 53 30 05 53 mov $0x53053053,%esi - 805bfe1: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 805bfe1: e9 b3 b2 08 00 jmp 80e7299 <_end+0x651> ++ 805bfe1: + 805bfe6: 90 nop + 805bfe7: 90 nop 805bfe8: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -403,13 +404,13 @@ expression: diff - 805c9b5: 31 db xor %ebx,%ebx - 805c9b7: 89 c8 mov %ecx,%eax - 805c9b9: cd 80 int $0x80 -+ 805c9b5: e9 f1 a8 08 00 jmp 80e72ab <_end+0x663> ++ 805c9b5: + 805c9ba: 90 nop 805c9bb: 89 c2 mov %eax,%edx - 805c9bd: 8d 1c 28 lea (%eax,%ebp,1),%ebx - 805c9c0: 89 c8 mov %ecx,%eax - 805c9c2: cd 80 int $0x80 -+ 805c9bd: e9 ff a8 08 00 jmp 80e72c1 <_end+0x679> ++ 805c9bd: + 805c9c2: 90 nop + 805c9c3: 90 nop 805c9c4: 39 c2 cmp %eax,%edx @@ -420,7 +421,7 @@ expression: diff 807afc1: f7 d1 not %ecx 807afc3: 81 e1 80 00 00 00 and $0x80,%ecx - 807afc9: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 807afc9: e9 0a c3 06 00 jmp 80e72d8 <_end+0x690> ++ 807afc9: + 807afce: 90 nop + 807afcf: 90 nop 807afd0: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -431,7 +432,7 @@ expression: diff 807b172: b8 f0 00 00 00 mov $0xf0,%eax 807b177: 89 ce mov %ecx,%esi - 807b179: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 807b179: e9 6c c1 06 00 jmp 80e72ea <_end+0x6a2> ++ 807b179: + 807b17e: 90 nop + 807b17f: 90 nop 807b180: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -442,7 +443,7 @@ expression: diff 807b340: b9 07 00 00 00 mov $0x7,%ecx 807b345: 89 d6 mov %edx,%esi - 807b347: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 807b347: e9 b0 bf 06 00 jmp 80e72fc <_end+0x6b4> ++ 807b347: + 807b34c: 90 nop + 807b34d: 90 nop 807b34e: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -453,7 +454,7 @@ expression: diff 807b8fe: 81 e1 80 00 00 00 and $0x80,%ecx 807b904: 80 f1 81 xor $0x81,%cl - 807b907: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 807b907: e9 02 ba 06 00 jmp 80e730e <_end+0x6c6> ++ 807b907: + 807b90c: 90 nop + 807b90d: 90 nop 807b90e: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -464,7 +465,7 @@ expression: diff 807bb85: b8 f0 00 00 00 mov $0xf0,%eax 807bb8a: 89 d1 mov %edx,%ecx - 807bb8c: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 807bb8c: e9 8f b7 06 00 jmp 80e7320 <_end+0x6d8> ++ 807bb8c: + 807bb91: 90 nop + 807bb92: 90 nop 807bb93: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -475,7 +476,7 @@ expression: diff 807bbb5: b8 f0 00 00 00 mov $0xf0,%eax 807bbba: 89 d6 mov %edx,%esi - 807bbbc: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 807bbbc: e9 71 b7 06 00 jmp 80e7332 <_end+0x6ea> ++ 807bbbc: + 807bbc1: 90 nop + 807bbc2: 90 nop 807bbc3: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -486,7 +487,7 @@ expression: diff 807bddf: b9 80 00 00 00 mov $0x80,%ecx 807bde4: 89 fb mov %edi,%ebx - 807bde6: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 807bde6: e9 59 b5 06 00 jmp 80e7344 <_end+0x6fc> ++ 807bde6: + 807bdeb: 90 nop + 807bdec: 90 nop 807bded: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -497,7 +498,7 @@ expression: diff 807be90: 89 fb mov %edi,%ebx 807be92: c7 07 02 00 00 00 movl $0x2,(%edi) - 807be98: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 807be98: e9 b9 b4 06 00 jmp 80e7356 <_end+0x70e> ++ 807be98: + 807be9d: 90 nop + 807be9e: 90 nop 807be9f: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -508,7 +509,7 @@ expression: diff 807bf1f: 8b 5c 24 10 mov 0x10(%esp),%ebx 807bf23: c7 03 00 00 00 00 movl $0x0,(%ebx) - 807bf29: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 807bf29: e9 3a b4 06 00 jmp 80e7368 <_end+0x720> ++ 807bf29: + 807bf2e: 90 nop + 807bf2f: 90 nop 807bf30: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -519,7 +520,7 @@ expression: diff 807c11e: c1 e1 07 shl $0x7,%ecx 807c121: 80 f1 81 xor $0x81,%cl - 807c124: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 807c124: e9 51 b2 06 00 jmp 80e737a <_end+0x732> ++ 807c124: + 807c129: 90 nop + 807c12a: 90 nop 807c12b: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -530,7 +531,7 @@ expression: diff 807c295: 89 eb mov %ebp,%ebx 807c297: 80 f1 81 xor $0x81,%cl - 807c29a: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 807c29a: e9 ed b0 06 00 jmp 80e738c <_end+0x744> ++ 807c29a: + 807c29f: 90 nop + 807c2a0: 90 nop 807c2a1: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -541,7 +542,7 @@ expression: diff 807c2f4: 31 f6 xor %esi,%esi 807c2f6: 80 f1 81 xor $0x81,%cl - 807c2f9: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 807c2f9: e9 a0 b0 06 00 jmp 80e739e <_end+0x756> ++ 807c2f9: + 807c2fe: 90 nop + 807c2ff: 90 nop 807c300: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -552,7 +553,7 @@ expression: diff 807c385: 89 fb mov %edi,%ebx 807c387: 80 f1 81 xor $0x81,%cl - 807c38a: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 807c38a: e9 21 b0 06 00 jmp 80e73b0 <_end+0x768> ++ 807c38a: + 807c38f: 90 nop + 807c390: 90 nop 807c391: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -563,7 +564,7 @@ expression: diff 807c3fa: ba ff ff ff 7f mov $0x7fffffff,%edx 807c3ff: 80 f1 81 xor $0x81,%cl - 807c402: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 807c402: e9 bb af 06 00 jmp 80e73c2 <_end+0x77a> ++ 807c402: + 807c407: 90 nop + 807c408: 90 nop 807c409: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -574,7 +575,7 @@ expression: diff 807c6af: ba 01 00 00 00 mov $0x1,%edx 807c6b4: 80 f1 81 xor $0x81,%cl - 807c6b7: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 807c6b7: e9 18 ad 06 00 jmp 80e73d4 <_end+0x78c> ++ 807c6b7: + 807c6bc: 90 nop + 807c6bd: 90 nop 807c6be: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -585,7 +586,7 @@ expression: diff 807c6de: 89 fb mov %edi,%ebx 807c6e0: 80 f1 81 xor $0x81,%cl - 807c6e3: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 807c6e3: e9 fe ac 06 00 jmp 80e73e6 <_end+0x79e> ++ 807c6e3: + 807c6e8: 90 nop + 807c6e9: 90 nop 807c6ea: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -596,7 +597,7 @@ expression: diff 807c75d: 31 f6 xor %esi,%esi 807c75f: 80 f1 81 xor $0x81,%cl - 807c762: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 807c762: e9 91 ac 06 00 jmp 80e73f8 <_end+0x7b0> ++ 807c762: + 807c767: 90 nop + 807c768: 90 nop 807c769: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -607,7 +608,7 @@ expression: diff 808a570: 0f 47 d0 cmova %eax,%edx 808a573: b8 dc 00 00 00 mov $0xdc,%eax - 808a578: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808a578: e9 8d ce 05 00 jmp 80e740a <_end+0x7c2> ++ 808a578: + 808a57d: 90 nop + 808a57e: 90 nop 808a57f: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -618,7 +619,7 @@ expression: diff 808a706: 8b 5c 24 04 mov 0x4(%esp),%ebx 808a70a: b8 9b 00 00 00 mov $0x9b,%eax - 808a70f: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808a70f: e9 08 cd 05 00 jmp 80e741c <_end+0x7d4> ++ 808a70f: + 808a714: 90 nop + 808a715: 90 nop 808a716: 89 d3 mov %edx,%ebx @@ -629,7 +630,7 @@ expression: diff 808a732: 8b 5c 24 04 mov 0x4(%esp),%ebx 808a736: b8 9d 00 00 00 mov $0x9d,%eax - 808a73b: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808a73b: e9 ee cc 05 00 jmp 80e742e <_end+0x7e6> ++ 808a73b: + 808a740: 90 nop + 808a741: 90 nop 808a742: 89 d3 mov %edx,%ebx @@ -640,7 +641,7 @@ expression: diff 808a752: 8b 5c 24 04 mov 0x4(%esp),%ebx 808a756: b8 9f 00 00 00 mov $0x9f,%eax - 808a75b: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808a75b: e9 e0 cc 05 00 jmp 80e7440 <_end+0x7f8> ++ 808a75b: + 808a760: 90 nop + 808a761: 90 nop 808a762: 89 d3 mov %edx,%ebx @@ -651,7 +652,7 @@ expression: diff 808a772: 8b 5c 24 04 mov 0x4(%esp),%ebx 808a776: b8 a0 00 00 00 mov $0xa0,%eax - 808a77b: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808a77b: e9 d2 cc 05 00 jmp 80e7452 <_end+0x80a> ++ 808a77b: + 808a780: 90 nop + 808a781: 90 nop 808a782: 89 d3 mov %edx,%ebx @@ -662,7 +663,7 @@ expression: diff 808a799: 8b 5c 24 08 mov 0x8(%esp),%ebx 808a79d: b8 9c 00 00 00 mov $0x9c,%eax - 808a7a2: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808a7a2: e9 bd cc 05 00 jmp 80e7464 <_end+0x81c> ++ 808a7a2: + 808a7a7: 90 nop + 808a7a8: 90 nop 808a7a9: 5b pop %ebx @@ -673,7 +674,7 @@ expression: diff 808a837: b8 b7 00 00 00 mov $0xb7,%eax 808a83c: 89 f1 mov %esi,%ecx - 808a83e: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808a83e: e9 33 cc 05 00 jmp 80e7476 <_end+0x82e> ++ 808a83e: + 808a843: 90 nop + 808a844: 90 nop 808a845: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -684,7 +685,7 @@ expression: diff 808b0ac: 8b 7c 24 3c mov 0x3c(%esp),%edi 808b0b0: b8 8c 00 00 00 mov $0x8c,%eax - 808b0b5: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808b0b5: e9 ce c3 05 00 jmp 80e7488 <_end+0x840> ++ 808b0b5: + 808b0ba: 90 nop + 808b0bb: 90 nop 808b0bc: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -695,7 +696,7 @@ expression: diff 808b1fe: bb 9c ff ff ff mov $0xffffff9c,%ebx 808b203: 89 ea mov %ebp,%edx - 808b205: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808b205: e9 90 c2 05 00 jmp 80e749a <_end+0x852> ++ 808b205: + 808b20a: 90 nop + 808b20b: 90 nop 808b20c: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -706,7 +707,7 @@ expression: diff 808b242: 89 44 24 08 mov %eax,0x8(%esp) 808b246: b8 27 01 00 00 mov $0x127,%eax - 808b24b: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808b24b: e9 5c c2 05 00 jmp 80e74ac <_end+0x864> ++ 808b24b: + 808b250: 90 nop + 808b251: 90 nop 808b252: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -717,7 +718,7 @@ expression: diff 808b2f0: b8 27 01 00 00 mov $0x127,%eax 808b2f5: 89 ea mov %ebp,%edx - 808b2f7: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808b2f7: e9 c2 c1 05 00 jmp 80e74be <_end+0x876> ++ 808b2f7: + 808b2fc: 90 nop + 808b2fd: 90 nop 808b2fe: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -728,7 +729,7 @@ expression: diff 808b331: 89 44 24 08 mov %eax,0x8(%esp) 808b335: b8 27 01 00 00 mov $0x127,%eax - 808b33a: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808b33a: e9 91 c1 05 00 jmp 80e74d0 <_end+0x888> ++ 808b33a: + 808b33f: 90 nop + 808b340: 90 nop 808b341: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -739,7 +740,7 @@ expression: diff 808b3c3: b8 03 00 00 00 mov $0x3,%eax 808b3c8: 8b 54 24 28 mov 0x28(%esp),%edx - 808b3cc: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808b3cc: e9 11 c1 05 00 jmp 80e74e2 <_end+0x89a> ++ 808b3cc: + 808b3d1: 90 nop + 808b3d2: 90 nop 808b3d3: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -750,7 +751,7 @@ expression: diff 808b3f9: 8b 54 24 28 mov 0x28(%esp),%edx 808b3fd: b8 03 00 00 00 mov $0x3,%eax - 808b402: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808b402: e9 ed c0 05 00 jmp 80e74f4 <_end+0x8ac> ++ 808b402: + 808b407: 90 nop + 808b408: 90 nop 808b409: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -761,7 +762,7 @@ expression: diff 808b483: b8 04 00 00 00 mov $0x4,%eax 808b488: 8b 54 24 28 mov 0x28(%esp),%edx - 808b48c: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808b48c: e9 75 c0 05 00 jmp 80e7506 <_end+0x8be> ++ 808b48c: + 808b491: 90 nop + 808b492: 90 nop 808b493: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -772,7 +773,7 @@ expression: diff 808b4b9: 8b 54 24 28 mov 0x28(%esp),%edx 808b4bd: b8 04 00 00 00 mov $0x4,%eax - 808b4c2: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808b4c2: e9 51 c0 05 00 jmp 80e7518 <_end+0x8d0> ++ 808b4c2: + 808b4c7: 90 nop + 808b4c8: 90 nop 808b4c9: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -783,7 +784,7 @@ expression: diff 808b525: 8b 6f 08 mov 0x8(%edi),%ebp 808b528: 8b 7f 04 mov 0x4(%edi),%edi - 808b52b: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808b52b: e9 fa bf 05 00 jmp 80e752a <_end+0x8e2> ++ 808b52b: + 808b530: 90 nop + 808b531: 90 nop 808b532: 5d pop %ebp @@ -795,7 +796,7 @@ expression: diff 808b545: 8b 6f 08 mov 0x8(%edi),%ebp - 808b548: 8b 7f 04 mov 0x4(%edi),%edi - 808b54b: cd 80 int $0x80 -+ 808b548: e9 ef bf 05 00 jmp 80e753c <_end+0x8f4> ++ 808b548: 808b54d: 5d pop %ebp 808b54e: 5f pop %edi 808b54f: 5b pop %ebx @@ -804,7 +805,7 @@ expression: diff 808b58b: b8 27 01 00 00 mov $0x127,%eax 808b590: bb 9c ff ff ff mov $0xffffff9c,%ebx - 808b595: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808b595: e9 b7 bf 05 00 jmp 80e7551 <_end+0x909> ++ 808b595: + 808b59a: 90 nop + 808b59b: 90 nop 808b59c: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -815,7 +816,7 @@ expression: diff 808b608: 8b 5c 24 10 mov 0x10(%esp),%ebx 808b60c: b8 27 01 00 00 mov $0x127,%eax - 808b611: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808b611: e9 4d bf 05 00 jmp 80e7563 <_end+0x91b> ++ 808b611: + 808b616: 90 nop + 808b617: 90 nop 808b618: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -826,7 +827,7 @@ expression: diff 808b670: 8b 74 24 20 mov 0x20(%esp),%esi 808b674: 8b 7c 24 24 mov 0x24(%esp),%edi - 808b678: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808b678: e9 f8 be 05 00 jmp 80e7575 <_end+0x92d> ++ 808b678: + 808b67d: 90 nop + 808b67e: 90 nop 808b67f: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -837,7 +838,7 @@ expression: diff 808b6c6: 8b 54 24 14 mov 0x14(%esp),%edx 808b6ca: 8b 5c 24 0c mov 0xc(%esp),%ebx - 808b6ce: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808b6ce: e9 b4 be 05 00 jmp 80e7587 <_end+0x93f> ++ 808b6ce: + 808b6d3: 90 nop + 808b6d4: 90 nop 808b6d5: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -848,7 +849,7 @@ expression: diff 808b72a: 8d 54 24 08 lea 0x8(%esp),%edx 808b72e: b8 36 00 00 00 mov $0x36,%eax - 808b733: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808b733: e9 61 be 05 00 jmp 80e7599 <_end+0x951> ++ 808b733: + 808b738: 90 nop + 808b739: 90 nop 808b73a: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -859,7 +860,7 @@ expression: diff 808b801: 8b 4c 24 0c mov 0xc(%esp),%ecx 808b805: 8b 5c 24 08 mov 0x8(%esp),%ebx - 808b809: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 808b809: e9 9d bd 05 00 jmp 80e75ab <_end+0x963> ++ 808b809: + 808b80e: 90 nop + 808b80f: 90 nop 808b810: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -870,7 +871,7 @@ expression: diff 8092201: 8d 58 1c lea 0x1c(%eax),%ebx 8092204: b8 f0 00 00 00 mov $0xf0,%eax - 8092209: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 8092209: e9 af 53 05 00 jmp 80e75bd <_end+0x975> ++ 8092209: + 809220e: 90 nop + 809220f: 90 nop 8092210: 83 ec 0c sub $0xc,%esp @@ -881,7 +882,7 @@ expression: diff 8092e5f: 8d 8d ea dc fc ff lea -0x32316(%ebp),%ecx 8092e65: 89 fa mov %edi,%edx - 8092e67: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 8092e67: e9 63 47 05 00 jmp 80e75cf <_end+0x987> ++ 8092e67: + 8092e6c: 90 nop + 8092e6d: 90 nop 8092e6e: 85 c0 test %eax,%eax @@ -893,7 +894,7 @@ expression: diff 80930a6: 8d 4c 24 30 lea 0x30(%esp),%ecx - 80930aa: b8 92 00 00 00 mov $0x92,%eax - 80930af: cd 80 int $0x80 -+ 80930aa: e9 32 45 05 00 jmp 80e75e1 <_end+0x999> ++ 80930aa: + 80930af: 90 nop + 80930b0: 90 nop 80930b1: 81 c4 bc 04 00 00 add $0x4bc,%esp @@ -904,7 +905,7 @@ expression: diff 809521e: 31 f6 xor %esi,%esi 8095220: 89 f8 mov %edi,%eax - 8095222: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 8095222: e9 d1 23 05 00 jmp 80e75f8 <_end+0x9b0> ++ 8095222: + 8095227: 90 nop + 8095228: 90 nop 8095229: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -915,7 +916,7 @@ expression: diff 80952b6: 31 f6 xor %esi,%esi 80952b8: 89 f8 mov %edi,%eax - 80952ba: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 80952ba: e9 4b 23 05 00 jmp 80e760a <_end+0x9c2> ++ 80952ba: + 80952bf: 90 nop + 80952c0: 90 nop 80952c1: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -927,7 +928,7 @@ expression: diff 08098e30 <__restore_rt>: - 8098e30: b8 ad 00 00 00 mov $0xad,%eax - 8098e35: cd 80 int $0x80 -+ 8098e30: e9 e7 e7 04 00 jmp 80e761c <_end+0x9d4> ++ 8098e30: + 8098e35: 90 nop + 8098e36: 90 nop 8098e37: 90 nop @@ -936,7 +937,7 @@ expression: diff 8098e38: 58 pop %eax - 8098e39: b8 77 00 00 00 mov $0x77,%eax - 8098e3e: cd 80 int $0x80 -+ 8098e39: e9 f5 e7 04 00 jmp 80e7633 <_end+0x9eb> ++ 8098e39: + 8098e3e: 90 nop + 8098e3f: 90 nop @@ -947,7 +948,7 @@ expression: diff 8098eca: be 08 00 00 00 mov $0x8,%esi 8098ecf: 8d 94 24 a0 00 00 00 lea 0xa0(%esp),%edx - 8098ed6: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 8098ed6: e9 6f e7 04 00 jmp 80e764a <_end+0xa02> ++ 8098ed6: + 8098edb: 90 nop + 8098edc: 90 nop 8098edd: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -958,7 +959,7 @@ expression: diff 8098f6c: be 08 00 00 00 mov $0x8,%esi 8098f71: 89 ea mov %ebp,%edx - 8098f73: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 8098f73: e9 e4 e6 04 00 jmp 80e765c <_end+0xa14> ++ 8098f73: + 8098f78: 90 nop + 8098f79: 90 nop 8098f7a: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -969,7 +970,7 @@ expression: diff 809e209: 31 f6 xor %esi,%esi 809e20b: 89 e8 mov %ebp,%eax - 809e20d: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 809e20d: e9 5c 94 04 00 jmp 80e766e <_end+0xa26> ++ 809e20d: + 809e212: 90 nop + 809e213: 90 nop 809e214: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -980,7 +981,7 @@ expression: diff 809e753: b8 a6 01 00 00 mov $0x1a6,%eax 809e758: 31 d2 xor %edx,%edx - 809e75a: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 809e75a: e9 21 8f 04 00 jmp 80e7680 <_end+0xa38> ++ 809e75a: + 809e75f: 90 nop + 809e760: 90 nop 809e761: 83 f8 da cmp $0xffffffda,%eax @@ -991,7 +992,7 @@ expression: diff 809e7f3: b8 f0 00 00 00 mov $0xf0,%eax 809e7f8: 31 d2 xor %edx,%edx - 809e7fa: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 809e7fa: e9 93 8e 04 00 jmp 80e7692 <_end+0xa4a> ++ 809e7fa: + 809e7ff: 90 nop + 809e800: 90 nop 809e801: 83 f8 da cmp $0xffffffda,%eax @@ -1002,7 +1003,7 @@ expression: diff 809eae1: 89 54 24 08 mov %edx,0x8(%esp) 809eae5: 8d 8d dc 73 fe ff lea -0x18c24(%ebp),%ecx - 809eaeb: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 809eaeb: e9 b4 8b 04 00 jmp 80e76a4 <_end+0xa5c> ++ 809eaeb: + 809eaf0: 90 nop + 809eaf1: 90 nop 809eaf2: ba 01 00 00 00 mov $0x1,%edx @@ -1013,7 +1014,7 @@ expression: diff 809eb2d: 8b 4c 24 08 mov 0x8(%esp),%ecx 809eb31: be 08 00 00 00 mov $0x8,%esi - 809eb36: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 809eb36: e9 7b 8b 04 00 jmp 80e76b6 <_end+0xa6e> ++ 809eb36: + 809eb3b: 90 nop + 809eb3c: 90 nop 809eb3d: 8b 44 24 1c mov 0x1c(%esp),%eax @@ -1024,7 +1025,7 @@ expression: diff 809eb6e: 89 c3 mov %eax,%ebx 809eb70: b8 0e 01 00 00 mov $0x10e,%eax - 809eb75: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 809eb75: e9 4e 8b 04 00 jmp 80e76c8 <_end+0xa80> ++ 809eb75: + 809eb7a: 90 nop + 809eb7b: 90 nop 809eb7c: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -1035,7 +1036,7 @@ expression: diff 809eb89: 8d b4 26 00 00 00 00 lea 0x0(%esi,%eiz,1),%esi 809eb90: b8 e0 00 00 00 mov $0xe0,%eax - 809eb95: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 809eb95: e9 40 8b 04 00 jmp 80e76da <_end+0xa92> ++ 809eb95: + 809eb9a: 90 nop + 809eb9b: 90 nop 809eb9c: 89 eb mov %ebp,%ebx @@ -1046,7 +1047,7 @@ expression: diff 809ebab: 89 c3 mov %eax,%ebx 809ebad: b8 0e 01 00 00 mov $0x10e,%eax - 809ebb2: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 809ebb2: e9 35 8b 04 00 jmp 80e76ec <_end+0xaa4> ++ 809ebb2: + 809ebb7: 90 nop + 809ebb8: 90 nop 809ebb9: 89 c7 mov %eax,%edi @@ -1057,7 +1058,7 @@ expression: diff 809ec91: b8 af 00 00 00 mov $0xaf,%eax 809ec96: be 08 00 00 00 mov $0x8,%esi - 809ec9b: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 809ec9b: e9 5e 8a 04 00 jmp 80e76fe <_end+0xab6> ++ 809ec9b: + 809eca0: 90 nop + 809eca1: 90 nop 809eca2: 89 c2 mov %eax,%edx @@ -1068,7 +1069,7 @@ expression: diff 0809f8e0 <__getpid>: 809f8e0: b8 14 00 00 00 mov $0x14,%eax - 809f8e5: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 809f8e5: e9 26 7e 04 00 jmp 80e7710 <_end+0xac8> ++ 809f8e5: + 809f8ea: 90 nop + 809f8eb: 90 nop 809f8ec: c3 ret @@ -1079,7 +1080,7 @@ expression: diff 80a009b: b8 8c 00 00 00 mov $0x8c,%eax 80a00a0: 8b 4c 24 0c mov 0xc(%esp),%ecx - 80a00a4: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 80a00a4: e9 79 76 04 00 jmp 80e7722 <_end+0xada> ++ 80a00a4: + 80a00a9: 90 nop + 80a00aa: 90 nop 80a00ab: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -1090,7 +1091,7 @@ expression: diff 80a3578: 8d 58 1c lea 0x1c(%eax),%ebx 80a357b: b8 f0 00 00 00 mov $0xf0,%eax - 80a3580: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 80a3580: e9 af 41 04 00 jmp 80e7734 <_end+0xaec> ++ 80a3580: + 80a3585: 90 nop + 80a3586: 90 nop 80a3587: eb 87 jmp 80a3510 <_dl_fixup+0xf0> @@ -1101,7 +1102,7 @@ expression: diff 80a718c: 8d 58 1c lea 0x1c(%eax),%ebx 80a718f: b8 f0 00 00 00 mov $0xf0,%eax - 80a7194: 65 ff 15 10 00 00 00 call *%gs:0x10 -+ 80a7194: e9 ad 05 04 00 jmp 80e7746 <_end+0xafe> ++ 80a7194: + 80a7199: 90 nop + 80a719a: 90 nop 80a719b: 8b 44 24 1c mov 0x1c(%esp),%eax 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 1ae0fc2b4..9f933eb4d 100644 --- a/litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-diff.snap +++ b/litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-diff.snap @@ -1,5 +1,6 @@ --- source: litebox_syscall_rewriter/tests/snapshot_tests.rs +assertion_line: 99 expression: diff --- --- original @@ -10,7 +11,7 @@ expression: diff 401222: bf 01 00 00 00 mov $0x1,%edi - 401227: b8 0e 00 00 00 mov $0xe,%eax - 40122c: 0f 05 syscall -+ 401227: e9 dc 0d 0b 00 jmp 4b2008 <_end+0xde0> ++ 401227: + 40122c: 90 nop + 40122d: 90 nop 40122e: 8b 05 0c fc 0a 00 mov 0xafc0c(%rip),%eax # 4b0e40 @@ -23,7 +24,7 @@ expression: diff - 401e78: 31 ff xor %edi,%edi - 401e7a: 89 d0 mov %edx,%eax - 401e7c: 0f 05 syscall -+ 401e78: e9 9d 01 0b 00 jmp 4b201a <_end+0xdf2> ++ 401e78: + 401e7d: 90 nop 401e7e: eb f8 jmp 401e78 <__libc_start_call_main+0x88> 401e80: 31 c0 xor %eax,%eax @@ -34,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: e9 41 e1 0a 00 jmp 4b202b <_end+0xe03> ++ 403ee5: + 403eea: 90 nop + 403eeb: 90 nop 403eec: 44 89 ef mov %r13d,%edi @@ -46,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: e9 67 dc 0a 00 jmp 4b203d <_end+0xe15> ++ 4043d1: + 4043d6: 90 nop 4043d7: 85 c0 test %eax,%eax 4043d9: 74 24 je 4043ff <__libc_setup_tls+0x1df> @@ -55,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: e9 5f dc 0a 00 jmp 4b204e <_end+0xe26> ++ 4043ea: + 4043ef: 90 nop + 4043f0: 90 nop + 4043f1: 90 nop @@ -63,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: e9 65 dc 0a 00 jmp 4b2062 <_end+0xe3a> ++ 4043f8: + 4043fd: 90 nop + 4043fe: 90 nop 4043ff: e8 dc ba 01 00 call 41fee0 <__tls_init_tp> @@ -75,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: e9 b0 db 0a 00 jmp 4b2074 <_end+0xe4c> ++ 4044bf: + 4044c4: 90 nop + 4044c5: 90 nop + 4044c6: 90 nop @@ -83,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: e9 b6 db 0a 00 jmp 4b2088 <_end+0xe60> ++ 4044cd: + 4044d2: 90 nop + 4044d3: 90 nop 4044d4: e9 70 fe ff ff jmp 404349 <__libc_setup_tls+0x129> @@ -95,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: e9 a9 7c 0a 00 jmp 4b209a <_end+0xe72> ++ 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 @@ -105,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: e9 d1 7a 0a 00 jmp 4b20aa <_end+0xe82> ++ 40a5d4: + 40a5d9: 90 nop + 40a5da: 90 nop 40a5db: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -117,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: e9 7d 7a 0a 00 jmp 4b20bc <_end+0xe94> ++ 40a63a: + 40a63f: 90 nop 40a640: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax 40a646: 76 d6 jbe 40a61e <__lll_lock_wait+0xe> @@ -128,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: e9 47 7a 0a 00 jmp 4b20cd <_end+0xea5> ++ 40a681: + 40a686: 90 nop + 40a687: 90 nop 40a688: c3 ret @@ -140,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: e9 3a 7a 0a 00 jmp 4b20df <_end+0xeb7> ++ 40a6a0: + 40a6a5: 90 nop + 40a6a6: 90 nop 40a6a7: c3 ret @@ -152,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: e9 0a 65 0a 00 jmp 4b20f1 <_end+0xec9> ++ 40bbe2: + 40bbe7: 90 nop + 40bbe8: 90 nop 40bbe9: 48 8d 5d d0 lea -0x30(%rbp),%rbx @@ -164,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: e9 1e 9f 09 00 jmp 4b2103 <_end+0xedb> ++ 4181e0: + 4181e5: 90 nop + 4181e6: 90 nop 4181e7: 85 c0 test %eax,%eax @@ -176,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: e9 c5 9e 09 00 jmp 4b2115 <_end+0xeed> ++ 41824b: + 418250: 90 nop + 418251: 90 nop + 418252: 90 nop @@ -189,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: e9 c4 9e 09 00 jmp 4b212d <_end+0xf05> ++ 418264: + 418269: 90 nop + 41826a: 90 nop 41826b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -201,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: e9 a6 9e 09 00 jmp 4b213f <_end+0xf17> ++ 418294: + 418299: 90 nop + 41829a: 90 nop 41829b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -213,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: e9 51 9e 09 00 jmp 4b2151 <_end+0xf29> ++ 4182fb: + 418300: 90 nop + 418301: 90 nop 418302: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -225,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: e9 35 9e 09 00 jmp 4b2163 <_end+0xf3b> ++ 418329: + 41832e: 90 nop + 41832f: 90 nop 418330: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -237,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: e9 f0 9d 09 00 jmp 4b2175 <_end+0xf4d> ++ 418380: + 418385: 90 nop + 418386: 90 nop 418387: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -249,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: e9 d9 9d 09 00 jmp 4b2187 <_end+0xf5f> ++ 4183a9: + 4183ae: 90 nop + 4183af: 90 nop 4183b0: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -261,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: e9 77 9d 09 00 jmp 4b2199 <_end+0xf71> ++ 41841d: + 418422: 90 nop + 418423: 90 nop 418424: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -274,7 +275,7 @@ expression: diff - 418480: f3 0f 1e fa endbr64 - 418484: 31 c0 xor %eax,%eax - 418486: 0f 05 syscall -+ 418480: e9 26 9d 09 00 jmp 4b21ab <_end+0xf83> ++ 418480: + 418485: 90 nop + 418486: 90 nop + 418487: 90 nop @@ -287,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: e9 05 9d 09 00 jmp 4b21be <_end+0xf96> ++ 4184b4: + 4184b9: 90 nop + 4184ba: 90 nop 4184bb: 48 89 05 96 83 09 00 mov %rax,0x98396(%rip) # 4b0858 <__curbrk> @@ -299,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: e9 aa 9a 09 00 jmp 4b21d0 <_end+0xfa8> ++ 418721: + 418726: 90 nop + 418727: 90 nop 418728: 85 c0 test %eax,%eax @@ -311,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: e9 99 96 09 00 jmp 4b21e2 <_end+0xfba> ++ 418b44: + 418b49: 90 nop + 418b4a: 90 nop 418b4b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax @@ -323,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: e9 5a 96 09 00 jmp 4b21f4 <_end+0xfcc> ++ 418b95: + 418b9a: 90 nop + 418b9b: 90 nop 418b9c: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -335,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: e9 0f 96 09 00 jmp 4b2206 <_end+0xfde> ++ 418bf2: + 418bf7: 90 nop 418bf8: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax 418bfe: 76 a4 jbe 418ba4 <__mmap64+0x34> @@ -346,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: e9 de 95 09 00 jmp 4b2217 <_end+0xfef> ++ 418c34: + 418c39: 90 nop + 418c3a: 90 nop 418c3b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax @@ -358,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: e9 c0 95 09 00 jmp 4b2229 <_end+0x1001> ++ 418c64: + 418c69: 90 nop + 418c6a: 90 nop 418c6b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax @@ -370,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: e9 ec 94 09 00 jmp 4b223b <_end+0x1013> ++ 418d4a: + 418d4f: 90 nop + 418d50: 90 nop 418d51: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -382,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: e9 20 94 09 00 jmp 4b224d <_end+0x1025> ++ 418e28: + 418e2d: 90 nop + 418e2e: 90 nop 418e2f: 83 f8 ea cmp $0xffffffea,%eax @@ -394,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: e9 06 94 09 00 jmp 4b225f <_end+0x1037> ++ 418e54: + 418e59: 90 nop + 418e5a: 90 nop 418e5b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax @@ -406,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: e9 d1 3d 09 00 jmp 4b2271 <_end+0x1049> ++ 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 @@ -416,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: e9 9a 3b 09 00 jmp 4b2281 <_end+0x1059> ++ 41e6e2: + 41e6e7: 90 nop + 41e6e8: 90 nop 41e6e9: 48 81 c4 38 09 00 00 add $0x938,%rsp @@ -428,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: e9 63 23 09 00 jmp 4b2293 <_end+0x106b> ++ 41ff2b: + 41ff30: 90 nop + 41ff31: 90 nop 41ff32: 89 83 d0 02 00 00 mov %eax,0x2d0(%rbx) @@ -440,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: e9 1b 23 09 00 jmp 4b22a5 <_end+0x107d> ++ 41ff85: + 41ff8a: 90 nop + 41ff8b: 90 nop + 41ff8c: 90 nop @@ -454,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: e9 bd 22 09 00 jmp 4b22b9 <_end+0x1091> ++ 41fff7: + 41fffc: 90 nop + 41fffd: 90 nop 41fffe: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -466,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: e9 7d 0f 09 00 jmp 4b22cb <_end+0x10a3> ++ 421349: + 42134e: 90 nop + 42134f: 90 nop 421350: 89 c7 mov %eax,%edi @@ -478,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: e9 30 0f 09 00 jmp 4b22dd <_end+0x10b5> ++ 4213a8: + 4213ad: 90 nop 4213ae: 48 85 c0 test %rax,%rax 4213b1: 74 15 je 4213c8 <_dl_cet_setup_features+0x38> @@ -490,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: e9 f2 0e 09 00 jmp 4b22ee <_end+0x10c6> ++ 4213f7: + 4213fc: 90 nop + 4213fd: 90 nop + 4213fe: 90 nop @@ -505,13 +506,13 @@ expression: diff - 421455: 31 ff xor %edi,%edi - 421457: 89 f0 mov %esi,%eax - 421459: 0f 05 syscall -+ 421455: e9 a8 0e 09 00 jmp 4b2302 <_end+0x10da> ++ 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: e9 b0 0e 09 00 jmp 4b2313 <_end+0x10eb> ++ 42145e: + 421463: 90 nop + 421464: 90 nop + 421465: 90 nop @@ -524,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: e9 9d 0e 09 00 jmp 4b2326 <_end+0x10fe> ++ 421484: + 421489: 90 nop + 42148a: 90 nop 42148b: 31 d2 xor %edx,%edx @@ -536,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: e9 16 d7 06 00 jmp 4b2338 <_end+0x1110> ++ 444c1d: + 444c22: 90 nop + 444c23: 90 nop 444c24: 31 c0 xor %eax,%eax @@ -548,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: e9 dd d6 06 00 jmp 4b234a <_end+0x1122> ++ 444c68: + 444c6d: 90 nop + 444c6e: 90 nop 444c6f: 48 8b 45 d8 mov -0x28(%rbp),%rax @@ -560,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: e9 ad d6 06 00 jmp 4b235c <_end+0x1134> ++ 444caa: + 444caf: 90 nop + 444cb0: 90 nop 444cb1: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -571,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: e9 a9 d6 06 00 jmp 4b236e <_end+0x1146> ++ 444cc0: + 444cc5: 90 nop + 444cc6: 90 nop 444cc7: 89 c3 mov %eax,%ebx @@ -581,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: e9 a6 d6 06 00 jmp 4b2380 <_end+0x1158> ++ 444cd5: + 444cda: 90 nop + 444cdb: 90 nop 444cdc: 89 c3 mov %eax,%ebx @@ -593,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: e9 12 d6 06 00 jmp 4b2392 <_end+0x116a> ++ 444d7b: + 444d80: 90 nop + 444d81: 90 nop + 444d82: 90 nop @@ -607,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: e9 d8 d5 06 00 jmp 4b23a6 <_end+0x117e> ++ 444dc9: + 444dce: 90 nop + 444dcf: 90 nop 444dd0: 48 8b 45 c8 mov -0x38(%rbp),%rax @@ -619,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: e9 a9 d5 06 00 jmp 4b23b8 <_end+0x1190> ++ 444e0a: + 444e0f: 90 nop + 444e10: 90 nop 444e11: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -629,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: e9 a5 d5 06 00 jmp 4b23ca <_end+0x11a2> ++ 444e20: + 444e25: 90 nop + 444e26: 90 nop 444e27: 89 c3 mov %eax,%ebx @@ -639,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: e9 a2 d5 06 00 jmp 4b23dc <_end+0x11b4> ++ 444e35: + 444e3a: 90 nop + 444e3b: 90 nop 444e3c: 41 89 c6 mov %eax,%r14d @@ -651,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: e9 e0 d2 06 00 jmp 4b23ee <_end+0x11c6> ++ 445109: + 44510e: 90 nop + 44510f: 90 nop + 445110: 90 nop @@ -664,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: e9 15 d1 06 00 jmp 4b2401 <_end+0x11d9> ++ 4452e7: + 4452ec: 90 nop + 4452ed: 90 nop 4452ee: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -676,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: e9 0a cf 06 00 jmp 4b2413 <_end+0x11eb> ++ 445504: + 445509: 90 nop + 44550a: 90 nop 44550b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -688,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: e9 71 c9 06 00 jmp 4b2425 <_end+0x11fd> ++ 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> @@ -699,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: e9 31 c7 06 00 jmp 4b2436 <_end+0x120e> ++ 445d00: + 445d05: 90 nop + 445d06: 90 nop 445d07: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -711,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: e9 17 c7 06 00 jmp 4b2448 <_end+0x1220> ++ 445d2c: + 445d31: 90 nop + 445d32: 90 nop 445d33: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -723,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: e9 43 c4 06 00 jmp 4b245a <_end+0x1232> ++ 446012: + 446017: 90 nop + 446018: 90 nop 446019: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -735,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: e9 b4 c3 06 00 jmp 4b246c <_end+0x1244> ++ 4460b3: + 4460b8: 90 nop + 4460b9: 90 nop 4460ba: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -747,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: e9 32 c3 06 00 jmp 4b247e <_end+0x1256> ++ 446147: + 44614c: 90 nop + 44614d: 90 nop 44614e: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -759,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: e9 a5 c1 06 00 jmp 4b2490 <_end+0x1268> ++ 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> @@ -770,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: e9 61 c0 06 00 jmp 4b24a1 <_end+0x1279> ++ 44643b: + 446440: 90 nop + 446441: 90 nop 446442: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -782,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: e9 1f c0 06 00 jmp 4b24b3 <_end+0x128b> ++ 44648f: + 446494: 90 nop 446495: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax 44649b: 76 83 jbe 446420 <___pthread_rwlock_unlock+0x50> @@ -793,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: e9 aa bf 06 00 jmp 4b24c4 <_end+0x129c> ++ 446515: + 44651a: 90 nop + 44651b: 90 nop 44651c: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -805,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: e9 55 bf 06 00 jmp 4b24d6 <_end+0x12ae> ++ 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> @@ -816,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: e9 89 bc 06 00 jmp 4b24e7 <_end+0x12bf> ++ 446859: + 44685e: 90 nop + 44685f: 90 nop 446860: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -828,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: e9 70 bc 06 00 jmp 4b24f9 <_end+0x12d1> ++ 446884: + 446889: 90 nop + 44688a: 90 nop 44688b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -840,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: e9 de bb 06 00 jmp 4b250b <_end+0x12e3> ++ 446928: + 44692d: 90 nop + 44692e: 90 nop 44692f: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -852,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: e9 07 bb 06 00 jmp 4b251d <_end+0x12f5> ++ 446a11: + 446a16: 90 nop + 446a17: 90 nop 446a18: 89 c2 mov %eax,%edx @@ -864,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: e9 fa 6a 05 00 jmp 4b252f <_end+0x1307> ++ 45ba30: + 45ba35: 90 nop + 45ba36: 90 nop 45ba37: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -876,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: e9 e8 69 05 00 jmp 4b2541 <_end+0x1319> ++ 45bb54: + 45bb59: 90 nop + 45bb5a: 90 nop 45bb5b: c3 ret @@ -888,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: e9 aa 69 05 00 jmp 4b2553 <_end+0x132b> ++ 45bba4: + 45bba9: 90 nop + 45bbaa: 90 nop 45bbab: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax @@ -900,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: e9 8c 69 05 00 jmp 4b2565 <_end+0x133d> ++ 45bbd4: + 45bbd9: 90 nop + 45bbda: 90 nop 45bbdb: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax @@ -912,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: e9 6e 69 05 00 jmp 4b2577 <_end+0x134f> ++ 45bc04: + 45bc09: 90 nop + 45bc0a: 90 nop 45bc0b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax @@ -924,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: e9 50 69 05 00 jmp 4b2589 <_end+0x1361> ++ 45bc34: + 45bc39: 90 nop + 45bc3a: 90 nop 45bc3b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax @@ -936,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: e9 32 69 05 00 jmp 4b259b <_end+0x1373> ++ 45bc64: + 45bc69: 90 nop + 45bc6a: 90 nop 45bc6b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax @@ -948,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: e9 94 68 05 00 jmp 4b25ad <_end+0x1385> ++ 45bd14: + 45bd19: 90 nop + 45bd1a: 90 nop 45bd1b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -960,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: e9 a6 60 05 00 jmp 4b25bf <_end+0x1397> ++ 45c514: + 45c519: 90 nop + 45c51a: 90 nop 45c51b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -972,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: e9 1e 60 05 00 jmp 4b25d1 <_end+0x13a9> ++ 45c5ae: + 45c5b3: 90 nop + 45c5b4: 90 nop 45c5b5: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -984,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: e9 c0 5f 05 00 jmp 4b25e3 <_end+0x13bb> ++ 45c61e: + 45c623: 90 nop + 45c624: 90 nop 45c625: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -996,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: e9 35 5f 05 00 jmp 4b25f5 <_end+0x13cd> ++ 45c6bb: + 45c6c0: 90 nop + 45c6c1: 90 nop 45c6c2: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -1008,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: e9 d2 5e 05 00 jmp 4b2607 <_end+0x13df> ++ 45c730: + 45c735: 90 nop + 45c736: 90 nop 45c737: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -1020,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: e9 75 5e 05 00 jmp 4b2619 <_end+0x13f1> ++ 45c79f: + 45c7a4: 90 nop + 45c7a5: 90 nop + 45c7a6: 90 nop @@ -1034,7 +1035,7 @@ expression: diff - 45c7d3: 8b 7d f8 mov -0x8(%rbp),%edi - 45c7d6: 31 c0 xor %eax,%eax - 45c7d8: 0f 05 syscall -+ 45c7d3: e9 59 5e 05 00 jmp 4b2631 <_end+0x1409> ++ 45c7d3: + 45c7d8: 90 nop + 45c7d9: 90 nop 45c7da: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -1046,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: e9 e1 5d 05 00 jmp 4b2643 <_end+0x141b> ++ 45c85d: + 45c862: 90 nop + 45c863: 90 nop 45c864: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -1058,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: e9 ba 5d 05 00 jmp 4b2655 <_end+0x142d> ++ 45c896: + 45c89b: 90 nop + 45c89c: 90 nop 45c89d: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -1070,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: e9 40 5d 05 00 jmp 4b2667 <_end+0x143f> ++ 45c922: + 45c927: 90 nop + 45c928: 90 nop 45c929: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -1082,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: e9 ed 5c 05 00 jmp 4b2679 <_end+0x1451> ++ 45c987: + 45c98c: 90 nop + 45c98d: 90 nop 45c98e: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -1094,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: e9 c2 5c 05 00 jmp 4b268b <_end+0x1463> ++ 45c9c4: + 45c9c9: 90 nop + 45c9ca: 90 nop 45c9cb: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -1106,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: e9 81 5c 05 00 jmp 4b269d <_end+0x1475> ++ 45ca17: + 45ca1c: 90 nop + 45ca1d: 90 nop 45ca1e: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -1119,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: e9 ef 5b 05 00 jmp 4b26af <_end+0x1487> ++ 45cabb: + 45cac0: 90 nop + 45cac1: 90 nop + 45cac2: 90 nop @@ -1133,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: e9 1a 27 05 00 jmp 4b26c3 <_end+0x149b> ++ 45ffa4: + 45ffa9: 90 nop + 45ffaa: 90 nop 45ffab: 48 8d 3d 6e ab 04 00 lea 0x4ab6e(%rip),%rdi # 4aab20 <_dl_load_lock> @@ -1145,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: e9 61 f6 04 00 jmp 4b26d5 <_end+0x14ad> ++ 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 @@ -1155,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: e9 d1 f5 04 00 jmp 4b26e5 <_end+0x14bd> ++ 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 @@ -1165,7 +1166,7 @@ expression: diff 00000000004669d0 <__restore_rt>: - 4669d0: 48 c7 c0 0f 00 00 00 mov $0xf,%rax - 4669d7: 0f 05 syscall -+ 4669d0: e9 20 bd 04 00 jmp 4b26f5 <_end+0x14cd> ++ 4669d0: + 4669d5: 90 nop + 4669d6: 90 nop + 4669d7: 90 nop @@ -1179,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: e9 51 bc 04 00 jmp 4b2709 <_end+0x14e1> ++ 466ab3: + 466ab8: 90 nop + 466ab9: 90 nop 466aba: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax @@ -1191,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: e9 fb 5b 04 00 jmp 4b271b <_end+0x14f3> ++ 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 @@ -1201,7 +1202,7 @@ expression: diff 46ccdf: 44 31 c6 xor %r8d,%esi - 46cce2: 45 31 c0 xor %r8d,%r8d - 46cce5: 0f 05 syscall -+ 46cce2: e9 44 5a 04 00 jmp 4b272b <_end+0x1503> ++ 46cce2: 46cce7: 85 c0 test %eax,%eax 46cce9: 7f 27 jg 46cd12 <__futex_abstimed_wait64+0x62> 46cceb: 83 f8 ea cmp $0xffffffea,%eax @@ -1211,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: e9 aa 59 04 00 jmp 4b273b <_end+0x1513> ++ 46cd8c: + 46cd91: 90 nop + 46cd92: 90 nop 46cd93: 48 89 c3 mov %rax,%rbx @@ -1223,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: e9 2e 59 04 00 jmp 4b274d <_end+0x1525> ++ 46ce1a: + 46ce1f: 90 nop + 46ce20: 90 nop 46ce21: 44 89 ef mov %r13d,%edi @@ -1235,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: e9 ec 58 04 00 jmp 4b275f <_end+0x1537> ++ 46ce6e: + 46ce73: 90 nop + 46ce74: 90 nop 46ce75: 83 f8 da cmp $0xffffffda,%eax @@ -1247,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: e9 25 34 04 00 jmp 4b2771 <_end+0x1549> ++ 46f347: + 46f34c: 90 nop + 46f34d: 90 nop 46f34e: 3d 00 f0 ff ff cmp $0xfffff000,%eax @@ -1259,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: e9 05 fe 03 00 jmp 4b2783 <_end+0x155b> ++ 472979: + 47297e: 90 nop + 47297f: 90 nop 472980: eb 8c jmp 47290e <_dl_fixup+0x10e> @@ -1271,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: e9 7c bb 03 00 jmp 4b2795 <_end+0x156d> ++ 476c14: + 476c19: 90 nop + 476c1a: 90 nop 476c1b: 48 83 7d 98 00 cmpq $0x0,-0x68(%rbp) @@ -1283,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: e9 54 b9 03 00 jmp 4b27a7 <_end+0x157f> ++ 476e4e: + 476e53: 90 nop + 476e54: 90 nop 476e55: 48 83 7d 98 00 cmpq $0x0,-0x68(%rbp) From bcb8f989c51cc86d400fa1bb1e54f45e43dd5948 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Sat, 4 Apr 2026 14:47:21 -0700 Subject: [PATCH 02/26] Syscall rewriter: harden ELF rewrite edge cases Reject unsupported Bun executables during packaging, keep rewritten trailer semantics compatible with the loader, and turn malformed rewrite failures into explicit errors. --- litebox_packager/src/lib.rs | 11 +- litebox_runner_linux_userland/src/lib.rs | 4 +- .../src/lib.rs | 12 +- litebox_syscall_rewriter/src/lib.rs | 353 +++++++++++++----- litebox_syscall_rewriter/src/main.rs | 14 +- 5 files changed, 279 insertions(+), 115 deletions(-) diff --git a/litebox_packager/src/lib.rs b/litebox_packager/src/lib.rs index 0b5490a6a..5917c5764 100644 --- a/litebox_packager/src/lib.rs +++ b/litebox_packager/src/lib.rs @@ -620,13 +620,10 @@ fn rewrite_elf(data: &[u8], path: &Path, verbose: bool) -> anyhow::Result { - if verbose { - eprintln!( - " warning: {} is a Bun-packaged executable, using as-is", - path.display() - ); - } - Ok(data.to_vec()) + anyhow::bail!( + "{} is a Bun-packaged executable and cannot be safely packaged without syscall rewriting", + path.display() + ) } Err(e) => Err(e).with_context(|| format!("failed to rewrite {}", path.display())), } diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 28521c370..cb8ee9b8f 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use anyhow::{Result, anyhow}; +use anyhow::{Context as _, Result, anyhow}; use clap::Parser; use litebox::fs::{FileSystem as _, Mode}; use litebox_platform_multiplex::Platform; @@ -191,7 +191,7 @@ pub fn run(cli_args: CliArgs) -> Result<()> { let mut skipped_addrs = Vec::new(); let rewritten = litebox_syscall_rewriter::hook_syscalls_in_elf(file.data, None, &mut skipped_addrs) - .unwrap(); + .with_context(|| format!("failed to rewrite {}", prog.display()))?; if !skipped_addrs.is_empty() { eprintln!( "warning: program has {} unpatchable syscall instruction(s) at {:?}", diff --git a/litebox_runner_optee_on_linux_userland/src/lib.rs b/litebox_runner_optee_on_linux_userland/src/lib.rs index 6a31e3926..298a32958 100644 --- a/litebox_runner_optee_on_linux_userland/src/lib.rs +++ b/litebox_runner_optee_on_linux_userland/src/lib.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use anyhow::Result; +use anyhow::{Context as _, Result}; use clap::Parser; use litebox_common_optee::{TeeUuid, UteeEntryFunc, UteeParamOwned}; use litebox_platform_multiplex::Platform; @@ -65,12 +65,13 @@ pub enum InterceptionBackend { pub fn run(cli_args: CliArgs) -> Result<()> { let ldelf_data: Vec = { let ldelf = PathBuf::from(&cli_args.ldelf); - let data = std::fs::read(ldelf).unwrap(); + let data = + std::fs::read(&ldelf).with_context(|| format!("failed to read {}", cli_args.ldelf))?; if cli_args.rewrite_syscalls { let mut skipped_addrs = Vec::new(); let rewritten = litebox_syscall_rewriter::hook_syscalls_in_elf(&data, None, &mut skipped_addrs) - .unwrap(); + .with_context(|| format!("failed to rewrite {}", cli_args.ldelf))?; if !skipped_addrs.is_empty() { eprintln!( "warning: {} has {} unpatchable syscall instruction(s) at {:?}", @@ -87,12 +88,13 @@ pub fn run(cli_args: CliArgs) -> Result<()> { let prog_data: Vec = { let prog = PathBuf::from(&cli_args.program); - let data = std::fs::read(prog).unwrap(); + let data = + std::fs::read(&prog).with_context(|| format!("failed to read {}", cli_args.program))?; if cli_args.rewrite_syscalls { let mut skipped_addrs = Vec::new(); let rewritten = litebox_syscall_rewriter::hook_syscalls_in_elf(&data, None, &mut skipped_addrs) - .unwrap(); + .with_context(|| format!("failed to rewrite {}", cli_args.program))?; if !skipped_addrs.is_empty() { eprintln!( "warning: {} has {} unpatchable syscall instruction(s) at {:?}", diff --git a/litebox_syscall_rewriter/src/lib.rs b/litebox_syscall_rewriter/src/lib.rs index f6de6503e..a354f2d68 100644 --- a/litebox_syscall_rewriter/src/lib.rs +++ b/litebox_syscall_rewriter/src/lib.rs @@ -95,8 +95,8 @@ struct TextSectionInfo { /// The `trampoline` must be an absolute address if specified; if unspecified, it will be set to /// zeros, and it is the caller's decision to overwrite it at loading time. /// -/// If it succeeds, it produces an executable with trampoline code appended at a page-aligned -/// offset after the ELF file. The file layout is: +/// If rewriting emits trampoline stubs, the returned executable has trampoline code appended at a +/// page-aligned offset after the ELF file. The file layout is: /// `[original ELF][padding to page boundary][trampoline code][header]` /// /// The header at the end contains: @@ -105,7 +105,9 @@ struct TextSectionInfo { /// - trampoline virtual address (8 bytes for 64-bit, 4 bytes for 32-bit) /// - trampoline size (8 bytes for 64-bit, 4 bytes for 32-bit) /// -/// This layout allows loaders to read just the last 32/20 bytes to get the metadata. +/// This layout allows loaders to read just the last 32/20 bytes to get the metadata. Even when +/// no syscall instructions are patched, the rewriter still appends the header and the initial +/// syscall-entry placeholder so the loader/audit path can tell the binary was processed. /// /// `skipped_addrs` receives the virtual addresses of any `syscall` /// instructions that could not be patched (replaced with `UD2` so they @@ -175,7 +177,7 @@ pub fn hook_syscalls_in_elf( let control_transfer_targets = get_control_transfer_targets(arch, &*buf, &text_sections)?; - let trampoline_base_addr = find_addr_for_trampoline_code(&file); + let trampoline_base_addr = find_addr_for_trampoline_code(&file)?; let fork_to_vfork_patch = find_fork_vfork_patch(&file, &text_sections); @@ -199,7 +201,6 @@ pub fn hook_syscalls_in_elf( let trampoline = u32::try_from(trampoline).map_err(|_| Error::TrampolineAddressTooLarge)?; trampoline_data.extend_from_slice(&trampoline.to_le_bytes()); } - // Patch syscalls in-place in buf for s in &text_sections { let section_data = section_slice_mut(buf, s)?; @@ -222,16 +223,18 @@ pub fn hook_syscalls_in_elf( // 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, rel32)) = fork_to_vfork_patch { + 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; - if off + 5 <= buf.len() { + #[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 offset {off:#x} + 5 exceeds buffer length {}", - buf.len() + "fork→vfork patch range {off:#x}..{patch_end:#x} is invalid for buffer length {}", + buf.len(), ))); } } @@ -453,7 +456,11 @@ fn hook_syscalls_in_section( let replace_start = replace_start.unwrap(); let replace_len = usize::try_from(replace_end - replace_start).unwrap(); - let target_addr = trampoline_base_addr + trampoline_data.len() as u64; + let target_addr = checked_add_u64( + trampoline_base_addr, + trampoline_data.len() as u64, + "syscall trampoline target", + )?; // Copy the original instructions to the trampoline if replace_start < inst.ip() { @@ -466,54 +473,81 @@ fn hook_syscalls_in_section( let return_addr = inst.next_ip(); if arch == Arch::X86_64 { // Put jump back location into rcx. - let jmp_back_offset = i64::try_from(return_addr).unwrap() - - i64::try_from(trampoline_base_addr + trampoline_data.len() as u64 + 7).unwrap(); + let jmp_back_base = checked_add_u64( + trampoline_base_addr, + trampoline_data.len() as u64, + "x86_64 trampoline jump-back base", + )?; + let jmp_back_base = + checked_add_u64(jmp_back_base, 7, "x86_64 trampoline jump-back base")?; trampoline_data.extend_from_slice(&[0x48, 0x8D, 0x0D]); // LEA RCX, [RIP + disp32] - trampoline_data - .extend_from_slice(&(i32::try_from(jmp_back_offset).unwrap().to_le_bytes())); + trampoline_data.extend_from_slice(&rel32_bytes( + return_addr, + jmp_back_base, + "x86_64 trampoline jump-back", + )?); // Add jmp [rip + offset_to_entry_point] trampoline_data.extend_from_slice(&[0xFF, 0x25]); // RIP after this instruction = trampoline_base_addr + trampoline_data.len() + 4 // We want: RIP + disp32 = syscall_entry_addr - #[allow(clippy::cast_possible_wrap)] - let disp32 = i64::try_from(syscall_entry_addr).unwrap() - - i64::try_from(trampoline_base_addr).unwrap() - - trampoline_data.len() as i64 - - 4; - trampoline_data.extend_from_slice(&(i32::try_from(disp32).unwrap().to_le_bytes())); + let entry_base = checked_add_u64( + trampoline_base_addr, + trampoline_data.len() as u64, + "x86_64 trampoline entry base", + )?; + let entry_base = checked_add_u64(entry_base, 4, "x86_64 trampoline entry base")?; + trampoline_data.extend_from_slice(&rel32_bytes( + syscall_entry_addr, + entry_base, + "x86_64 trampoline entry", + )?); } else { // For 32-bit, use a different approach to simulate indirect call trampoline_data.push(0x50); // PUSH EAX 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 - #[allow(clippy::cast_possible_wrap)] - let disp32 = i64::try_from(syscall_entry_addr).unwrap() - - i64::try_from(trampoline_base_addr).unwrap() - - trampoline_data.len() as i64 - + 3; - trampoline_data.extend_from_slice(&(i32::try_from(disp32).unwrap().to_le_bytes())); + // 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, + "x86 trampoline entry base", + )?; + let call_base = checked_sub_u64(call_base, 3, "x86 trampoline entry base")?; + trampoline_data.extend_from_slice(&rel32_bytes( + syscall_entry_addr, + call_base, + "x86 trampoline entry", + )?); // Note we skip `POP EAX` here as it is done by the callback `syscall_callback` // from litebox_shim_linux/src/lib.rs, which helps reduce the size of the trampoline. // Add jmp back to original after syscall - let jmp_back_offset = i64::try_from(return_addr).unwrap() - - i64::try_from(trampoline_base_addr + trampoline_data.len() as u64 + 5).unwrap(); + let jmp_back_base = checked_add_u64( + trampoline_base_addr, + trampoline_data.len() as u64, + "x86 trampoline jump-back base", + )?; + let jmp_back_base = checked_add_u64(jmp_back_base, 5, "x86 trampoline jump-back base")?; trampoline_data.push(0xE9); - trampoline_data - .extend_from_slice(&(i32::try_from(jmp_back_offset).unwrap().to_le_bytes())); + trampoline_data.extend_from_slice(&rel32_bytes( + return_addr, + jmp_back_base, + "x86 trampoline jump-back", + )?); } // Replace original instructions with jump to trampoline let replace_offset = usize::try_from(replace_start - section_base_addr).unwrap(); section_data[replace_offset] = 0xE9; // JMP rel32 - let jump_offset = - i64::try_from(target_addr).unwrap() - i64::try_from(replace_start + 5).unwrap(); - section_data[replace_offset + 1..replace_offset + 5] - .copy_from_slice(&(i32::try_from(jump_offset).unwrap().to_le_bytes())); + let patch_base = checked_add_u64(replace_start, 5, "syscall patch jump base")?; + section_data[replace_offset + 1..replace_offset + 5].copy_from_slice(&rel32_bytes( + target_addr, + patch_base, + "syscall patch jump", + )?); // Fill remaining bytes with NOP for idx in 5..replace_len { @@ -560,19 +594,40 @@ fn fixup_phdr_alignment(buf: &mut [u8]) { return; // already aligned } - let phdr_size = e_phentsize * e_phnum; - let old_start = usize::try_from(e_phoff).expect("e_phoff must fit in usize"); - let old_end = old_start + usize::try_from(phdr_size).expect("phdr_size must fit in usize"); + let Some(phdr_size) = e_phentsize.checked_mul(e_phnum) else { + return; + }; + let Ok(old_start) = usize::try_from(e_phoff) else { + return; + }; + let Ok(phdr_size) = usize::try_from(phdr_size) else { + return; + }; + let Some(old_end) = old_start.checked_add(phdr_size) else { + return; + }; // Shift forward to align: new offset is the next 8-byte boundary. - let padding = usize::try_from(8 - misalignment).expect("padding must fit in usize"); - let new_start = old_start + padding; - let new_end = new_start + usize::try_from(phdr_size).expect("phdr_size must fit in usize"); + let Ok(padding) = usize::try_from(8 - misalignment) else { + return; + }; + let Some(new_start) = old_start.checked_add(padding) else { + return; + }; + let Some(new_end) = new_start.checked_add(phdr_size) else { + return; + }; if old_end > buf.len() || new_end > buf.len() { return; // corrupt phdr table or not enough room } + // Only relocate when the overwritten bytes are padding. Otherwise this would corrupt the file + // by destroying whatever payload follows the existing program header table. + if !buf[old_end..new_end].iter().all(|&byte| byte == 0) { + return; + } + // Move the phdr table forward (use copy_within since src and dst overlap). buf.copy_within(old_start..old_end, new_start); @@ -582,10 +637,16 @@ fn fixup_phdr_alignment(buf: &mut [u8]) { // Also update the PHDR segment's p_offset if present, so it matches. // PT_PHDR = 6, each Elf64_Phdr is e_phentsize bytes, p_type at offset 0, p_offset at offset 8. - for i in 0..e_phnum { - let entry_off = new_start - + usize::try_from(i).expect("i must fit in usize") - * usize::try_from(e_phentsize).expect("e_phentsize must fit in usize"); + let Ok(e_phentsize_usize) = usize::try_from(e_phentsize) else { + return; + }; + let Ok(e_phnum_usize) = usize::try_from(e_phnum) else { + return; + }; + for i in 0..e_phnum_usize { + let Some(entry_off) = new_start.checked_add(i.saturating_mul(e_phentsize_usize)) else { + break; + }; if entry_off + 16 > buf.len() { break; } @@ -610,23 +671,40 @@ fn fixup_phdr_alignment(buf: &mut [u8]) { fn find_fork_vfork_patch( file: &object::File<'_>, text_sections: &[TextSectionInfo], -) -> Option<(u64, i32)> { +) -> Option<(u64, u64, i32)> { use object::ObjectSymbol as _; - // Search both .dynsym and .symtab for fork/vfork. let mut fork_vaddr = None; let mut vfork_vaddr = None; - for sym in file.dynamic_symbols().chain(file.symbols()) { + // 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 { - "fork" | "__libc_fork" if fork_vaddr.is_none() => { + "__libc_fork" if fork_vaddr.is_none() => { fork_vaddr = Some(sym.address()); } - "vfork" | "__libc_vfork" | "__vfork" if vfork_vaddr.is_none() => { + "__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()); } _ => {} @@ -635,12 +713,22 @@ fn find_fork_vfork_patch( 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 = text_sections.iter().find_map(|s| { + 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 { - Some(s.file_offset + (fork_vaddr - s.vaddr)) + 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 } @@ -654,7 +742,7 @@ fn find_fork_vfork_patch( .checked_sub(i64::try_from(fork_vaddr).ok()? + 5)?; let rel32 = i32::try_from(rel32).ok()?; - Some((fork_file_offset, rel32)) + Some((fork_file_offset, fork_patch_end, rel32)) } /// Check if the input binary has the Bun footer marker at the end. @@ -681,6 +769,26 @@ fn replace_with_ud2(section_data: &mut [u8], section_base_addr: u64, inst: &iced } } +fn checked_add_u64(base: u64, addend: u64, context: &'static str) -> Result { + base.checked_add(addend) + .ok_or_else(|| Error::ParseError(format!("{context} address overflow"))) +} + +fn checked_sub_u64(base: u64, subtrahend: u64, context: &'static str) -> Result { + base.checked_sub(subtrahend) + .ok_or_else(|| Error::ParseError(format!("{context} address underflow"))) +} + +fn rel32_bytes(target: u64, base: u64, context: &'static str) -> Result<[u8; 4]> { + let disp = i128::from(target) - i128::from(base); + let disp = i32::try_from(disp).map_err(|_| { + Error::ParseError(format!( + "{context} displacement out of range: target {target:#x}, base {base:#x}" + )) + })?; + Ok(disp.to_le_bytes()) +} + /// Patch a single mapped code segment in-place, returning trampoline stubs. /// /// This is the runtime counterpart to [`hook_syscalls_in_elf`]. Instead of @@ -743,20 +851,21 @@ pub fn patch_code_segment( } } -fn find_addr_for_trampoline_code(file: &object::File<'_>) -> u64 { +fn find_addr_for_trampoline_code(file: &object::File<'_>) -> Result { // Find the highest virtual address among all PT_LOAD segments let max_virtual_addr = match file { object::File::Elf64(elf) => max_load_segment_end(elf), object::File::Elf32(elf) => max_load_segment_end(elf), _ => unreachable!(), - }; + } + .ok_or_else(|| Error::ParseError("no PT_LOAD segments found".into()))?; // Round up to the nearest page (assume 0x1000 page size) - max_virtual_addr.next_multiple_of(0x1000) + checked_add_u64(max_virtual_addr, 0xFFF, "trampoline base").map(|addr| addr & !0xFFF) } /// Returns the highest `p_vaddr + p_memsz` among all `PT_LOAD` segments. -fn max_load_segment_end(elf: &ElfFile<'_, Elf>) -> u64 +fn max_load_segment_end(elf: &ElfFile<'_, Elf>) -> Option where Elf::Word: Into, { @@ -764,9 +873,12 @@ where elf.elf_program_headers() .iter() .filter(|ph| ph.p_type(endian) == object::elf::PT_LOAD) - .map(|ph| ph.p_vaddr(endian).into() + ph.p_memsz(endian).into()) + .filter_map(|ph| { + ph.p_vaddr(endian) + .into() + .checked_add(ph.p_memsz(endian).into()) + }) .max() - .unwrap() } fn get_symbols(file: &object::File<'_>) -> Option { @@ -947,7 +1059,11 @@ fn hook_syscall_and_after( let replace_end = replace_end.unwrap(); - let target_addr = trampoline_base_addr + trampoline_data.len() as u64; + let target_addr = checked_add_u64( + trampoline_base_addr, + trampoline_data.len() as u64, + "syscall trampoline target", + )?; if arch == Arch::X86_64 { // Put jump back location into rcx, via lea rcx, [next instruction] @@ -957,26 +1073,36 @@ fn hook_syscall_and_after( trampoline_data.extend_from_slice(&[0xFF, 0x25]); // RIP after this instruction = trampoline_base_addr + trampoline_data.len() + 4 // We want: RIP + disp32 = syscall_entry_addr - #[allow(clippy::cast_possible_wrap)] - let disp32 = i64::try_from(syscall_entry_addr).unwrap() - - i64::try_from(trampoline_base_addr).unwrap() - - trampoline_data.len() as i64 - - 4; - trampoline_data.extend_from_slice(&(i32::try_from(disp32).unwrap().to_le_bytes())); + let entry_base = checked_add_u64( + trampoline_base_addr, + trampoline_data.len() as u64, + "x86_64 trampoline entry base", + )?; + let entry_base = checked_add_u64(entry_base, 4, "x86_64 trampoline entry base")?; + trampoline_data.extend_from_slice(&rel32_bytes( + syscall_entry_addr, + entry_base, + "x86_64 trampoline entry", + )?); } else { // For 32-bit, use a different approach to simulate indirect call trampoline_data.push(0x50); // PUSH EAX 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 - #[allow(clippy::cast_possible_wrap)] - let disp32 = i64::try_from(syscall_entry_addr).unwrap() - - i64::try_from(trampoline_base_addr).unwrap() - - trampoline_data.len() as i64 - + 3; - trampoline_data.extend_from_slice(&(i32::try_from(disp32).unwrap().to_le_bytes())); + // 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, + "x86 trampoline entry base", + )?; + let call_base = checked_sub_u64(call_base, 3, "x86 trampoline entry base")?; + trampoline_data.extend_from_slice(&rel32_bytes( + syscall_entry_addr, + call_base, + "x86 trampoline entry", + )?); // Note we skip `POP EAX` here as it is done by the callback `syscall_callback` // from litebox_shim_linux/src/lib.rs, which helps reduce the size of the trampoline. } @@ -991,18 +1117,28 @@ fn hook_syscall_and_after( } // Add jmp back to original after syscall - let jmp_back_offset = i64::try_from(replace_end).unwrap() - - i64::try_from(trampoline_base_addr + trampoline_data.len() as u64 + 5).unwrap(); + let jmp_back_base = checked_add_u64( + trampoline_base_addr, + trampoline_data.len() as u64, + "trampoline jump-back base", + )?; + let jmp_back_base = checked_add_u64(jmp_back_base, 5, "trampoline jump-back base")?; trampoline_data.push(0xE9); - trampoline_data.extend_from_slice(&(i32::try_from(jmp_back_offset).unwrap().to_le_bytes())); + trampoline_data.extend_from_slice(&rel32_bytes( + replace_end, + jmp_back_base, + "trampoline jump-back", + )?); // Replace original instructions with jump to trampoline let replace_offset = usize::try_from(replace_start - section_base_addr).unwrap(); section_data[replace_offset] = 0xE9; // JMP rel32 - let jump_offset = - i64::try_from(target_addr).unwrap() - i64::try_from(replace_start + 5).unwrap(); - section_data[replace_offset + 1..replace_offset + 5] - .copy_from_slice(&(i32::try_from(jump_offset).unwrap().to_le_bytes())); + let patch_base = checked_add_u64(replace_start, 5, "syscall patch jump base")?; + section_data[replace_offset + 1..replace_offset + 5].copy_from_slice(&rel32_bytes( + target_addr, + patch_base, + "syscall patch jump", + )?); // Fill remaining bytes with NOP let replace_len = usize::try_from(replace_end - replace_start).unwrap(); @@ -1080,7 +1216,11 @@ fn hook_syscall_before_and_after( } }; - let target_addr = trampoline_base_addr + trampoline_data.len() as u64; + let target_addr = checked_add_u64( + trampoline_base_addr, + trampoline_data.len() as u64, + "syscall trampoline target", + )?; let replace_start = prev_inst.ip(); let replace_len = usize::try_from(next_inst.next_ip() - replace_start).unwrap(); @@ -1095,14 +1235,19 @@ 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 - #[allow(clippy::cast_possible_wrap)] - let disp32 = i64::try_from(syscall_entry_addr).unwrap() - - i64::try_from(trampoline_base_addr).unwrap() - - trampoline_data.len() as i64 - + 3; - trampoline_data.extend_from_slice(&(i32::try_from(disp32).unwrap().to_le_bytes())); + // 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, + "x86 trampoline entry base", + )?; + let call_base = checked_sub_u64(call_base, 3, "x86 trampoline entry base")?; + trampoline_data.extend_from_slice(&rel32_bytes( + syscall_entry_addr, + call_base, + "x86 trampoline entry", + )?); // Note we skip `POP EAX` here as it is done by the callback `syscall_callback` // from litebox_shim_linux/src/lib.rs, which helps reduce the size of the trampoline. @@ -1115,19 +1260,29 @@ fn hook_syscall_before_and_after( // Add jmp back to original after syscall if needed if need_jump_back { let return_addr = next_inst.next_ip(); - let jmp_back_offset = i64::try_from(return_addr).unwrap() - - i64::try_from(trampoline_base_addr + trampoline_data.len() as u64 + 5).unwrap(); + let jmp_back_base = checked_add_u64( + trampoline_base_addr, + trampoline_data.len() as u64, + "x86 trampoline jump-back base", + )?; + let jmp_back_base = checked_add_u64(jmp_back_base, 5, "x86 trampoline jump-back base")?; trampoline_data.push(0xE9); - trampoline_data.extend_from_slice(&(i32::try_from(jmp_back_offset).unwrap().to_le_bytes())); + trampoline_data.extend_from_slice(&rel32_bytes( + return_addr, + jmp_back_base, + "x86 trampoline jump-back", + )?); } // Replace original instructions with jump to trampoline let replace_offset = usize::try_from(replace_start - section_base_addr).unwrap(); section_data[replace_offset] = 0xE9; // JMP rel32 - let jump_offset = - i64::try_from(target_addr).unwrap() - i64::try_from(replace_start + 5).unwrap(); - section_data[replace_offset + 1..replace_offset + 5] - .copy_from_slice(&(i32::try_from(jump_offset).unwrap().to_le_bytes())); + let patch_base = checked_add_u64(replace_start, 5, "syscall patch jump base")?; + section_data[replace_offset + 1..replace_offset + 5].copy_from_slice(&rel32_bytes( + target_addr, + patch_base, + "syscall patch jump", + )?); // Fill remaining bytes with NOP for idx in 5..replace_len { diff --git a/litebox_syscall_rewriter/src/main.rs b/litebox_syscall_rewriter/src/main.rs index c60065981..70beb251e 100644 --- a/litebox_syscall_rewriter/src/main.rs +++ b/litebox_syscall_rewriter/src/main.rs @@ -48,11 +48,21 @@ fn main() -> anyhow::Result<()> { let mut input_binary_bytes = vec![]; input_binary.read_to_end(&mut input_binary_bytes)?; let mut skipped_addrs = Vec::new(); - let output_binary = litebox_syscall_rewriter::hook_syscalls_in_elf( + let output_binary = match litebox_syscall_rewriter::hook_syscalls_in_elf( &input_binary_bytes, cli_args.trampoline_addr, &mut skipped_addrs, - )?; + ) { + Ok(output_binary) => output_binary, + Err(litebox_syscall_rewriter::Error::NoSyscallInstructionsFound) => { + eprintln!( + "warning: {} has no syscall instructions, copying as-is", + cli_args.input_binary.display() + ); + input_binary_bytes.clone() + } + Err(err) => return Err(err.into()), + }; if !skipped_addrs.is_empty() { eprintln!( "warning: {} unpatchable syscall instruction(s) at {:?}", From eeca867748748a16f7b78b295e93896ce95bbc85 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Sat, 4 Apr 2026 14:56:03 -0700 Subject: [PATCH 03/26] Fix syscall rewriter formatting Match CI rustfmt output for the x86 trampoline comments in the rewritten ELF path. --- 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 a354f2d68..049e796ed 100644 --- a/litebox_syscall_rewriter/src/lib.rs +++ b/litebox_syscall_rewriter/src/lib.rs @@ -508,8 +508,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, @@ -1090,8 +1090,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, @@ -1235,8 +1235,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 5081c533b81605795fafb6153d01b3f4b17ef1a4 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 7 Apr 2026 01:38:15 +0000 Subject: [PATCH 04/26] address review comments --- litebox_syscall_rewriter/src/lib.rs | 47 ++++++++++++++++------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/litebox_syscall_rewriter/src/lib.rs b/litebox_syscall_rewriter/src/lib.rs index 049e796ed..7dfc0c0d5 100644 --- a/litebox_syscall_rewriter/src/lib.rs +++ b/litebox_syscall_rewriter/src/lib.rs @@ -127,7 +127,7 @@ pub fn hook_syscalls_in_elf( // Check the ELF e_type field (bytes 16..18) before doing any work. if input_binary.len() >= 18 { let e_type = u16::from_le_bytes([input_binary[16], input_binary[17]]); - if e_type == 1 { + if e_type == object::elf::ET_REL { // ET_REL — relocatable object file return Err(Error::UnsupportedObjectFile); } @@ -223,14 +223,28 @@ pub fn hook_syscalls_in_elf( // 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 { + if let Some((fork_file_offset, fork_patch_end, mut rel32)) = fork_to_vfork_patch { + const ENDBR64: [u8; 4] = [0xF3, 0x0F, 0x1E, 0xFA]; #[allow(clippy::cast_possible_truncation)] - let off = fork_file_offset as usize; + let mut off = fork_file_offset as usize; #[allow(clippy::cast_possible_truncation)] let patch_end = fork_patch_end as usize; + + // If fork starts with endbr64 (F3 0F 1E FA), preserve it by placing + // the JMP after it. This keeps CET/IBT indirect-branch targets valid. + if off + 4 <= buf.len() && buf[off..off + 4] == ENDBR64 { + off += 4; + rel32 = rel32.wrapping_sub(4); // JMP is now 4 bytes later, adjust displacement + } + 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()); + // NOP-fill remaining bytes between end of JMP and the patch boundary + // to avoid leaving stale instructions that could be jumped into. + for b in &mut buf[off + 5..patch_end] { + *b = 0x90; // NOP + } } else { return Err(Error::ParseError(format!( "fork→vfork patch range {off:#x}..{patch_end:#x} is invalid for buffer length {}", @@ -475,11 +489,9 @@ fn hook_syscalls_in_section( // Put jump back location into rcx. let jmp_back_base = checked_add_u64( trampoline_base_addr, - trampoline_data.len() as u64, + trampoline_data.len() as u64 + 7, "x86_64 trampoline jump-back base", )?; - let jmp_back_base = - checked_add_u64(jmp_back_base, 7, "x86_64 trampoline jump-back base")?; trampoline_data.extend_from_slice(&[0x48, 0x8D, 0x0D]); // LEA RCX, [RIP + disp32] trampoline_data.extend_from_slice(&rel32_bytes( return_addr, @@ -493,10 +505,9 @@ fn hook_syscalls_in_section( // We want: RIP + disp32 = syscall_entry_addr let entry_base = checked_add_u64( trampoline_base_addr, - trampoline_data.len() as u64, + trampoline_data.len() as u64 + 4, "x86_64 trampoline entry base", )?; - let entry_base = checked_add_u64(entry_base, 4, "x86_64 trampoline entry base")?; trampoline_data.extend_from_slice(&rel32_bytes( syscall_entry_addr, entry_base, @@ -512,10 +523,9 @@ fn hook_syscalls_in_section( // We want: EAX + offset = syscall_entry_addr let call_base = checked_add_u64( trampoline_base_addr, - trampoline_data.len() as u64, + trampoline_data.len() as u64 - 3, "x86 trampoline entry base", )?; - let call_base = checked_sub_u64(call_base, 3, "x86 trampoline entry base")?; trampoline_data.extend_from_slice(&rel32_bytes( syscall_entry_addr, call_base, @@ -527,10 +537,9 @@ fn hook_syscalls_in_section( // Add jmp back to original after syscall let jmp_back_base = checked_add_u64( trampoline_base_addr, - trampoline_data.len() as u64, + trampoline_data.len() as u64 + 5, "x86 trampoline jump-back base", )?; - let jmp_back_base = checked_add_u64(jmp_back_base, 5, "x86 trampoline jump-back base")?; trampoline_data.push(0xE9); trampoline_data.extend_from_slice(&rel32_bytes( return_addr, @@ -651,7 +660,7 @@ fn fixup_phdr_alignment(buf: &mut [u8]) { break; } let p_type = u32::from_le_bytes(buf[entry_off..entry_off + 4].try_into().unwrap()); - if p_type == 6 { + if p_type == object::elf::PT_PHDR { // PT_PHDR — update p_offset to match new location let p_offset_off = entry_off + 8; let old_p_offset = @@ -747,8 +756,7 @@ fn find_fork_vfork_patch( /// Check if the input binary has the Bun footer marker at 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 + input_binary.ends_with(BUN_FOOTER_MARKER) } /// Replace an unpatchable syscall instruction with `UD2` (`0F 0B`) so that @@ -1075,10 +1083,9 @@ fn hook_syscall_and_after( // We want: RIP + disp32 = syscall_entry_addr let entry_base = checked_add_u64( trampoline_base_addr, - trampoline_data.len() as u64, + trampoline_data.len() as u64 + 4, "x86_64 trampoline entry base", )?; - let entry_base = checked_add_u64(entry_base, 4, "x86_64 trampoline entry base")?; trampoline_data.extend_from_slice(&rel32_bytes( syscall_entry_addr, entry_base, @@ -1119,10 +1126,9 @@ fn hook_syscall_and_after( // Add jmp back to original after syscall let jmp_back_base = checked_add_u64( trampoline_base_addr, - trampoline_data.len() as u64, + trampoline_data.len() as u64 + 5, "trampoline jump-back base", )?; - let jmp_back_base = checked_add_u64(jmp_back_base, 5, "trampoline jump-back base")?; trampoline_data.push(0xE9); trampoline_data.extend_from_slice(&rel32_bytes( replace_end, @@ -1262,10 +1268,9 @@ fn hook_syscall_before_and_after( let return_addr = next_inst.next_ip(); let jmp_back_base = checked_add_u64( trampoline_base_addr, - trampoline_data.len() as u64, + trampoline_data.len() as u64 + 5, "x86 trampoline jump-back base", )?; - let jmp_back_base = checked_add_u64(jmp_back_base, 5, "x86 trampoline jump-back base")?; trampoline_data.push(0xE9); trampoline_data.extend_from_slice(&rel32_bytes( return_addr, From 98ea1759e884d70dc953ff27c06c1aaacc40e120 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 7 Apr 2026 03:05:51 +0000 Subject: [PATCH 05/26] fixed a bug introduced in the last commit --- litebox_syscall_rewriter/src/lib.rs | 37 ++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/litebox_syscall_rewriter/src/lib.rs b/litebox_syscall_rewriter/src/lib.rs index 7dfc0c0d5..333f25221 100644 --- a/litebox_syscall_rewriter/src/lib.rs +++ b/litebox_syscall_rewriter/src/lib.rs @@ -223,12 +223,12 @@ pub fn hook_syscalls_in_elf( // 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, mut rel32)) = fork_to_vfork_patch { + if let Some((fork_file_offset, fork_func_end, mut rel32)) = fork_to_vfork_patch { const ENDBR64: [u8; 4] = [0xF3, 0x0F, 0x1E, 0xFA]; #[allow(clippy::cast_possible_truncation)] let mut off = fork_file_offset as usize; #[allow(clippy::cast_possible_truncation)] - let patch_end = fork_patch_end as usize; + let func_end = fork_func_end as usize; // If fork starts with endbr64 (F3 0F 1E FA), preserve it by placing // the JMP after it. This keeps CET/IBT indirect-branch targets valid. @@ -237,17 +237,17 @@ pub fn hook_syscalls_in_elf( rel32 = rel32.wrapping_sub(4); // JMP is now 4 bytes later, adjust displacement } - if off + 5 <= buf.len() && patch_end <= buf.len() && off + 5 <= patch_end { + if off + 5 <= func_end && func_end <= buf.len() { buf[off] = 0xE9; // JMP rel32 buf[off + 1..off + 5].copy_from_slice(&rel32.to_le_bytes()); - // NOP-fill remaining bytes between end of JMP and the patch boundary + // NOP-fill remaining bytes of the fork function body after the JMP // to avoid leaving stale instructions that could be jumped into. - for b in &mut buf[off + 5..patch_end] { + for b in &mut buf[off + 5..func_end] { *b = 0x90; // NOP } } else { return Err(Error::ParseError(format!( - "fork→vfork patch range {off:#x}..{patch_end:#x} is invalid for buffer length {}", + "fork→vfork patch range {off:#x}..{func_end:#x} is invalid for buffer length {}", buf.len(), ))); } @@ -675,8 +675,12 @@ 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 +/// redirect fork -> vfork. Returns `Some((fork_file_offset, fork_func_end, jmp_rel32))` if /// both symbols are found, or `None` if this binary doesn't export fork. +/// +/// `fork_func_end` is the file offset of the end of the fork function (based on +/// the symbol's size), clamped to the section boundary. This is used to NOP-fill +/// only the fork function body after the JMP, not the rest of the section. fn find_fork_vfork_patch( file: &object::File<'_>, text_sections: &[TextSectionInfo], @@ -684,6 +688,7 @@ fn find_fork_vfork_patch( use object::ObjectSymbol as _; let mut fork_vaddr = None; + let mut fork_size = None; let mut vfork_vaddr = None; // Restrict this rewrite to libc-specific symbols. Plain `fork`/`vfork` names may belong to @@ -696,6 +701,7 @@ fn find_fork_vfork_patch( match name { "__libc_fork" if fork_vaddr.is_none() => { fork_vaddr = Some(sym.address()); + fork_size = Some(sym.size()); } "__libc_vfork" | "__vfork" if vfork_vaddr.is_none() => { vfork_vaddr = Some(sym.address()); @@ -712,6 +718,7 @@ fn find_fork_vfork_patch( match name { "__libc_fork" if fork_vaddr.is_none() => { fork_vaddr = Some(sym.address()); + fork_size = Some(sym.size()); } "__libc_vfork" | "__vfork" if vfork_vaddr.is_none() => { vfork_vaddr = Some(sym.address()); @@ -721,13 +728,14 @@ fn find_fork_vfork_patch( } let fork_vaddr = fork_vaddr?; + let fork_size = fork_size?; 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 (fork_file_offset, fork_func_end) = text_sections.iter().find_map(|s| { let section_end = s.vaddr + s.size; if fork_vaddr >= s.vaddr && fork_vaddr < section_end @@ -736,8 +744,15 @@ fn find_fork_vfork_patch( .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)) + let section_file_end = s.file_offset + s.size; + // Compute the end of the fork function, clamped to the section boundary. + let func_file_end = if fork_size > 0 { + (file_offset + fork_size).min(section_file_end) + } else { + // No size info — only NOP-fill the JMP itself (no extra NOPs). + file_offset + 5 + }; + Some((file_offset, func_file_end)) } else { None } @@ -751,7 +766,7 @@ fn find_fork_vfork_patch( .checked_sub(i64::try_from(fork_vaddr).ok()? + 5)?; let rel32 = i32::try_from(rel32).ok()?; - Some((fork_file_offset, fork_patch_end, rel32)) + Some((fork_file_offset, fork_func_end, rel32)) } /// Check if the input binary has the Bun footer marker at the end. From 9fc38e2fbd2c392851cc952249784c96face3fbc Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 7 Apr 2026 21:15:01 -0700 Subject: [PATCH 06/26] Remove fork-to-vfork patching from syscall rewriter --- litebox_syscall_rewriter/src/lib.rs | 140 +--------------------------- 1 file changed, 1 insertion(+), 139 deletions(-) diff --git a/litebox_syscall_rewriter/src/lib.rs b/litebox_syscall_rewriter/src/lib.rs index 333f25221..6acfa8725 100644 --- a/litebox_syscall_rewriter/src/lib.rs +++ b/litebox_syscall_rewriter/src/lib.rs @@ -147,14 +147,7 @@ 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, - fork_to_vfork_patch, - ) = { + let (arch, dl_sysinfo_int80, text_sections, control_transfer_targets, trampoline_base_addr) = { let file = object::File::parse(&*buf).map_err(|e| Error::ParseError(e.to_string()))?; let arch = match file { @@ -179,15 +172,12 @@ 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, ) }; @@ -220,39 +210,6 @@ pub fn hook_syscalls_in_elf( } } - // 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_func_end, mut rel32)) = fork_to_vfork_patch { - const ENDBR64: [u8; 4] = [0xF3, 0x0F, 0x1E, 0xFA]; - #[allow(clippy::cast_possible_truncation)] - let mut off = fork_file_offset as usize; - #[allow(clippy::cast_possible_truncation)] - let func_end = fork_func_end as usize; - - // If fork starts with endbr64 (F3 0F 1E FA), preserve it by placing - // the JMP after it. This keeps CET/IBT indirect-branch targets valid. - if off + 4 <= buf.len() && buf[off..off + 4] == ENDBR64 { - off += 4; - rel32 = rel32.wrapping_sub(4); // JMP is now 4 bytes later, adjust displacement - } - - if off + 5 <= func_end && func_end <= buf.len() { - buf[off] = 0xE9; // JMP rel32 - buf[off + 1..off + 5].copy_from_slice(&rel32.to_le_bytes()); - // NOP-fill remaining bytes of the fork function body after the JMP - // to avoid leaving stale instructions that could be jumped into. - for b in &mut buf[off + 5..func_end] { - *b = 0x90; // NOP - } - } else { - return Err(Error::ParseError(format!( - "fork→vfork patch range {off:#x}..{func_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; @@ -674,101 +631,6 @@ 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, fork_func_end, jmp_rel32))` if -/// both symbols are found, or `None` if this binary doesn't export fork. -/// -/// `fork_func_end` is the file offset of the end of the fork function (based on -/// the symbol's size), clamped to the section boundary. This is used to NOP-fill -/// only the fork function body after the JMP, not the rest of the section. -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 fork_size = 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()); - fork_size = Some(sym.size()); - } - "__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()); - fork_size = Some(sym.size()); - } - "__libc_vfork" | "__vfork" if vfork_vaddr.is_none() => { - vfork_vaddr = Some(sym.address()); - } - _ => {} - } - } - - let fork_vaddr = fork_vaddr?; - let fork_size = fork_size?; - 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_func_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 section_file_end = s.file_offset + s.size; - // Compute the end of the fork function, clamped to the section boundary. - let func_file_end = if fork_size > 0 { - (file_offset + fork_size).min(section_file_end) - } else { - // No size info — only NOP-fill the JMP itself (no extra NOPs). - file_offset + 5 - }; - Some((file_offset, func_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_func_end, rel32)) -} - /// Check if the input binary has the Bun footer marker at the end. fn has_bun_footer_marker(input_binary: &[u8]) -> bool { input_binary.ends_with(BUN_FOOTER_MARKER) From 0446a215985dbf6b477daf5e56f59d59dbc7b915 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 7 Apr 2026 21:46:32 -0700 Subject: [PATCH 07/26] Refactor syscall rewriter API: return skipped addrs, improve error variants, use ICEBP;HLT trap - Change hook_syscalls_in_elf and patch_code_segment to return (Vec, Vec) instead of taking &mut Vec for skipped addresses - Rename UnsupportedBunExecutable to UnsupportedExecutable(String) and add context to UnsupportedObjectFile(String) - Add PatchError variant for address arithmetic failures (previously ParseError) - Replace UD2 (0F 0B) with ICEBP;HLT (F1 F4) for poisoned syscalls to avoid confusion with unreachable!() paths - Make anyhow/clap features use dep: syntax in Cargo.toml - Update all callers in packager, runner, and tests --- litebox_packager/src/lib.rs | 9 +- litebox_runner_linux_userland/src/lib.rs | 5 +- .../src/lib.rs | 10 +- litebox_syscall_rewriter/Cargo.toml | 2 + litebox_syscall_rewriter/src/lib.rs | 124 +++++++++--------- litebox_syscall_rewriter/src/main.rs | 8 +- .../tests/snapshot_tests.rs | 4 +- 7 files changed, 77 insertions(+), 85 deletions(-) diff --git a/litebox_packager/src/lib.rs b/litebox_packager/src/lib.rs index 5917c5764..adfd1543d 100644 --- a/litebox_packager/src/lib.rs +++ b/litebox_packager/src/lib.rs @@ -569,9 +569,8 @@ fn rewrite_elf(data: &[u8], path: &Path, verbose: bool) -> anyhow::Result { + match litebox_syscall_rewriter::hook_syscalls_in_elf(data, None) { + Ok((rewritten, skipped_addrs)) => { if !skipped_addrs.is_empty() { eprintln!( " warning: {} has {} unpatchable syscall instruction(s) at {:?}", @@ -601,7 +600,7 @@ fn rewrite_elf(data: &[u8], path: &Path, verbose: bool) -> anyhow::Result { + Err(litebox_syscall_rewriter::Error::UnsupportedObjectFile(_)) => { if verbose { eprintln!( " warning: {} is not a supported ELF, including as-is", @@ -619,7 +618,7 @@ fn rewrite_elf(data: &[u8], path: &Path, verbose: bool) -> anyhow::Result { + Err(litebox_syscall_rewriter::Error::UnsupportedExecutable(_)) => { anyhow::bail!( "{} is a Bun-packaged executable and cannot be safely packaged without syscall rewriting", path.display() diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index cb8ee9b8f..291a53a1f 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -188,9 +188,8 @@ pub fn run(cli_args: CliArgs) -> Result<()> { .collect(); let file = mmapped_file(&prog)?; let data = if cli_args.rewrite_syscalls { - let mut skipped_addrs = Vec::new(); - let rewritten = - litebox_syscall_rewriter::hook_syscalls_in_elf(file.data, None, &mut skipped_addrs) + let (rewritten, skipped_addrs) = + litebox_syscall_rewriter::hook_syscalls_in_elf(file.data, None) .with_context(|| format!("failed to rewrite {}", prog.display()))?; if !skipped_addrs.is_empty() { eprintln!( diff --git a/litebox_runner_optee_on_linux_userland/src/lib.rs b/litebox_runner_optee_on_linux_userland/src/lib.rs index 298a32958..16c31b117 100644 --- a/litebox_runner_optee_on_linux_userland/src/lib.rs +++ b/litebox_runner_optee_on_linux_userland/src/lib.rs @@ -68,9 +68,8 @@ pub fn run(cli_args: CliArgs) -> Result<()> { let data = std::fs::read(&ldelf).with_context(|| format!("failed to read {}", cli_args.ldelf))?; if cli_args.rewrite_syscalls { - let mut skipped_addrs = Vec::new(); - let rewritten = - litebox_syscall_rewriter::hook_syscalls_in_elf(&data, None, &mut skipped_addrs) + let (rewritten, skipped_addrs) = + litebox_syscall_rewriter::hook_syscalls_in_elf(&data, None) .with_context(|| format!("failed to rewrite {}", cli_args.ldelf))?; if !skipped_addrs.is_empty() { eprintln!( @@ -91,9 +90,8 @@ pub fn run(cli_args: CliArgs) -> Result<()> { let data = std::fs::read(&prog).with_context(|| format!("failed to read {}", cli_args.program))?; if cli_args.rewrite_syscalls { - let mut skipped_addrs = Vec::new(); - let rewritten = - litebox_syscall_rewriter::hook_syscalls_in_elf(&data, None, &mut skipped_addrs) + let (rewritten, skipped_addrs) = + litebox_syscall_rewriter::hook_syscalls_in_elf(&data, None) .with_context(|| format!("failed to rewrite {}", cli_args.program))?; if !skipped_addrs.is_empty() { eprintln!( diff --git a/litebox_syscall_rewriter/Cargo.toml b/litebox_syscall_rewriter/Cargo.toml index 3d88f9027..644eb55a8 100644 --- a/litebox_syscall_rewriter/Cargo.toml +++ b/litebox_syscall_rewriter/Cargo.toml @@ -6,6 +6,8 @@ edition = "2024" [features] default = ["std", "anyhow", "clap"] std = [] +anyhow = ["dep:anyhow"] +clap = ["dep:clap"] [dependencies] iced-x86 = { version = "1.21", default-features = false, features = ["no_std", "decoder", "encoder", "instr_info"] } diff --git a/litebox_syscall_rewriter/src/lib.rs b/litebox_syscall_rewriter/src/lib.rs index 6acfa8725..ebe4b74a4 100644 --- a/litebox_syscall_rewriter/src/lib.rs +++ b/litebox_syscall_rewriter/src/lib.rs @@ -34,10 +34,10 @@ use zerocopy::{FromBytes, Immutable, IntoBytes}; pub enum Error { #[error("failed to parse: {0}")] ParseError(String), - #[error("unsupported executable")] - UnsupportedObjectFile, - #[error("unsupported Bun-packaged executable")] - UnsupportedBunExecutable, + #[error("unsupported object file: {0}")] + UnsupportedObjectFile(String), + #[error("unsupported executable: {0}")] + UnsupportedExecutable(String), #[error("executable is already hooked with trampoline")] AlreadyHooked, #[error("no .text section found")] @@ -50,6 +50,8 @@ pub enum Error { InsufficientBytesBeforeOrAfter(u64), #[error("provided trampoline address is too large for 32-bit executable")] TrampolineAddressTooLarge, + #[error("patch failed: {0}")] + PatchError(String), } type Result = core::result::Result; @@ -109,16 +111,18 @@ struct TextSectionInfo { /// no syscall instructions are patched, the rewriter still appends the header and the initial /// syscall-entry placeholder so the loader/audit path can tell the binary was processed. /// -/// `skipped_addrs` receives the virtual addresses of any `syscall` -/// instructions that could not be patched (replaced with `UD2` so they -/// trap instead of escaping to the host kernel). +/// Returns a tuple of (rewritten binary, skipped syscall addresses). Skipped +/// addresses are syscall instructions that could not be patched because there +/// is not enough space around the instruction (replaced with `icebp; hlt` so +/// they trap instead of escaping to the host kernel). pub fn hook_syscalls_in_elf( input_binary: &[u8], trampoline: Option, - skipped_addrs: &mut Vec, -) -> Result> { - if has_bun_footer_marker(input_binary) { - return Err(Error::UnsupportedBunExecutable); +) -> Result<(Vec, Vec)> { + if input_binary.ends_with(BUN_FOOTER_MARKER) { + return Err(Error::UnsupportedExecutable( + "Bun-packaged executable".into(), + )); } // Relocatable object files (.o) must not be patched: they are linker @@ -129,7 +133,9 @@ pub fn hook_syscalls_in_elf( let e_type = u16::from_le_bytes([input_binary[16], input_binary[17]]); if e_type == object::elf::ET_REL { // ET_REL — relocatable object file - return Err(Error::UnsupportedObjectFile); + return Err(Error::UnsupportedObjectFile( + "relocatable object file".into(), + )); } } @@ -153,7 +159,7 @@ pub fn hook_syscalls_in_elf( let arch = match file { object::File::Elf64(_) => Arch::X86_64, object::File::Elf32(_) => Arch::X86_32, - _ => return Err(Error::UnsupportedObjectFile), + _ => return Err(Error::UnsupportedObjectFile("not an ELF file".into())), }; let dl_sysinfo_int80 = if arch == Arch::X86_32 { @@ -192,6 +198,7 @@ pub fn hook_syscalls_in_elf( trampoline_data.extend_from_slice(&trampoline.to_le_bytes()); } // Patch syscalls in-place in buf + let mut skipped_addrs = Vec::new(); for s in &text_sections { let section_data = section_slice_mut(buf, s)?; match hook_syscalls_in_section( @@ -203,9 +210,9 @@ pub fn hook_syscalls_in_elf( trampoline_base_addr, // entry point is at offset 0 of trampoline dl_sysinfo_int80, &mut trampoline_data, - skipped_addrs, ) { - Ok(()) | Err(Error::NoSyscallInstructionsFound) => {} + Ok(addrs) => skipped_addrs.extend(addrs), + Err(Error::NoSyscallInstructionsFound) => {} Err(e) => return Err(e), } } @@ -246,7 +253,7 @@ pub fn hook_syscalls_in_elf( }; out.extend_from_slice(header.as_bytes()); } - Ok(out) + Ok((out, skipped_addrs)) } /// (private) Get metadata for executable sections @@ -350,10 +357,10 @@ fn hook_syscalls_in_section( syscall_entry_addr: u64, dl_sysinfo_int80: Option, trampoline_data: &mut Vec, - skipped_addrs: &mut Vec, -) -> Result<()> { +) -> Result> { let instructions = decode_section_instructions(arch, section_data, section_base_addr)?; let mut found_any = false; + let mut skipped_addrs = Vec::new(); for (i, inst) in instructions.iter().enumerate() { // Forward search for `syscall` / `int 0x80` / `call DWORD PTR gs:0x10` match arch { @@ -414,9 +421,9 @@ fn hook_syscalls_in_section( ) { Ok(()) => {} Err(Error::InsufficientBytesBeforeOrAfter(_)) => { - // Replace the unpatchable syscall with UD2 so it traps - // instead of escaping to the host kernel. - replace_with_ud2(section_data, section_base_addr, inst); + // Replace the unpatchable syscall with ICEBP;HLT so it + // traps instead of escaping to the host kernel. + replace_with_trap(section_data, section_base_addr, inst); skipped_addrs.push(inst.ip()); } Err(e) => return Err(e), @@ -522,7 +529,7 @@ fn hook_syscalls_in_section( } if found_any { - Ok(()) + Ok(skipped_addrs) } else { Err(Error::NoSyscallInstructionsFound) } @@ -631,23 +638,29 @@ fn fixup_phdr_alignment(buf: &mut [u8]) { } } -/// Check if the input binary has the Bun footer marker at the end. -fn has_bun_footer_marker(input_binary: &[u8]) -> bool { - input_binary.ends_with(BUN_FOOTER_MARKER) -} - -/// Replace an unpatchable syscall instruction with `UD2` (`0F 0B`) so that -/// reaching it triggers SIGILL instead of silently escaping to the host kernel. +/// Replace an unpatchable syscall instruction with `ICEBP; HLT` (`F1 F4`) so +/// that reaching it traps instead of silently escaping to the host kernel. +/// +/// We avoid `UD2` (`0F 0B`) because it commonly appears in binaries to mark +/// `unreachable!()` paths. The `ICEBP; HLT` sequence is a strong, distinctive +/// indicator of "this syscall was intentionally poisoned" — `ICEBP` alone does +/// not trap on Linux in userspace, but `HLT` does (SIGILL in ring 3), and the +/// `F1` prefix makes it easy for a signal handler to distinguish an +/// intentional break from a spurious one. /// /// `syscall` (0F 05) and `int 0x80` (CD 80) are both 2 bytes — same size as -/// `ud2`. For `call DWORD PTR gs:0x10` (7 bytes), the remaining 5 bytes are -/// filled with NOPs. -fn replace_with_ud2(section_data: &mut [u8], section_base_addr: u64, inst: &iced_x86::Instruction) { +/// `ICEBP; HLT`. For `call DWORD PTR gs:0x10` (7 bytes), the remaining 5 +/// bytes are filled with NOPs. +fn replace_with_trap( + section_data: &mut [u8], + section_base_addr: u64, + inst: &iced_x86::Instruction, +) { let offset = usize::try_from(inst.ip() - section_base_addr).unwrap(); let len = inst.len(); - // UD2 = 0F 0B - section_data[offset] = 0x0F; - section_data[offset + 1] = 0x0B; + // ICEBP (F1) + HLT (F4): traps in userspace, easy to identify in a handler. + section_data[offset] = 0xF1; + section_data[offset + 1] = 0xF4; // Fill any remaining bytes (e.g. 7-byte `call gs:0x10`) with NOPs. for b in &mut section_data[offset + 2..offset + len] { *b = 0x90; @@ -656,56 +669,40 @@ fn replace_with_ud2(section_data: &mut [u8], section_base_addr: u64, inst: &iced fn checked_add_u64(base: u64, addend: u64, context: &'static str) -> Result { base.checked_add(addend) - .ok_or_else(|| Error::ParseError(format!("{context} address overflow"))) + .ok_or_else(|| Error::PatchError(format!("{context} address overflow"))) } fn checked_sub_u64(base: u64, subtrahend: u64, context: &'static str) -> Result { base.checked_sub(subtrahend) - .ok_or_else(|| Error::ParseError(format!("{context} address underflow"))) + .ok_or_else(|| Error::PatchError(format!("{context} address underflow"))) } fn rel32_bytes(target: u64, base: u64, context: &'static str) -> Result<[u8; 4]> { let disp = i128::from(target) - i128::from(base); let disp = i32::try_from(disp).map_err(|_| { - Error::ParseError(format!( + Error::PatchError(format!( "{context} displacement out of range: target {target:#x}, base {base:#x}" )) })?; Ok(disp.to_le_bytes()) } -/// Patch a single mapped code segment in-place, returning trampoline stubs. +/// Patch a single mapped code segment in-place, returning trampoline stubs and +/// the addresses of any syscall instructions that could not be patched +/// (replaced with `ICEBP; HLT` so they trap instead of escaping to the host +/// kernel). /// /// This is the runtime counterpart to [`hook_syscalls_in_elf`]. Instead of /// processing a whole ELF file, it operates on a single already-mapped code /// region — the caller is responsible for making the region writable before -/// calling and restoring permissions afterwards. -/// -/// # Arguments -/// -/// * `code` — mutable slice of the mapped code segment. -/// * `code_vaddr` — virtual address of `code[0]` in guest memory. -/// * `trampoline_write_vaddr` — virtual address where the returned stub bytes -/// will be placed by the caller. -/// * `syscall_entry_addr` — address of the 8-byte entry-point value that -/// each stub's indirect jump targets. -/// -/// # Returns -/// -/// The trampoline stub bytes. The caller must copy them to -/// `trampoline_write_vaddr`. Returns an empty `Vec` if no syscall -/// instructions are found in `code`. -/// -/// `skipped_addrs` receives the virtual addresses of any `syscall` -/// instructions that could not be patched (replaced with `UD2` so they -/// trap instead of escaping to the host kernel). +/// calling and restoring permissions afterwards. The caller must copy the +/// returned stubs to `trampoline_write_vaddr`. pub fn patch_code_segment( code: &mut [u8], code_vaddr: u64, trampoline_write_vaddr: u64, syscall_entry_addr: u64, - skipped_addrs: &mut Vec, -) -> Result> { +) -> Result<(Vec, Vec)> { let arch = Arch::X86_64; // runtime patching is x86-64 only // Build control-transfer targets for this segment. @@ -728,10 +725,9 @@ pub fn patch_code_segment( syscall_entry_addr, None, // dl_sysinfo_int80 — not applicable on x86-64 &mut trampoline_data, - skipped_addrs, ) { - Ok(()) => Ok(trampoline_data), - Err(Error::NoSyscallInstructionsFound) => Ok(Vec::new()), + Ok(skipped_addrs) => Ok((trampoline_data, skipped_addrs)), + Err(Error::NoSyscallInstructionsFound) => Ok((Vec::new(), Vec::new())), Err(e) => Err(e), } } diff --git a/litebox_syscall_rewriter/src/main.rs b/litebox_syscall_rewriter/src/main.rs index 70beb251e..209caeaa4 100644 --- a/litebox_syscall_rewriter/src/main.rs +++ b/litebox_syscall_rewriter/src/main.rs @@ -47,19 +47,17 @@ fn main() -> anyhow::Result<()> { let mut input_binary = std::fs::File::open(&cli_args.input_binary)?; let mut input_binary_bytes = vec![]; input_binary.read_to_end(&mut input_binary_bytes)?; - let mut skipped_addrs = Vec::new(); - let output_binary = match litebox_syscall_rewriter::hook_syscalls_in_elf( + let (output_binary, skipped_addrs) = match litebox_syscall_rewriter::hook_syscalls_in_elf( &input_binary_bytes, cli_args.trampoline_addr, - &mut skipped_addrs, ) { - Ok(output_binary) => output_binary, + Ok((output_binary, skipped_addrs)) => (output_binary, skipped_addrs), Err(litebox_syscall_rewriter::Error::NoSyscallInstructionsFound) => { eprintln!( "warning: {} has no syscall instructions, copying as-is", cli_args.input_binary.display() ); - input_binary_bytes.clone() + (input_binary_bytes.clone(), Vec::new()) } Err(err) => return Err(err.into()), }; diff --git a/litebox_syscall_rewriter/tests/snapshot_tests.rs b/litebox_syscall_rewriter/tests/snapshot_tests.rs index d05ab1a09..f8c257978 100644 --- a/litebox_syscall_rewriter/tests/snapshot_tests.rs +++ b/litebox_syscall_rewriter/tests/snapshot_tests.rs @@ -86,8 +86,8 @@ const HELLO_INPUT_64: &[u8] = include_bytes!("hello"); const HELLO_INPUT_32: &[u8] = include_bytes!("hello-32"); fn run_snapshot_test(input: &[u8], snapshot: &str) { - let output = - litebox_syscall_rewriter::hook_syscalls_in_elf(input, None, &mut Vec::new()).unwrap(); + let (output, _skipped_addrs) = + litebox_syscall_rewriter::hook_syscalls_in_elf(input, None).unwrap(); let diff = similar::udiff::unified_diff( similar::Algorithm::Myers, &objdump(input), From ec58ba70e147590025371cd21baf7b28373a0104 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 1 Apr 2026 22:55:57 -0700 Subject: [PATCH 08/26] 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 09/26] 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 10/26] 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 11/26] 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 12/26] 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 13/26] 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 14/26] 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 15/26] 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 16/26] 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 17/26] 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 18/26] 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 19/26] 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 20/26] 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 4aaa78adf064a687560c7d6cb4e3bd7648b7d7fe Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 2 Apr 2026 22:23:19 -0700 Subject: [PATCH 21/26] Cross-platform packager: remove Linux-only gates, add OCI symlink resolution, rewrite-include flag Make litebox_packager compile and work on non-Linux hosts (primarily Windows) by: - Remove #![cfg(target_os = "linux")] crate-level gate and the dual-main pattern; gate only the host-mode code path behind cfg(target_os) - Add file_mode() helper with unix/non-unix variants to replace MetadataExt::mode() calls - Extract run_host_mode() behind #[cfg(target_os = "linux")] - Track OCI layer symlinks in-memory instead of creating OS symlinks (Windows requires special privileges for symlinks); materialize them after all layers are extracted via resolve_symlink_in_rootfs() - Add is_unix_absolute(), strip_unix_root(), normalize_path() helpers for cross-platform path handling - Force linux/amd64 platform when pulling OCI images - Normalize path separators to Unix-style in tar entries - Add --rewrite-include CLI flag for dlopen'd libraries - Change Bun executable detection from warning to hard error - Switch tar headers from GNU to UStar format --- litebox_packager/src/lib.rs | 156 +++++++++++++++-- litebox_packager/src/main.rs | 10 -- litebox_packager/src/oci.rs | 326 +++++++++++++++++++++++++++++++---- 3 files changed, 432 insertions(+), 60 deletions(-) diff --git a/litebox_packager/src/lib.rs b/litebox_packager/src/lib.rs index 0b7afed8c..b06c32d01 100644 --- a/litebox_packager/src/lib.rs +++ b/litebox_packager/src/lib.rs @@ -1,21 +1,37 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -// Restrict this crate to only work on Linux, as it relies on `ldd` for -// dependency discovery and other Linux-specific functionality. -#![cfg(target_os = "linux")] - #[cfg(target_arch = "x86_64")] pub mod oci; use anyhow::{Context, bail}; use clap::Parser; use rayon::prelude::*; -use std::collections::{BTreeMap, BTreeSet}; -use std::os::unix::fs::MetadataExt as _; +#[cfg(target_os = "linux")] +use std::collections::BTreeMap; +use std::collections::BTreeSet; use std::path::{Path, PathBuf}; use tar::{Builder, Header}; +/// Return Unix permission mode bits for a file. +/// +/// On Unix this returns the real mode from metadata. On other platforms it +/// returns 0o755 for files with a read-only attribute cleared, 0o644 otherwise. +#[cfg(unix)] +fn file_mode(metadata: &std::fs::Metadata) -> u32 { + use std::os::unix::fs::MetadataExt as _; + metadata.mode() +} + +#[cfg(not(unix))] +fn file_mode(metadata: &std::fs::Metadata) -> u32 { + if metadata.permissions().readonly() { + 0o644 + } else { + 0o755 + } +} + /// Package Linux ELF programs for execution under LiteBox. /// /// Discovers shared library dependencies, rewrites all ELF files using the @@ -54,6 +70,14 @@ pub struct CliArgs { #[arg(long = "include", value_name = "HOST_PATH:TAR_PATH")] pub include: Vec, + /// Include extra ELF files in the tar **with** syscall rewriting. + /// Use this for shared libraries that are loaded at runtime via `dlopen` + /// (e.g., NSS modules like `libnss_dns.so.2`) and therefore not discovered + /// by the automatic dependency scan. + /// Format: HOST_PATH:TAR_PATH (same as `--include`). + #[arg(long = "rewrite-include", value_name = "HOST_PATH:TAR_PATH")] + pub rewrite_include: Vec, + /// Skip rewriting specific files (by their absolute path on the host). #[arg(long = "no-rewrite", value_name = "PATH")] pub no_rewrite: Vec, @@ -99,7 +123,24 @@ pub fn run(args: CliArgs) -> anyhow::Result<()> { } } - // --- Phase 1: Validate inputs --- + // Host mode (local ELF files + ldd dependency discovery) is Linux-only. + #[cfg(target_os = "linux")] + { + run_host_mode(args) + } + + #[cfg(not(target_os = "linux"))] + { + bail!( + "Host mode (local ELF files) is only supported on Linux. \ + Use --oci-image to pull a container image instead." + ); + } +} + +/// Host mode: package local ELF files with ldd-based dependency discovery. +#[cfg(target_os = "linux")] +fn run_host_mode(args: CliArgs) -> anyhow::Result<()> { let input_files: Vec = args .input_files .iter() @@ -116,6 +157,24 @@ pub fn run(args: CliArgs) -> anyhow::Result<()> { }) .collect::>>()?; + let includes: Vec = args + .include + .iter() + .map(|s| parse_include(s)) + .collect::>>()?; + + let rewrite_includes: Vec = args + .rewrite_include + .iter() + .map(|s| parse_include(s)) + .collect::>>()?; + + for inc in includes.iter().chain(&rewrite_includes) { + if !inc.host_path.exists() { + bail!("included file does not exist: {}", inc.host_path.display()); + } + } + let no_rewrite: BTreeSet = args .no_rewrite .iter() @@ -151,12 +210,13 @@ pub fn run(args: CliArgs) -> anyhow::Result<()> { let par_results: Vec>> = file_map_vec .into_par_iter() - .map(|(real_path, tar_paths)| { + .map(|(real_path, tar_paths): (&PathBuf, &Vec)| { let data = std::fs::read(real_path) .with_context(|| format!("failed to read {}", real_path.display()))?; - let mode = std::fs::metadata(real_path) - .with_context(|| format!("failed to stat {}", real_path.display()))? - .mode(); + let mode = file_mode( + &std::fs::metadata(real_path) + .with_context(|| format!("failed to stat {}", real_path.display()))?, + ); let rewritten = if no_rewrite.contains(real_path) { if verbose { @@ -208,7 +268,7 @@ fn run_oci(image_ref: &str, args: &CliArgs) -> anyhow::Result<()> { // --- Phase 2: Scan rootfs for files --- eprintln!("Scanning rootfs..."); - let file_map = oci::scan_rootfs(&extracted.rootfs_path, args.verbose)?; + let file_map = oci::scan_rootfs(&extracted.rootfs_path, &extracted.symlinks, args.verbose)?; let no_rewrite: BTreeSet = args .no_rewrite @@ -309,11 +369,11 @@ fn run_oci(image_ref: &str, args: &CliArgs) -> anyhow::Result<()> { } // --------------------------------------------------------------------------- -// Shared finalization: includes, rtld audit injection, tar build, size report +// Shared finalization: includes, tar build, size report // --------------------------------------------------------------------------- -/// Append `--include` files, inject the rtld audit library, build the output -/// tar, and print a size summary. +/// Append `--include` and `--rewrite-include` files, build the output tar, +/// and print a size summary. /// /// Both host mode and OCI mode call this after producing their rewritten /// `TarEntry` list. @@ -342,7 +402,7 @@ fn finalize_tar( let data = std::fs::read(&inc.host_path) .with_context(|| format!("failed to read included file {}", inc.host_path.display()))?; let mode = std::fs::metadata(&inc.host_path) - .map(|m| m.mode()) + .map(|m| file_mode(&m)) .unwrap_or(0o644); if args.verbose { eprintln!( @@ -358,6 +418,40 @@ fn finalize_tar( }); } + // Include extra ELF files **with** rewriting (for dlopen'd libraries). + let rewrite_includes: Vec = args + .rewrite_include + .iter() + .map(|s| parse_include(s)) + .collect::>>()?; + + for inc in &rewrite_includes { + if !added_tar_paths.insert(inc.tar_path.clone()) { + bail!( + "duplicate tar path from --rewrite-include: '{}' (already present)", + inc.tar_path + ); + } + let data = std::fs::read(&inc.host_path) + .with_context(|| format!("failed to read {}", inc.host_path.display()))?; + let mode = std::fs::metadata(&inc.host_path) + .map(|m| file_mode(&m)) + .unwrap_or(0o755); + let rewritten = rewrite_elf(&data, &inc.host_path, args.verbose)?; + if args.verbose { + eprintln!( + " rewrite-including {} as {}", + inc.host_path.display(), + inc.tar_path + ); + } + tar_entries.push(TarEntry { + tar_path: inc.tar_path.clone(), + data: rewritten, + mode, + }); + } + // Build tar. eprintln!("Creating {}...", args.output.display()); build_tar(&tar_entries, &args.output)?; @@ -378,20 +472,23 @@ fn finalize_tar( } // --------------------------------------------------------------------------- -// Dependency discovery (via ldd) +// Dependency discovery (via ldd) — Linux only // --------------------------------------------------------------------------- +#[cfg(target_os = "linux")] struct ResolvedDep { ldd_path: PathBuf, real_path: PathBuf, } +#[cfg(target_os = "linux")] struct DepDiscoveryResult { resolved: Vec, missing: Vec, } /// Run `ldd` on the given ELF and return resolved dependencies. +#[cfg(target_os = "linux")] fn find_dependencies(elf_path: &Path, verbose: bool) -> anyhow::Result { let output = std::process::Command::new("ldd") .arg(elf_path) @@ -483,6 +580,7 @@ fn find_dependencies(elf_path: &Path, verbose: bool) -> anyhow::Result anyhow::Result bail!( + "{} is a Bun-packaged executable and cannot be packaged as-is: \ + tar-loaded programs must already contain LiteBox syscall trampolines", + path.display() + ), Err(litebox_syscall_rewriter::Error::NoTextSectionFound) => { if verbose { eprintln!( @@ -627,7 +730,7 @@ fn build_tar(entries: &[TarEntry], output: &Path) -> anyhow::Result<()> { let mut builder = Builder::new(file); for entry in entries { - let mut header = Header::new_gnu(); + let mut header = Header::new_ustar(); header.set_size(entry.data.len() as u64); // Mask to permission bits only (rwxrwxrwx). The full st_mode from // MetadataExt::mode() includes file type bits (e.g., 0o100755) which @@ -645,3 +748,20 @@ fn build_tar(entries: &[TarEntry], output: &Path) -> anyhow::Result<()> { builder.finish().context("failed to finalize tar archive")?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::rewrite_elf; + use std::path::Path; + + #[test] + fn rewrite_elf_rejects_bun_packaged_executables() { + let mut bun_binary = b"\x7fELF".to_vec(); + bun_binary.extend_from_slice(b"\n---- Bun! ----\n"); + + let error = rewrite_elf(&bun_binary, Path::new("/tmp/claude"), false) + .expect_err("bun-packaged executable should not be packaged as-is"); + + assert!(error.to_string().contains("Bun-packaged executable")); + } +} diff --git a/litebox_packager/src/main.rs b/litebox_packager/src/main.rs index 2acb1167d..01987d6e8 100644 --- a/litebox_packager/src/main.rs +++ b/litebox_packager/src/main.rs @@ -1,18 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -// Restrict this crate to only work on Linux, as it relies on `ldd` for -// dependency discovery and other Linux-specific functionality. - -#[cfg(target_os = "linux")] fn main() -> anyhow::Result<()> { use clap::Parser as _; use litebox_packager::CliArgs; litebox_packager::run(CliArgs::parse()) } - -#[cfg(not(target_os = "linux"))] -fn main() { - eprintln!("This program is only supported on Linux"); - std::process::exit(1); -} diff --git a/litebox_packager/src/oci.rs b/litebox_packager/src/oci.rs index adb951833..e14aecfb3 100644 --- a/litebox_packager/src/oci.rs +++ b/litebox_packager/src/oci.rs @@ -7,9 +7,8 @@ //! extracts its filesystem layers into a temporary rootfs directory, then //! walks the rootfs to discover all ELF files for syscall rewriting. -use std::collections::{BTreeMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::io::Read; -use std::os::unix::fs::PermissionsExt as _; use std::path::{Path, PathBuf}; use anyhow::Context; @@ -38,6 +37,9 @@ pub struct ExtractedImage { pub config: ImageConfig, /// Raw OCI image config JSON blob (the full config descriptor data). pub config_json: Vec, + /// Symlinks collected during layer extraction (used for cross-platform + /// resolution since OS symlinks may not work on all platforms). + pub symlinks: Vec, } /// Result of scanning an extracted rootfs for files to package. @@ -96,6 +98,17 @@ pub fn pull_and_extract(image_ref: &str, verbose: bool) -> anyhow::Result anyhow::Result = Vec::new(); for (i, layer) in image_data.layers.iter().enumerate() { if verbose { eprintln!( @@ -143,10 +157,17 @@ pub fn pull_and_extract(image_ref: &str, verbose: bool) -> anyhow::Result anyhow::Result String { /// Extract a single OCI layer (tar or tar+gzip) into the rootfs directory. /// /// Handles OCI whiteout files (`.wh.*` prefixed entries) which indicate -/// files deleted in upper layers. -fn extract_layer(data: &[u8], media_type: &str, rootfs: &Path) -> anyhow::Result<()> { +/// files deleted in upper layers. Symlinks are collected into `symlinks` for +/// cross-platform resolution after all layers are extracted. +fn extract_layer( + data: &[u8], + media_type: &str, + rootfs: &Path, + symlinks: &mut Vec, +) -> anyhow::Result<()> { // Determine if the layer is gzipped let is_gzip = media_type.contains("gzip") || is_gzip_data(data); if is_gzip { let decoder = flate2::read::GzDecoder::new(data); - extract_tar(decoder, rootfs) + extract_tar(decoder, rootfs, symlinks) } else { - extract_tar(data, rootfs) + extract_tar(data, rootfs, symlinks) } } @@ -306,11 +334,26 @@ struct DeferredHardLink { link_source: PathBuf, } +/// Tracked symlink from a container image layer. +pub struct DeferredSymlink { + /// Relative path inside the rootfs (e.g., `usr/lib64/ld-linux-x86-64.so.2`). + rel_path: PathBuf, + /// Symlink target as stored in the tar (Unix-style, may be relative or absolute). + link_target: PathBuf, +} + /// Extract a tar archive into the rootfs, handling OCI whiteout files. /// -/// Hard links whose targets appear later in the archive are collected during -/// the first pass and resolved after all regular entries have been extracted. -fn extract_tar(reader: R, rootfs: &Path) -> anyhow::Result<()> { +/// Symlinks are NOT created as OS symlinks. Instead they are tracked in +/// `symlinks` so the caller can resolve them cross-platform after all layers +/// are extracted. Hard links whose targets appear later in the archive are +/// collected during the first pass and resolved after all regular entries +/// have been extracted. +fn extract_tar( + reader: R, + rootfs: &Path, + symlinks: &mut Vec, +) -> anyhow::Result<()> { let mut archive = tar::Archive::new(reader); archive.set_preserve_permissions(true); archive.set_unpack_xattrs(true); @@ -364,11 +407,12 @@ fn extract_tar(reader: R, rootfs: &Path) -> anyhow::Result<()> { std::fs::create_dir_all(parent)?; } + let entry_type = entry.header().entry_type(); + // Handle hard links: copy the link target instead of creating an OS // hard link. The tar crate's unpack() tries std::fs::hard_link which // can fail if the target hasn't been extracted yet (ordering issue), // and the litebox filesystem doesn't support hard links anyway. - let entry_type = entry.header().entry_type(); if entry_type == tar::EntryType::Link { let link_name = entry .link_name()? @@ -393,7 +437,26 @@ fn extract_tar(reader: R, rootfs: &Path) -> anyhow::Result<()> { continue; } - // Normal file/directory/symlink: use the standard unpack + // Track symlinks in memory instead of creating OS symlinks. + // OS symlinks on Windows require special privileges and don't handle + // Unix-style relative paths reliably, so we resolve them ourselves + // after all layers are extracted. + if entry_type == tar::EntryType::Symlink { + let link_target = entry + .link_name()? + .context("symlink entry has no link name")? + .into_owned(); + // A later layer may override this symlink, so remove any stale + // entry with the same rel_path. + symlinks.retain(|s| s.rel_path != path); + symlinks.push(DeferredSymlink { + rel_path: path.clone(), + link_target, + }); + continue; + } + + // Normal file/directory: use the standard unpack entry .unpack(&target) .with_context(|| format!("failed to unpack entry: {path_str}"))?; @@ -426,20 +489,220 @@ fn extract_tar(reader: R, rootfs: &Path) -> anyhow::Result<()> { Ok(()) } +/// Resolve a symlink target within the rootfs using the symlink map. +/// +/// Handles both absolute targets (e.g., `/lib/x86_64-linux-gnu/ld.so`) and +/// relative targets (e.g., `../lib/x86_64-linux-gnu/ld.so`). Follows symlink +/// chains up to `max_depth` hops. +fn resolve_symlink_in_rootfs( + rel_path: &Path, + rootfs: &Path, + symlink_map: &HashMap, + max_depth: u32, +) -> Option { + if max_depth == 0 { + return None; + } + + // Check if this rel_path is itself a symlink + if let Some(link_target) = symlink_map.get(rel_path) { + // Resolve the target to a new rel_path + let resolved_rel = if is_unix_absolute(link_target) { + strip_unix_root(link_target) + } else { + // Relative target: resolve from parent of the symlink + let parent = rel_path.parent().unwrap_or(Path::new("")); + normalize_path(&parent.join(link_target)) + }; + // Recurse to follow chains + return resolve_symlink_in_rootfs(&resolved_rel, rootfs, symlink_map, max_depth - 1); + } + + // Not a symlink — check if any ancestor is a symlink (e.g., `lib64/foo` where + // `lib64` → `usr/lib64`). + let components: Vec<_> = rel_path.components().collect(); + for i in 1..components.len() { + let prefix: PathBuf = components[..i].iter().collect(); + if let Some(link_target) = symlink_map.get(&prefix) { + let resolved_prefix = if is_unix_absolute(link_target) { + strip_unix_root(link_target) + } else { + let parent = prefix.parent().unwrap_or(Path::new("")); + normalize_path(&parent.join(link_target)) + }; + let suffix: PathBuf = components[i..].iter().collect(); + let new_rel = resolved_prefix.join(suffix); + return resolve_symlink_in_rootfs(&new_rel, rootfs, symlink_map, max_depth - 1); + } + } + + let host_path = rootfs.join(rel_path); + if host_path.exists() { + Some(host_path) + } else { + None + } +} + +/// Check if a path starts with `/` (Unix-style absolute). +/// +/// On Windows, `Path::is_absolute()` requires a drive letter, so Unix-style +/// paths like `/lib/foo` are not detected as absolute. This helper checks +/// the raw string instead. +fn is_unix_absolute(path: &Path) -> bool { + path.as_os_str() + .to_str() + .is_some_and(|s| s.starts_with('/')) + || path.is_absolute() +} + +/// Strip the leading `/` from a Unix-style absolute path to make it +/// rootfs-relative. Returns the path unchanged if it doesn't start with `/`. +fn strip_unix_root(path: &Path) -> PathBuf { + if let Some(stripped) = path.as_os_str().to_str().and_then(|s| s.strip_prefix('/')) { + return PathBuf::from(stripped); + } + path.strip_prefix("/").unwrap_or(path).to_path_buf() +} + +/// Normalize a path by resolving `.` and `..` components without touching the +/// filesystem (no symlink resolution, no existence checks). Strips any root +/// component so the result is always a relative path. +fn normalize_path(path: &Path) -> PathBuf { + let mut result = Vec::new(); + for component in path.components() { + match component { + std::path::Component::ParentDir => { + result.pop(); + } + std::path::Component::CurDir | std::path::Component::RootDir => {} + c => result.push(c), + } + } + result.iter().collect() +} + +/// Materialize all deferred symlinks by copying or creating directories. +/// +/// This is called after all OCI layers have been extracted, so every real file +/// should be on disk. Symlinks are resolved through the in-memory map (handling +/// chains like `lib64` → `usr/lib64` → real dir) and then: +/// - File symlinks: the target file is copied to the symlink location. +/// - Directory symlinks: an empty directory is created (its contents will be +/// expanded by `scan_rootfs`'s dir-symlink logic). +fn materialize_symlinks( + symlinks: &[DeferredSymlink], + rootfs: &Path, + verbose: bool, +) -> anyhow::Result<()> { + // Build a map for O(1) lookup during resolution. + let symlink_map: HashMap = symlinks + .iter() + .map(|s| (s.rel_path.clone(), s.link_target.clone())) + .collect(); + + for sym in symlinks { + let host_path = rootfs.join(&sym.rel_path); + if host_path.exists() { + // A later layer may have replaced the symlink with a real file. + continue; + } + + if let Some(resolved) = resolve_symlink_in_rootfs( + &sym.rel_path, + rootfs, + &symlink_map, + 32, // max chain depth + ) { + if let Some(parent) = host_path.parent() { + std::fs::create_dir_all(parent)?; + } + + if resolved.is_dir() { + // Directory symlink: create directory placeholder. + // scan_rootfs will discover this is a "dir symlink" and expand + // it through the symlink_map. + std::fs::create_dir_all(&host_path)?; + if verbose { + eprintln!( + " [symlink→dir] {} -> {}", + sym.rel_path.display(), + sym.link_target.display() + ); + } + } else if resolved.is_file() { + std::fs::copy(&resolved, &host_path).with_context(|| { + format!( + "failed to materialize symlink {} -> {}", + sym.rel_path.display(), + resolved.display() + ) + })?; + if verbose { + eprintln!( + " [symlink→file] {} -> {}", + sym.rel_path.display(), + sym.link_target.display() + ); + } + } + } else if verbose { + eprintln!( + " [symlink-broken] {} -> {} (unresolvable)", + sym.rel_path.display(), + sym.link_target.display() + ); + } + } + + Ok(()) +} + /// Scan an extracted rootfs directory and build a file map for packaging. /// /// Walks the rootfs directory tree and collects all regular files with their -/// paths and permission bits. Symlinks are resolved within the rootfs context -/// and flattened into regular file copies (the litebox tar RO filesystem does -/// not support symlinks). +/// paths and permission bits. After `materialize_symlinks` has been called, +/// file symlinks are already materialized as regular file copies on disk. /// -/// **Directory symlinks** (e.g., `/lib64` → `/usr/lib64`) are expanded: all -/// files under the target directory are duplicated under the symlink's path -/// prefix so that paths like `/lib64/ld-linux-x86-64.so.2` exist in the tar. -pub fn scan_rootfs(rootfs: &Path, verbose: bool) -> anyhow::Result { +/// `deferred_symlinks` provides the original symlink map from extraction so +/// that **directory symlinks** (e.g., `lib64` → `usr/lib64`) can be expanded: +/// all files under the target directory are duplicated under the symlink's +/// path prefix so that paths like `lib64/ld-linux-x86-64.so.2` exist in the tar. +pub fn scan_rootfs( + rootfs: &Path, + deferred_symlinks: &[DeferredSymlink], + verbose: bool, +) -> anyhow::Result { let mut files = BTreeMap::new(); - // Collect directory symlinks to expand after the initial walk. + + // Build the symlink map for resolution. + let symlink_map: HashMap = deferred_symlinks + .iter() + .map(|s| (s.rel_path.clone(), s.link_target.clone())) + .collect(); + + // Identify directory symlinks and their resolved targets on disk. let mut dir_symlinks: Vec<(PathBuf, PathBuf)> = Vec::new(); + for sym in deferred_symlinks { + let host_path = rootfs.join(&sym.rel_path); + if host_path.is_dir() { + // This dir symlink was materialized as an empty directory. + // Resolve the target to find the real directory to expand from. + if let Some(resolved) = + resolve_symlink_in_rootfs(&sym.rel_path, rootfs, &symlink_map, 32) + .filter(|r| r.is_dir()) + { + if verbose { + eprintln!( + " [dir-symlink] {} -> {}", + sym.rel_path.display(), + sym.link_target.display() + ); + } + dir_symlinks.push((host_path, resolved)); + } + } + } for entry in walkdir::WalkDir::new(rootfs) .follow_links(false) @@ -454,10 +717,12 @@ pub fn scan_rootfs(rootfs: &Path, verbose: bool) -> anyhow::Result anyhow::Result {}", resolved.display()); } @@ -552,6 +812,8 @@ pub fn scan_rootfs(rootfs: &Path, verbose: bool) -> anyhow::Result anyhow::Result Option Date: Thu, 2 Apr 2026 22:28:16 -0700 Subject: [PATCH 22/26] Fix OCI whiteout symlink pruning and degenerate symlink target resolution Two bugs found during review: 1. Opaque whiteouts (.wh..wh..opq) and regular whiteouts (.wh.) removed files from disk but did not prune corresponding entries from the in-memory symlinks vec. This caused materialize_symlinks() to resurrect deleted symlinks that a later layer intended to remove. 2. resolve_symlink_in_rootfs() could return Some(rootfs) when a degenerate symlink target with excess .. segments normalized to an empty path via normalize_path(). rootfs.join("") == rootfs, which exists as a directory, causing the entire rootfs to be treated as a resolution target. Guard against empty rel_path at function entry. --- litebox_packager/src/oci.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/litebox_packager/src/oci.rs b/litebox_packager/src/oci.rs index e14aecfb3..25aa9a9fb 100644 --- a/litebox_packager/src/oci.rs +++ b/litebox_packager/src/oci.rs @@ -383,17 +383,25 @@ fn extract_tar( } } } + // Also prune in-memory symlinks under this directory so + // they are not resurrected by materialize_symlinks. + symlinks.retain(|s| !s.rel_path.starts_with(parent)); } continue; } if let Some(target_name) = file_name.strip_prefix(".wh.") { // Regular whiteout: delete the specific file/directory if let Some(parent) = path.parent() { - let target = rootfs.join(parent).join(target_name); + let whiteout_rel = parent.join(target_name); + let target = rootfs.join(&whiteout_rel); if target.is_dir() { let _ = std::fs::remove_dir_all(&target); + // Prune symlinks under the removed directory. + symlinks.retain(|s| !s.rel_path.starts_with(&whiteout_rel)); } else { let _ = std::fs::remove_file(&target); + // Prune the exact symlink entry if present. + symlinks.retain(|s| s.rel_path != whiteout_rel); } } continue; @@ -504,6 +512,12 @@ fn resolve_symlink_in_rootfs( return None; } + // Empty rel_path would resolve to the rootfs directory itself — treat + // as unresolvable to avoid accidentally matching the entire rootfs. + if rel_path.as_os_str().is_empty() { + return None; + } + // Check if this rel_path is itself a symlink if let Some(link_target) = symlink_map.get(rel_path) { // Resolve the target to a new rel_path From 5fbdeab9864f46a2b9b98fc4cdd255343ad96cbc Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 2 Apr 2026 22:41:21 -0700 Subject: [PATCH 23/26] Remove no-op build.rs from litebox_packager (rtld_audit fully removed) --- litebox_packager/build.rs | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 litebox_packager/build.rs diff --git a/litebox_packager/build.rs b/litebox_packager/build.rs deleted file mode 100644 index f189226e4..000000000 --- a/litebox_packager/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. -} From 9ebf54fcc7361d5aa7e084c3599879313ada02e8 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 3 Apr 2026 21:00:59 +0000 Subject: [PATCH 24/26] Preserve tar header permissions for cross-platform OCI extraction - Store Unix permission modes from tar headers in a HashMap during extraction, so permission bits are accurate on non-Unix hosts (Windows) instead of relying on the file_mode() heuristic which returns wrong answers (0o755 for most files). - Build symlink_map once in pull_and_extract and pass through to materialize_symlinks and scan_rootfs (was duplicated in both). - Add lookup_mode() helper that prefers tar header permissions, falls back to file_mode(), defaults to 0o644. - Add existence check for --rewrite-include in finalize_tar (was missing). - Remove redundant --include/--rewrite-include parsing from run_host_mode. - Replace Bun test with rewrite_elf_skips_non_elf_files test. - Add 22 unit tests for normalize_path, is_unix_absolute, strip_unix_root, resolve_symlink_in_rootfs, and lookup_mode. - Add litebox_packager to build_and_test_windows CI job. --- .github/workflows/ci.yml | 6 +- litebox_packager/src/lib.rs | 44 ++-- litebox_packager/src/oci.rs | 405 +++++++++++++++++++++++++++++++----- 3 files changed, 377 insertions(+), 78 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2255b608..7d328968c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -188,16 +188,20 @@ jobs: tool: nextest@${{ env.NEXTEST_VERSION }} - uses: Swatinem/rust-cache@v2 - run: cargo clippy --locked --verbose --all-targets --all-features -p litebox_runner_linux_on_windows_userland + - run: cargo clippy --locked --verbose --all-targets --all-features -p litebox_packager - run: cargo build --locked --verbose -p litebox_runner_linux_on_windows_userland + - run: cargo build --locked --verbose -p litebox_packager - run: cargo nextest run --locked --profile ci -p litebox_runner_linux_on_windows_userland + - run: cargo nextest run --locked --profile ci -p litebox_packager - run: cargo nextest run --locked --profile ci -p litebox_shim_linux --no-default-features --features platform_windows_userland - run: | cargo test --locked --verbose --doc -p litebox_runner_linux_on_windows_userland + cargo test --locked --verbose --doc -p litebox_packager # We need to run `cargo test --doc` separately because doc tests # aren't included in nextest at the moment. See relevant discussion at # https://github.com/nextest-rs/nextest/issues/16 - name: Build documentation (fail on warnings) - run: cargo doc --locked --verbose --no-deps --all-features --document-private-items -p litebox_runner_linux_on_windows_userland + run: cargo doc --locked --verbose --no-deps --all-features --document-private-items -p litebox_runner_linux_on_windows_userland -p litebox_packager build_and_test_snp: name: Build and Test SNP diff --git a/litebox_packager/src/lib.rs b/litebox_packager/src/lib.rs index b06c32d01..dff9fa0a2 100644 --- a/litebox_packager/src/lib.rs +++ b/litebox_packager/src/lib.rs @@ -157,24 +157,6 @@ fn run_host_mode(args: CliArgs) -> anyhow::Result<()> { }) .collect::>>()?; - let includes: Vec = args - .include - .iter() - .map(|s| parse_include(s)) - .collect::>>()?; - - let rewrite_includes: Vec = args - .rewrite_include - .iter() - .map(|s| parse_include(s)) - .collect::>>()?; - - for inc in includes.iter().chain(&rewrite_includes) { - if !inc.host_path.exists() { - bail!("included file does not exist: {}", inc.host_path.display()); - } - } - let no_rewrite: BTreeSet = args .no_rewrite .iter() @@ -268,7 +250,12 @@ fn run_oci(image_ref: &str, args: &CliArgs) -> anyhow::Result<()> { // --- Phase 2: Scan rootfs for files --- eprintln!("Scanning rootfs..."); - let file_map = oci::scan_rootfs(&extracted.rootfs_path, &extracted.symlinks, args.verbose)?; + let file_map = oci::scan_rootfs( + &extracted.rootfs_path, + &extracted.symlink_map, + &extracted.permissions, + args.verbose, + )?; let no_rewrite: BTreeSet = args .no_rewrite @@ -426,6 +413,12 @@ fn finalize_tar( .collect::>>()?; for inc in &rewrite_includes { + if !inc.host_path.exists() { + bail!( + "rewrite-included file does not exist: {}", + inc.host_path.display() + ); + } if !added_tar_paths.insert(inc.tar_path.clone()) { bail!( "duplicate tar path from --rewrite-include: '{}' (already present)", @@ -755,13 +748,10 @@ mod tests { use std::path::Path; #[test] - fn rewrite_elf_rejects_bun_packaged_executables() { - let mut bun_binary = b"\x7fELF".to_vec(); - bun_binary.extend_from_slice(b"\n---- Bun! ----\n"); - - let error = rewrite_elf(&bun_binary, Path::new("/tmp/claude"), false) - .expect_err("bun-packaged executable should not be packaged as-is"); - - assert!(error.to_string().contains("Bun-packaged executable")); + fn rewrite_elf_skips_non_elf_files() { + // Non-ELF data should be returned unmodified. + let data = b"#!/bin/sh\necho hello\n"; + let result = rewrite_elf(data, Path::new("/tmp/script.sh"), false).unwrap(); + assert_eq!(result, data); } } diff --git a/litebox_packager/src/oci.rs b/litebox_packager/src/oci.rs index 25aa9a9fb..28d65ff4e 100644 --- a/litebox_packager/src/oci.rs +++ b/litebox_packager/src/oci.rs @@ -37,9 +37,13 @@ pub struct ExtractedImage { pub config: ImageConfig, /// Raw OCI image config JSON blob (the full config descriptor data). pub config_json: Vec, - /// Symlinks collected during layer extraction (used for cross-platform - /// resolution since OS symlinks may not work on all platforms). - pub symlinks: Vec, + /// Symlink map from layer extraction: maps relative paths inside the + /// rootfs to their (Unix-style) link targets for cross-platform resolution. + pub symlink_map: HashMap, + /// Unix permission modes captured from tar headers during extraction. + /// Keyed by relative path inside the rootfs. Used instead of querying + /// filesystem metadata, which loses Unix mode bits on non-Unix hosts. + pub permissions: HashMap, } /// Result of scanning an extracted rootfs for files to package. @@ -148,6 +152,7 @@ pub fn pull_and_extract(image_ref: &str, verbose: bool) -> anyhow::Result = Vec::new(); + let mut permissions: HashMap = HashMap::new(); for (i, layer) in image_data.layers.iter().enumerate() { if verbose { eprintln!( @@ -157,16 +162,28 @@ pub fn pull_and_extract(image_ref: &str, verbose: bool) -> anyhow::Result = symlinks + .iter() + .map(|s| (s.rel_path.clone(), s.link_target.clone())) + .collect(); + // Materialize symlinks cross-platform: resolve chains through the in-memory // map and copy target files (or create directories) instead of OS symlinks. if verbose { eprintln!(" Resolving {} symlinks...", symlinks.len()); } - materialize_symlinks(&symlinks, &rootfs_path, verbose)?; + materialize_symlinks(&symlink_map, &rootfs_path, &mut permissions, verbose)?; if verbose { eprintln!(" Rootfs extracted to {}", rootfs_path.display()); @@ -209,7 +226,8 @@ pub fn pull_and_extract(image_ref: &str, verbose: bool) -> anyhow::Result String { /// /// Handles OCI whiteout files (`.wh.*` prefixed entries) which indicate /// files deleted in upper layers. Symlinks are collected into `symlinks` for -/// cross-platform resolution after all layers are extracted. +/// cross-platform resolution after all layers are extracted. Permission modes +/// from tar headers are recorded in `permissions` for cross-platform use. fn extract_layer( data: &[u8], media_type: &str, rootfs: &Path, symlinks: &mut Vec, + permissions: &mut HashMap, ) -> anyhow::Result<()> { // Determine if the layer is gzipped let is_gzip = media_type.contains("gzip") || is_gzip_data(data); if is_gzip { let decoder = flate2::read::GzDecoder::new(data); - extract_tar(decoder, rootfs, symlinks) + extract_tar(decoder, rootfs, symlinks, permissions) } else { - extract_tar(data, rootfs, symlinks) + extract_tar(data, rootfs, symlinks, permissions) } } @@ -332,10 +352,12 @@ struct DeferredHardLink { target: PathBuf, /// Source path inside the rootfs (the file the hard link points to). link_source: PathBuf, + /// Original link name from the tar header (used for permission lookup). + link_name: PathBuf, } /// Tracked symlink from a container image layer. -pub struct DeferredSymlink { +struct DeferredSymlink { /// Relative path inside the rootfs (e.g., `usr/lib64/ld-linux-x86-64.so.2`). rel_path: PathBuf, /// Symlink target as stored in the tar (Unix-style, may be relative or absolute). @@ -348,11 +370,13 @@ pub struct DeferredSymlink { /// `symlinks` so the caller can resolve them cross-platform after all layers /// are extracted. Hard links whose targets appear later in the archive are /// collected during the first pass and resolved after all regular entries -/// have been extracted. +/// have been extracted. Permission modes from tar headers are recorded in +/// `permissions` keyed by relative path. fn extract_tar( reader: R, rootfs: &Path, symlinks: &mut Vec, + permissions: &mut HashMap, ) -> anyhow::Result<()> { let mut archive = tar::Archive::new(reader); archive.set_preserve_permissions(true); @@ -386,6 +410,8 @@ fn extract_tar( // Also prune in-memory symlinks under this directory so // they are not resurrected by materialize_symlinks. symlinks.retain(|s| !s.rel_path.starts_with(parent)); + // Prune permissions for files under the cleared directory. + permissions.retain(|p, _| !p.starts_with(parent)); } continue; } @@ -398,10 +424,14 @@ fn extract_tar( let _ = std::fs::remove_dir_all(&target); // Prune symlinks under the removed directory. symlinks.retain(|s| !s.rel_path.starts_with(&whiteout_rel)); + // Prune permissions under the removed directory. + permissions.retain(|p, _| !p.starts_with(&whiteout_rel)); } else { let _ = std::fs::remove_file(&target); // Prune the exact symlink entry if present. symlinks.retain(|s| s.rel_path != whiteout_rel); + // Prune the exact permissions entry. + permissions.remove(&whiteout_rel); } } continue; @@ -435,11 +465,17 @@ fn extract_tar( target.display() ) })?; + // Copy permission mode from the link source. + let link_rel = normalize_path(&link_name); + if let Some(&mode) = permissions.get(&link_rel) { + permissions.insert(path.clone(), mode); + } } else { // Target hasn't been extracted yet — defer to second pass. deferred_links.push(DeferredHardLink { target, link_source, + link_name: link_name.clone(), }); } continue; @@ -468,6 +504,11 @@ fn extract_tar( entry .unpack(&target) .with_context(|| format!("failed to unpack entry: {path_str}"))?; + + // Record the permission mode from the tar header for cross-platform use. + if let Ok(mode) = entry.header().mode() { + permissions.insert(path.clone(), mode); + } } // Second pass: resolve deferred hard links now that all entries are extracted. @@ -483,6 +524,12 @@ fn extract_tar( link.target.display() ) })?; + // Copy permission mode from the link source. + let link_rel = normalize_path(&link.link_name); + if let Some(&mode) = permissions.get(&link_rel) { + let target_rel = link.target.strip_prefix(rootfs).unwrap_or(&link.target); + permissions.insert(target_rel.to_path_buf(), mode); + } } else { // Target still doesn't exist after the full layer extraction — // this is unusual but not fatal; warn and skip. @@ -602,30 +649,26 @@ fn normalize_path(path: &Path) -> PathBuf { /// should be on disk. Symlinks are resolved through the in-memory map (handling /// chains like `lib64` → `usr/lib64` → real dir) and then: /// - File symlinks: the target file is copied to the symlink location. +/// The resolved target's permission mode is also recorded for the symlink path. /// - Directory symlinks: an empty directory is created (its contents will be /// expanded by `scan_rootfs`'s dir-symlink logic). fn materialize_symlinks( - symlinks: &[DeferredSymlink], + symlink_map: &HashMap, rootfs: &Path, + permissions: &mut HashMap, verbose: bool, ) -> anyhow::Result<()> { - // Build a map for O(1) lookup during resolution. - let symlink_map: HashMap = symlinks - .iter() - .map(|s| (s.rel_path.clone(), s.link_target.clone())) - .collect(); - - for sym in symlinks { - let host_path = rootfs.join(&sym.rel_path); + for (rel_path, link_target) in symlink_map { + let host_path = rootfs.join(rel_path); if host_path.exists() { // A later layer may have replaced the symlink with a real file. continue; } if let Some(resolved) = resolve_symlink_in_rootfs( - &sym.rel_path, + rel_path, rootfs, - &symlink_map, + symlink_map, 32, // max chain depth ) { if let Some(parent) = host_path.parent() { @@ -640,31 +683,39 @@ fn materialize_symlinks( if verbose { eprintln!( " [symlink→dir] {} -> {}", - sym.rel_path.display(), - sym.link_target.display() + rel_path.display(), + link_target.display() ); } } else if resolved.is_file() { std::fs::copy(&resolved, &host_path).with_context(|| { format!( "failed to materialize symlink {} -> {}", - sym.rel_path.display(), + rel_path.display(), resolved.display() ) })?; + // Record the resolved target's permission mode for this symlink path. + let resolved_rel = resolved + .strip_prefix(rootfs) + .unwrap_or(&resolved) + .to_path_buf(); + if let Some(&mode) = permissions.get(&resolved_rel) { + permissions.insert(rel_path.clone(), mode); + } if verbose { eprintln!( " [symlink→file] {} -> {}", - sym.rel_path.display(), - sym.link_target.display() + rel_path.display(), + link_target.display() ); } } } else if verbose { eprintln!( " [symlink-broken] {} -> {} (unresolvable)", - sym.rel_path.display(), - sym.link_target.display() + rel_path.display(), + link_target.display() ); } } @@ -672,45 +723,59 @@ fn materialize_symlinks( Ok(()) } +/// Look up the Unix permission mode for a file. +/// +/// Prefers the tar-header–derived `permissions` map (keyed by rootfs-relative +/// path) which is accurate on all platforms. Falls back to `file_mode()` on +/// the host path (accurate on Unix, heuristic on Windows), and finally +/// defaults to 0o644 if neither source is available. +fn lookup_mode(rel_path: &Path, host_path: &Path, permissions: &HashMap) -> u32 { + if let Some(&mode) = permissions.get(rel_path) { + return mode & 0o7777; + } + if let Ok(metadata) = std::fs::metadata(host_path) { + return super::file_mode(&metadata) & 0o7777; + } + 0o644 +} + /// Scan an extracted rootfs directory and build a file map for packaging. /// /// Walks the rootfs directory tree and collects all regular files with their /// paths and permission bits. After `materialize_symlinks` has been called, /// file symlinks are already materialized as regular file copies on disk. /// -/// `deferred_symlinks` provides the original symlink map from extraction so +/// `symlink_map` provides the original symlink mapping from extraction so /// that **directory symlinks** (e.g., `lib64` → `usr/lib64`) can be expanded: /// all files under the target directory are duplicated under the symlink's /// path prefix so that paths like `lib64/ld-linux-x86-64.so.2` exist in the tar. +/// +/// `permissions` provides Unix permission modes captured from tar headers +/// during extraction, so permission bits are accurate on non-Unix hosts. +#[allow(clippy::implicit_hasher)] pub fn scan_rootfs( rootfs: &Path, - deferred_symlinks: &[DeferredSymlink], + symlink_map: &HashMap, + permissions: &HashMap, verbose: bool, ) -> anyhow::Result { let mut files = BTreeMap::new(); - // Build the symlink map for resolution. - let symlink_map: HashMap = deferred_symlinks - .iter() - .map(|s| (s.rel_path.clone(), s.link_target.clone())) - .collect(); - // Identify directory symlinks and their resolved targets on disk. let mut dir_symlinks: Vec<(PathBuf, PathBuf)> = Vec::new(); - for sym in deferred_symlinks { - let host_path = rootfs.join(&sym.rel_path); + for (rel_path, link_target) in symlink_map { + let host_path = rootfs.join(rel_path); if host_path.is_dir() { // This dir symlink was materialized as an empty directory. // Resolve the target to find the real directory to expand from. if let Some(resolved) = - resolve_symlink_in_rootfs(&sym.rel_path, rootfs, &symlink_map, 32) - .filter(|r| r.is_dir()) + resolve_symlink_in_rootfs(rel_path, rootfs, symlink_map, 32).filter(|r| r.is_dir()) { if verbose { eprintln!( " [dir-symlink] {} -> {}", - sym.rel_path.display(), - sym.link_target.display() + rel_path.display(), + link_target.display() ); } dir_symlinks.push((host_path, resolved)); @@ -735,8 +800,7 @@ pub fn scan_rootfs( let tar_path = tar_path.replace('\\', "/"); if entry.file_type().is_file() { - let metadata = entry.metadata()?; - let mode = super::file_mode(&metadata) & 0o7777; + let mode = lookup_mode(rel_path, entry.path(), permissions); let is_executable = mode & 0o111 != 0; if verbose && is_executable { @@ -756,8 +820,8 @@ pub fn scan_rootfs( // On platforms that still have OS symlinks (Linux), resolve them. if let Some(resolved) = resolve_in_rootfs(entry.path(), rootfs, 16) { if resolved.is_file() { - let metadata = std::fs::metadata(&resolved)?; - let mode = super::file_mode(&metadata) & 0o7777; + let resolved_rel = resolved.strip_prefix(rootfs).unwrap_or(&resolved); + let mode = lookup_mode(resolved_rel, &resolved, permissions); let is_executable = mode & 0o111 != 0; files.insert( @@ -838,8 +902,8 @@ pub fn scan_rootfs( continue; } - let metadata = std::fs::metadata(&read_path)?; - let mode = super::file_mode(&metadata) & 0o7777; + let read_rel = read_path.strip_prefix(rootfs).unwrap_or(&read_path); + let mode = lookup_mode(read_rel, &read_path, permissions); let is_executable = mode & 0o111 != 0; if verbose { @@ -918,4 +982,245 @@ mod tests { let result = resolve_in_rootfs(Path::new("/tmp"), Path::new("/tmp"), 0); assert!(result.is_none()); } + + // --- normalize_path --- + + #[test] + fn normalize_path_resolves_parent_components() { + let p = normalize_path(Path::new("usr/lib/../bin/sh")); + assert_eq!(p, PathBuf::from("usr/bin/sh")); + } + + #[test] + fn normalize_path_strips_current_dir() { + let p = normalize_path(Path::new("./usr/./bin/sh")); + assert_eq!(p, PathBuf::from("usr/bin/sh")); + } + + #[test] + fn normalize_path_strips_root() { + let p = normalize_path(Path::new("/usr/bin/sh")); + assert_eq!(p, PathBuf::from("usr/bin/sh")); + } + + #[test] + fn normalize_path_double_parent_at_start_clamps() { + // Going above root should just empty the stack. + let p = normalize_path(Path::new("../../foo")); + assert_eq!(p, PathBuf::from("foo")); + } + + #[test] + fn normalize_path_empty_input() { + let p = normalize_path(Path::new("")); + assert_eq!(p, PathBuf::from("")); + } + + // --- is_unix_absolute --- + + #[test] + fn is_unix_absolute_detects_slash_prefix() { + assert!(is_unix_absolute(Path::new("/usr/bin"))); + assert!(is_unix_absolute(Path::new("/"))); + } + + #[test] + fn is_unix_absolute_rejects_relative() { + assert!(!is_unix_absolute(Path::new("usr/bin"))); + assert!(!is_unix_absolute(Path::new("../lib"))); + assert!(!is_unix_absolute(Path::new(""))); + } + + // --- strip_unix_root --- + + #[test] + fn strip_unix_root_removes_leading_slash() { + assert_eq!( + strip_unix_root(Path::new("/usr/bin")), + PathBuf::from("usr/bin") + ); + } + + #[test] + fn strip_unix_root_noop_for_relative() { + assert_eq!( + strip_unix_root(Path::new("usr/bin")), + PathBuf::from("usr/bin") + ); + } + + #[test] + fn strip_unix_root_on_bare_slash() { + // "/" should become empty after stripping. + let p = strip_unix_root(Path::new("/")); + assert!(p.as_os_str().is_empty() || p == Path::new("")); + } + + // --- resolve_symlink_in_rootfs --- + + #[test] + fn resolve_symlink_direct_hit() { + // lib64 -> usr/lib64, and rootfs/usr/lib64/libc.so exists on disk. + let tmp = tempfile::tempdir().unwrap(); + let rootfs = tmp.path(); + std::fs::create_dir_all(rootfs.join("usr/lib64")).unwrap(); + std::fs::write(rootfs.join("usr/lib64/libc.so"), b"fake").unwrap(); + + let mut symlink_map = HashMap::new(); + symlink_map.insert(PathBuf::from("lib64"), PathBuf::from("usr/lib64")); + + // Resolving "lib64" itself should follow to rootfs/usr/lib64 (dir). + let resolved = resolve_symlink_in_rootfs(Path::new("lib64"), rootfs, &symlink_map, 32); + assert!(resolved.is_some()); + assert_eq!(resolved.unwrap(), rootfs.join("usr/lib64")); + } + + #[test] + fn resolve_symlink_chain() { + // a -> b, b -> c, rootfs/c exists. + let tmp = tempfile::tempdir().unwrap(); + let rootfs = tmp.path(); + std::fs::write(rootfs.join("c"), b"data").unwrap(); + + let mut symlink_map = HashMap::new(); + symlink_map.insert(PathBuf::from("a"), PathBuf::from("b")); + symlink_map.insert(PathBuf::from("b"), PathBuf::from("c")); + + let resolved = resolve_symlink_in_rootfs(Path::new("a"), rootfs, &symlink_map, 32); + assert_eq!(resolved, Some(rootfs.join("c"))); + } + + #[test] + fn resolve_symlink_max_depth_prevents_infinite_loop() { + // a -> b, b -> a (cycle). + let mut symlink_map = HashMap::new(); + symlink_map.insert(PathBuf::from("a"), PathBuf::from("b")); + symlink_map.insert(PathBuf::from("b"), PathBuf::from("a")); + + let tmp = tempfile::tempdir().unwrap(); + let resolved = resolve_symlink_in_rootfs(Path::new("a"), tmp.path(), &symlink_map, 32); + assert!(resolved.is_none()); + } + + #[test] + fn resolve_symlink_absolute_target() { + // link -> /usr/bin/sh, rootfs/usr/bin/sh exists. + let tmp = tempfile::tempdir().unwrap(); + let rootfs = tmp.path(); + std::fs::create_dir_all(rootfs.join("usr/bin")).unwrap(); + std::fs::write(rootfs.join("usr/bin/sh"), b"elf").unwrap(); + + let mut symlink_map = HashMap::new(); + symlink_map.insert(PathBuf::from("bin/sh"), PathBuf::from("/usr/bin/sh")); + + let resolved = resolve_symlink_in_rootfs(Path::new("bin/sh"), rootfs, &symlink_map, 32); + assert_eq!(resolved, Some(rootfs.join("usr/bin/sh"))); + } + + #[test] + fn resolve_symlink_relative_target() { + // usr/lib64/libfoo.so -> ../lib/libfoo.so, rootfs/usr/lib/libfoo.so exists. + let tmp = tempfile::tempdir().unwrap(); + let rootfs = tmp.path(); + std::fs::create_dir_all(rootfs.join("usr/lib")).unwrap(); + std::fs::write(rootfs.join("usr/lib/libfoo.so"), b"elf").unwrap(); + + let mut symlink_map = HashMap::new(); + symlink_map.insert( + PathBuf::from("usr/lib64/libfoo.so"), + PathBuf::from("../lib/libfoo.so"), + ); + + let resolved = + resolve_symlink_in_rootfs(Path::new("usr/lib64/libfoo.so"), rootfs, &symlink_map, 32); + assert_eq!(resolved, Some(rootfs.join("usr/lib/libfoo.so"))); + } + + #[test] + fn resolve_symlink_ancestor_is_symlink() { + // lib64 -> usr/lib64, resolve "lib64/foo.so" where rootfs/usr/lib64/foo.so exists. + let tmp = tempfile::tempdir().unwrap(); + let rootfs = tmp.path(); + std::fs::create_dir_all(rootfs.join("usr/lib64")).unwrap(); + std::fs::write(rootfs.join("usr/lib64/foo.so"), b"elf").unwrap(); + + let mut symlink_map = HashMap::new(); + symlink_map.insert(PathBuf::from("lib64"), PathBuf::from("usr/lib64")); + + let resolved = + resolve_symlink_in_rootfs(Path::new("lib64/foo.so"), rootfs, &symlink_map, 32); + assert_eq!(resolved, Some(rootfs.join("usr/lib64/foo.so"))); + } + + #[test] + fn resolve_symlink_empty_path_returns_none() { + let tmp = tempfile::tempdir().unwrap(); + let symlink_map = HashMap::new(); + let resolved = resolve_symlink_in_rootfs(Path::new(""), tmp.path(), &symlink_map, 32); + assert!(resolved.is_none()); + } + + #[test] + fn resolve_symlink_not_a_symlink_returns_host_path() { + // Regular file, not in symlink_map — should return host_path directly. + let tmp = tempfile::tempdir().unwrap(); + let rootfs = tmp.path(); + std::fs::write(rootfs.join("hello.txt"), b"hi").unwrap(); + + let symlink_map = HashMap::new(); + let resolved = resolve_symlink_in_rootfs(Path::new("hello.txt"), rootfs, &symlink_map, 32); + assert_eq!(resolved, Some(rootfs.join("hello.txt"))); + } + + #[test] + fn resolve_symlink_nonexistent_returns_none() { + let tmp = tempfile::tempdir().unwrap(); + let symlink_map = HashMap::new(); + let resolved = + resolve_symlink_in_rootfs(Path::new("does/not/exist"), tmp.path(), &symlink_map, 32); + assert!(resolved.is_none()); + } + + // --- lookup_mode --- + + #[test] + fn lookup_mode_prefers_permissions_map() { + let tmp = tempfile::tempdir().unwrap(); + let rootfs = tmp.path(); + std::fs::write(rootfs.join("file.sh"), b"#!/bin/sh").unwrap(); + + let mut permissions = HashMap::new(); + permissions.insert(PathBuf::from("file.sh"), 0o100755u32); + + // The permissions map value (masked) should win over filesystem metadata. + let mode = lookup_mode(Path::new("file.sh"), &rootfs.join("file.sh"), &permissions); + assert_eq!(mode, 0o755); + } + + #[test] + fn lookup_mode_falls_back_to_filesystem() { + let tmp = tempfile::tempdir().unwrap(); + let rootfs = tmp.path(); + std::fs::write(rootfs.join("file.txt"), b"data").unwrap(); + + let permissions = HashMap::new(); // empty + let mode = lookup_mode( + Path::new("file.txt"), + &rootfs.join("file.txt"), + &permissions, + ); + // On Unix the file should have some mode; just check it's non-zero. + assert!(mode > 0); + } + + #[test] + fn lookup_mode_defaults_to_644_when_nothing_available() { + let permissions = HashMap::new(); + let mode = lookup_mode( + Path::new("nonexistent"), + Path::new("/no/such/file"), + &permissions, + ); + assert_eq!(mode, 0o644); + } } From ca194cb23d7db3c0439a9d3191f34dc251d0f736 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Sat, 4 Apr 2026 20:35:04 -0700 Subject: [PATCH 25/26] Fix path traversal in OCI extraction, root-level opaque whiteout bug, and Windows colon parsing - Normalize all tar entry paths with normalize_path() to prevent path traversal via absolute paths, ../ escape, and ./prefix inconsistency - Normalize hard link source paths for the same protection - Fix root-level opaque whiteout where Path::starts_with("") matches all paths, wiping all in-memory symlinks and permissions - Handle Windows drive letter colons (C:\path) in parse_include so --include/--rewrite-include work correctly on Windows --- litebox_packager/src/lib.rs | 13 ++++++++++++- litebox_packager/src/oci.rs | 27 +++++++++++++++++++-------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/litebox_packager/src/lib.rs b/litebox_packager/src/lib.rs index dff9fa0a2..41949ed6a 100644 --- a/litebox_packager/src/lib.rs +++ b/litebox_packager/src/lib.rs @@ -94,7 +94,18 @@ struct IncludeEntry { } fn parse_include(spec: &str) -> anyhow::Result { - let Some(colon_idx) = spec.find(':') else { + // On Windows, skip past a drive letter prefix (e.g., `C:`) when searching + // for the HOST_PATH:TAR_PATH separator. + let search_start = if cfg!(windows) + && spec.len() >= 2 + && spec.as_bytes()[0].is_ascii_alphabetic() + && spec.as_bytes()[1] == b':' + { + 2 + } else { + 0 + }; + let Some(colon_idx) = spec[search_start..].find(':').map(|i| i + search_start) else { bail!("invalid --include format: expected HOST_PATH:TAR_PATH, got: {spec}"); }; let host_path = PathBuf::from(&spec[..colon_idx]); diff --git a/litebox_packager/src/oci.rs b/litebox_packager/src/oci.rs index 28d65ff4e..64a78b9ef 100644 --- a/litebox_packager/src/oci.rs +++ b/litebox_packager/src/oci.rs @@ -386,7 +386,9 @@ fn extract_tar( for entry_result in archive.entries()? { let mut entry = entry_result.context("failed to read tar entry")?; - let path = entry.path()?.into_owned(); + // Normalize the path to prevent path traversal (../ and absolute paths) + // and to strip inconsistent ./ prefixes that tar entries may carry. + let path = normalize_path(&entry.path()?); let path_str = path.to_string_lossy(); // Handle OCI whiteout files @@ -409,9 +411,17 @@ fn extract_tar( } // Also prune in-memory symlinks under this directory so // they are not resurrected by materialize_symlinks. - symlinks.retain(|s| !s.rel_path.starts_with(parent)); - // Prune permissions for files under the cleared directory. - permissions.retain(|p, _| !p.starts_with(parent)); + // Guard: Path::starts_with("") matches everything, so skip + // pruning when parent is empty (root-level opaque whiteout + // already cleared the filesystem above). + if parent.as_os_str().is_empty() { + symlinks.clear(); + permissions.clear(); + } else { + symlinks.retain(|s| !s.rel_path.starts_with(parent)); + // Prune permissions for files under the cleared directory. + permissions.retain(|p, _| !p.starts_with(parent)); + } } continue; } @@ -452,10 +462,11 @@ fn extract_tar( // can fail if the target hasn't been extracted yet (ordering issue), // and the litebox filesystem doesn't support hard links anyway. if entry_type == tar::EntryType::Link { - let link_name = entry - .link_name()? - .context("hard link entry has no link name")? - .into_owned(); + let link_name = normalize_path( + &entry + .link_name()? + .context("hard link entry has no link name")?, + ); let link_source = rootfs.join(&link_name); if link_source.exists() { std::fs::copy(&link_source, &target).with_context(|| { From 74b27eba69d030b0966927ab6e340ebe1ade67cf Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 8 Apr 2026 20:00:48 +0000 Subject: [PATCH 26/26] Address PR #741 review comments - Merge --rewrite-include into --include (auto-detect ELF via magic bytes) - Make --include host-mode-only (conflicts_with oci_image) - Remove file_mode() helper; inline MetadataExt::mode() in Linux-only paths - Simplify lookup_mode() to use OCI permissions map only (no host FS fallback) - Add Component::Prefix(_) to normalize_path for Windows path completeness - Remove redundant path.is_absolute() fallback from is_unix_absolute - Consolidate and prune tests in oci.rs - Fix stale UnsupportedBunExecutable match arm from rebase --- litebox_packager/src/lib.rs | 219 ++++++++------------------ litebox_packager/src/oci.rs | 296 +++++++----------------------------- 2 files changed, 114 insertions(+), 401 deletions(-) diff --git a/litebox_packager/src/lib.rs b/litebox_packager/src/lib.rs index 41949ed6a..3ef09bbf6 100644 --- a/litebox_packager/src/lib.rs +++ b/litebox_packager/src/lib.rs @@ -13,25 +13,6 @@ use std::collections::BTreeSet; use std::path::{Path, PathBuf}; use tar::{Builder, Header}; -/// Return Unix permission mode bits for a file. -/// -/// On Unix this returns the real mode from metadata. On other platforms it -/// returns 0o755 for files with a read-only attribute cleared, 0o644 otherwise. -#[cfg(unix)] -fn file_mode(metadata: &std::fs::Metadata) -> u32 { - use std::os::unix::fs::MetadataExt as _; - metadata.mode() -} - -#[cfg(not(unix))] -fn file_mode(metadata: &std::fs::Metadata) -> u32 { - if metadata.permissions().readonly() { - 0o644 - } else { - 0o755 - } -} - /// Package Linux ELF programs for execution under LiteBox. /// /// Discovers shared library dependencies, rewrites all ELF files using the @@ -64,20 +45,18 @@ pub struct CliArgs { #[arg(short = 'o', long = "output", default_value = "litebox_packager.tar")] pub output: PathBuf, - /// Include extra files in the tar. + /// Include extra files in the tar (host mode only). + /// ELF files are automatically run through the syscall rewriter; non-ELF + /// files are included as-is. /// Format: HOST_PATH:TAR_PATH (split on the first colon, so the tar path /// may contain colons but the host path must not). - #[arg(long = "include", value_name = "HOST_PATH:TAR_PATH")] + #[arg( + long = "include", + value_name = "HOST_PATH:TAR_PATH", + conflicts_with = "oci_image" + )] pub include: Vec, - /// Include extra ELF files in the tar **with** syscall rewriting. - /// Use this for shared libraries that are loaded at runtime via `dlopen` - /// (e.g., NSS modules like `libnss_dns.so.2`) and therefore not discovered - /// by the automatic dependency scan. - /// Format: HOST_PATH:TAR_PATH (same as `--include`). - #[arg(long = "rewrite-include", value_name = "HOST_PATH:TAR_PATH")] - pub rewrite_include: Vec, - /// Skip rewriting specific files (by their absolute path on the host). #[arg(long = "no-rewrite", value_name = "PATH")] pub no_rewrite: Vec, @@ -88,24 +67,15 @@ pub struct CliArgs { } /// Parsed `--include` entry. +#[cfg(target_os = "linux")] struct IncludeEntry { host_path: PathBuf, tar_path: String, } +#[cfg(target_os = "linux")] fn parse_include(spec: &str) -> anyhow::Result { - // On Windows, skip past a drive letter prefix (e.g., `C:`) when searching - // for the HOST_PATH:TAR_PATH separator. - let search_start = if cfg!(windows) - && spec.len() >= 2 - && spec.as_bytes()[0].is_ascii_alphabetic() - && spec.as_bytes()[1] == b':' - { - 2 - } else { - 0 - }; - let Some(colon_idx) = spec[search_start..].find(':').map(|i| i + search_start) else { + let Some(colon_idx) = spec.find(':') else { bail!("invalid --include format: expected HOST_PATH:TAR_PATH, got: {spec}"); }; let host_path = PathBuf::from(&spec[..colon_idx]); @@ -206,10 +176,12 @@ fn run_host_mode(args: CliArgs) -> anyhow::Result<()> { .map(|(real_path, tar_paths): (&PathBuf, &Vec)| { let data = std::fs::read(real_path) .with_context(|| format!("failed to read {}", real_path.display()))?; - let mode = file_mode( - &std::fs::metadata(real_path) - .with_context(|| format!("failed to stat {}", real_path.display()))?, - ); + let mode = { + use std::os::unix::fs::MetadataExt as _; + std::fs::metadata(real_path) + .with_context(|| format!("failed to stat {}", real_path.display()))? + .mode() + }; let rewritten = if no_rewrite.contains(real_path) { if verbose { @@ -247,7 +219,47 @@ fn run_host_mode(args: CliArgs) -> anyhow::Result<()> { } } - finalize_tar(tar_entries, added_tar_paths, &args)?; + // Append --include files (ELF files are automatically rewritten). + let includes: Vec = args + .include + .iter() + .map(|s| parse_include(s)) + .collect::>>()?; + + for inc in &includes { + if !inc.host_path.exists() { + bail!("included file does not exist: {}", inc.host_path.display()); + } + if !added_tar_paths.insert(inc.tar_path.clone()) { + bail!( + "duplicate tar path from --include: '{}' (already present)", + inc.tar_path + ); + } + let data = std::fs::read(&inc.host_path) + .with_context(|| format!("failed to read included file {}", inc.host_path.display()))?; + let mode = { + use std::os::unix::fs::MetadataExt as _; + std::fs::metadata(&inc.host_path) + .map(|m| m.mode()) + .unwrap_or(0o755) + }; + let rewritten = rewrite_elf(&data, &inc.host_path, args.verbose)?; + if args.verbose { + eprintln!( + " including {} as {}", + inc.host_path.display(), + inc.tar_path + ); + } + tar_entries.push(TarEntry { + tar_path: inc.tar_path.clone(), + data: rewritten, + mode, + }); + } + + finalize_tar(tar_entries, &args)?; Ok(()) } @@ -361,101 +373,17 @@ fn run_oci(image_ref: &str, args: &CliArgs) -> anyhow::Result<()> { } } - finalize_tar(tar_entries, added_tar_paths, args)?; + finalize_tar(tar_entries, args)?; Ok(()) } // --------------------------------------------------------------------------- -// Shared finalization: includes, tar build, size report +// Shared finalization: tar build, size report // --------------------------------------------------------------------------- -/// Append `--include` and `--rewrite-include` files, build the output tar, -/// and print a size summary. -/// -/// Both host mode and OCI mode call this after producing their rewritten -/// `TarEntry` list. -fn finalize_tar( - mut tar_entries: Vec, - mut added_tar_paths: BTreeSet, - args: &CliArgs, -) -> anyhow::Result<()> { - // Parse and append --include files. - let includes: Vec = args - .include - .iter() - .map(|s| parse_include(s)) - .collect::>>()?; - - for inc in &includes { - if !inc.host_path.exists() { - bail!("included file does not exist: {}", inc.host_path.display()); - } - if !added_tar_paths.insert(inc.tar_path.clone()) { - bail!( - "duplicate tar path from --include: '{}' (already present)", - inc.tar_path - ); - } - let data = std::fs::read(&inc.host_path) - .with_context(|| format!("failed to read included file {}", inc.host_path.display()))?; - let mode = std::fs::metadata(&inc.host_path) - .map(|m| file_mode(&m)) - .unwrap_or(0o644); - if args.verbose { - eprintln!( - " including {} as {}", - inc.host_path.display(), - inc.tar_path - ); - } - tar_entries.push(TarEntry { - tar_path: inc.tar_path.clone(), - data, - mode, - }); - } - - // Include extra ELF files **with** rewriting (for dlopen'd libraries). - let rewrite_includes: Vec = args - .rewrite_include - .iter() - .map(|s| parse_include(s)) - .collect::>>()?; - - for inc in &rewrite_includes { - if !inc.host_path.exists() { - bail!( - "rewrite-included file does not exist: {}", - inc.host_path.display() - ); - } - if !added_tar_paths.insert(inc.tar_path.clone()) { - bail!( - "duplicate tar path from --rewrite-include: '{}' (already present)", - inc.tar_path - ); - } - let data = std::fs::read(&inc.host_path) - .with_context(|| format!("failed to read {}", inc.host_path.display()))?; - let mode = std::fs::metadata(&inc.host_path) - .map(|m| file_mode(&m)) - .unwrap_or(0o755); - let rewritten = rewrite_elf(&data, &inc.host_path, args.verbose)?; - if args.verbose { - eprintln!( - " rewrite-including {} as {}", - inc.host_path.display(), - inc.tar_path - ); - } - tar_entries.push(TarEntry { - tar_path: inc.tar_path.clone(), - data: rewritten, - mode, - }); - } - +/// Build the output tar and print a size summary. +fn finalize_tar(tar_entries: Vec, args: &CliArgs) -> anyhow::Result<()> { // Build tar. eprintln!("Creating {}...", args.output.display()); build_tar(&tar_entries, &args.output)?; @@ -475,10 +403,6 @@ fn finalize_tar( Ok(()) } -// --------------------------------------------------------------------------- -// Dependency discovery (via ldd) — Linux only -// --------------------------------------------------------------------------- - #[cfg(target_os = "linux")] struct ResolvedDep { ldd_path: PathBuf, @@ -694,11 +618,6 @@ fn rewrite_elf(data: &[u8], path: &Path, verbose: bool) -> anyhow::Result bail!( - "{} is a Bun-packaged executable and cannot be packaged as-is: \ - tar-loaded programs must already contain LiteBox syscall trampolines", - path.display() - ), Err(litebox_syscall_rewriter::Error::NoTextSectionFound) => { if verbose { eprintln!( @@ -752,17 +671,3 @@ fn build_tar(entries: &[TarEntry], output: &Path) -> anyhow::Result<()> { builder.finish().context("failed to finalize tar archive")?; Ok(()) } - -#[cfg(test)] -mod tests { - use super::rewrite_elf; - use std::path::Path; - - #[test] - fn rewrite_elf_skips_non_elf_files() { - // Non-ELF data should be returned unmodified. - let data = b"#!/bin/sh\necho hello\n"; - let result = rewrite_elf(data, Path::new("/tmp/script.sh"), false).unwrap(); - assert_eq!(result, data); - } -} diff --git a/litebox_packager/src/oci.rs b/litebox_packager/src/oci.rs index 64a78b9ef..eb97a55f2 100644 --- a/litebox_packager/src/oci.rs +++ b/litebox_packager/src/oci.rs @@ -625,7 +625,6 @@ fn is_unix_absolute(path: &Path) -> bool { path.as_os_str() .to_str() .is_some_and(|s| s.starts_with('/')) - || path.is_absolute() } /// Strip the leading `/` from a Unix-style absolute path to make it @@ -647,8 +646,10 @@ fn normalize_path(path: &Path) -> PathBuf { std::path::Component::ParentDir => { result.pop(); } - std::path::Component::CurDir | std::path::Component::RootDir => {} - c => result.push(c), + std::path::Component::CurDir + | std::path::Component::RootDir + | std::path::Component::Prefix(_) => {} + c @ std::path::Component::Normal(_) => result.push(c), } } result.iter().collect() @@ -736,18 +737,14 @@ fn materialize_symlinks( /// Look up the Unix permission mode for a file. /// -/// Prefers the tar-header–derived `permissions` map (keyed by rootfs-relative -/// path) which is accurate on all platforms. Falls back to `file_mode()` on -/// the host path (accurate on Unix, heuristic on Windows), and finally -/// defaults to 0o644 if neither source is available. -fn lookup_mode(rel_path: &Path, host_path: &Path, permissions: &HashMap) -> u32 { +/// Look up the Unix file mode for a rootfs-relative path from the OCI tar +/// header permissions map. Defaults to 0o644 if not found. +fn lookup_mode(rel_path: &Path, permissions: &HashMap) -> u32 { if let Some(&mode) = permissions.get(rel_path) { - return mode & 0o7777; - } - if let Ok(metadata) = std::fs::metadata(host_path) { - return super::file_mode(&metadata) & 0o7777; + mode & 0o7777 + } else { + 0o644 } - 0o644 } /// Scan an extracted rootfs directory and build a file map for packaging. @@ -811,7 +808,7 @@ pub fn scan_rootfs( let tar_path = tar_path.replace('\\', "/"); if entry.file_type().is_file() { - let mode = lookup_mode(rel_path, entry.path(), permissions); + let mode = lookup_mode(rel_path, permissions); let is_executable = mode & 0o111 != 0; if verbose && is_executable { @@ -832,7 +829,7 @@ pub fn scan_rootfs( if let Some(resolved) = resolve_in_rootfs(entry.path(), rootfs, 16) { if resolved.is_file() { let resolved_rel = resolved.strip_prefix(rootfs).unwrap_or(&resolved); - let mode = lookup_mode(resolved_rel, &resolved, permissions); + let mode = lookup_mode(resolved_rel, permissions); let is_executable = mode & 0o111 != 0; files.insert( @@ -914,7 +911,7 @@ pub fn scan_rootfs( } let read_rel = read_path.strip_prefix(rootfs).unwrap_or(&read_path); - let mode = lookup_mode(read_rel, &read_path, permissions); + let mode = lookup_mode(read_rel, permissions); let is_executable = mode & 0o111 != 0; if verbose { @@ -974,264 +971,75 @@ mod tests { use super::*; #[test] - fn test_is_gzip_data() { - assert!(is_gzip_data(&[0x1f, 0x8b, 0x08])); - assert!(!is_gzip_data(&[0x00, 0x00])); - assert!(!is_gzip_data(&[0x1f])); - assert!(!is_gzip_data(&[])); - } - - #[test] - fn test_resolve_in_rootfs_non_symlink() { - // Non-existent path returns None - let result = resolve_in_rootfs(Path::new("/nonexistent"), Path::new("/tmp"), 16); - assert!(result.is_none()); - } - - #[test] - fn test_resolve_in_rootfs_max_depth_zero() { - let result = resolve_in_rootfs(Path::new("/tmp"), Path::new("/tmp"), 0); - assert!(result.is_none()); - } - - // --- normalize_path --- - - #[test] - fn normalize_path_resolves_parent_components() { - let p = normalize_path(Path::new("usr/lib/../bin/sh")); - assert_eq!(p, PathBuf::from("usr/bin/sh")); - } - - #[test] - fn normalize_path_strips_current_dir() { - let p = normalize_path(Path::new("./usr/./bin/sh")); - assert_eq!(p, PathBuf::from("usr/bin/sh")); - } - - #[test] - fn normalize_path_strips_root() { - let p = normalize_path(Path::new("/usr/bin/sh")); - assert_eq!(p, PathBuf::from("usr/bin/sh")); - } - - #[test] - fn normalize_path_double_parent_at_start_clamps() { - // Going above root should just empty the stack. - let p = normalize_path(Path::new("../../foo")); - assert_eq!(p, PathBuf::from("foo")); - } - - #[test] - fn normalize_path_empty_input() { - let p = normalize_path(Path::new("")); - assert_eq!(p, PathBuf::from("")); - } - - // --- is_unix_absolute --- - - #[test] - fn is_unix_absolute_detects_slash_prefix() { - assert!(is_unix_absolute(Path::new("/usr/bin"))); - assert!(is_unix_absolute(Path::new("/"))); - } - - #[test] - fn is_unix_absolute_rejects_relative() { - assert!(!is_unix_absolute(Path::new("usr/bin"))); - assert!(!is_unix_absolute(Path::new("../lib"))); - assert!(!is_unix_absolute(Path::new(""))); - } - - // --- strip_unix_root --- - - #[test] - fn strip_unix_root_removes_leading_slash() { - assert_eq!( - strip_unix_root(Path::new("/usr/bin")), - PathBuf::from("usr/bin") - ); - } - - #[test] - fn strip_unix_root_noop_for_relative() { - assert_eq!( - strip_unix_root(Path::new("usr/bin")), - PathBuf::from("usr/bin") - ); - } - - #[test] - fn strip_unix_root_on_bare_slash() { - // "/" should become empty after stripping. - let p = strip_unix_root(Path::new("/")); - assert!(p.as_os_str().is_empty() || p == Path::new("")); - } - - // --- resolve_symlink_in_rootfs --- - - #[test] - fn resolve_symlink_direct_hit() { - // lib64 -> usr/lib64, and rootfs/usr/lib64/libc.so exists on disk. + fn resolve_symlink_in_rootfs_happy_paths() { let tmp = tempfile::tempdir().unwrap(); let rootfs = tmp.path(); std::fs::create_dir_all(rootfs.join("usr/lib64")).unwrap(); + std::fs::create_dir_all(rootfs.join("usr/lib")).unwrap(); + std::fs::create_dir_all(rootfs.join("usr/bin")).unwrap(); std::fs::write(rootfs.join("usr/lib64/libc.so"), b"fake").unwrap(); - - let mut symlink_map = HashMap::new(); - symlink_map.insert(PathBuf::from("lib64"), PathBuf::from("usr/lib64")); - - // Resolving "lib64" itself should follow to rootfs/usr/lib64 (dir). - let resolved = resolve_symlink_in_rootfs(Path::new("lib64"), rootfs, &symlink_map, 32); - assert!(resolved.is_some()); - assert_eq!(resolved.unwrap(), rootfs.join("usr/lib64")); - } - - #[test] - fn resolve_symlink_chain() { - // a -> b, b -> c, rootfs/c exists. - let tmp = tempfile::tempdir().unwrap(); - let rootfs = tmp.path(); + std::fs::write(rootfs.join("usr/lib64/foo.so"), b"elf").unwrap(); + std::fs::write(rootfs.join("usr/lib/libfoo.so"), b"elf").unwrap(); + std::fs::write(rootfs.join("usr/bin/sh"), b"elf").unwrap(); std::fs::write(rootfs.join("c"), b"data").unwrap(); let mut symlink_map = HashMap::new(); + symlink_map.insert(PathBuf::from("lib64"), PathBuf::from("usr/lib64")); symlink_map.insert(PathBuf::from("a"), PathBuf::from("b")); symlink_map.insert(PathBuf::from("b"), PathBuf::from("c")); - - let resolved = resolve_symlink_in_rootfs(Path::new("a"), rootfs, &symlink_map, 32); - assert_eq!(resolved, Some(rootfs.join("c"))); - } - - #[test] - fn resolve_symlink_max_depth_prevents_infinite_loop() { - // a -> b, b -> a (cycle). - let mut symlink_map = HashMap::new(); - symlink_map.insert(PathBuf::from("a"), PathBuf::from("b")); - symlink_map.insert(PathBuf::from("b"), PathBuf::from("a")); - - let tmp = tempfile::tempdir().unwrap(); - let resolved = resolve_symlink_in_rootfs(Path::new("a"), tmp.path(), &symlink_map, 32); - assert!(resolved.is_none()); - } - - #[test] - fn resolve_symlink_absolute_target() { - // link -> /usr/bin/sh, rootfs/usr/bin/sh exists. - let tmp = tempfile::tempdir().unwrap(); - let rootfs = tmp.path(); - std::fs::create_dir_all(rootfs.join("usr/bin")).unwrap(); - std::fs::write(rootfs.join("usr/bin/sh"), b"elf").unwrap(); - - let mut symlink_map = HashMap::new(); symlink_map.insert(PathBuf::from("bin/sh"), PathBuf::from("/usr/bin/sh")); - - let resolved = resolve_symlink_in_rootfs(Path::new("bin/sh"), rootfs, &symlink_map, 32); - assert_eq!(resolved, Some(rootfs.join("usr/bin/sh"))); - } - - #[test] - fn resolve_symlink_relative_target() { - // usr/lib64/libfoo.so -> ../lib/libfoo.so, rootfs/usr/lib/libfoo.so exists. - let tmp = tempfile::tempdir().unwrap(); - let rootfs = tmp.path(); - std::fs::create_dir_all(rootfs.join("usr/lib")).unwrap(); - std::fs::write(rootfs.join("usr/lib/libfoo.so"), b"elf").unwrap(); - - let mut symlink_map = HashMap::new(); symlink_map.insert( PathBuf::from("usr/lib64/libfoo.so"), PathBuf::from("../lib/libfoo.so"), ); - let resolved = - resolve_symlink_in_rootfs(Path::new("usr/lib64/libfoo.so"), rootfs, &symlink_map, 32); - assert_eq!(resolved, Some(rootfs.join("usr/lib/libfoo.so"))); - } + // Direct symlink: lib64 -> usr/lib64 + let r = resolve_symlink_in_rootfs(Path::new("lib64"), rootfs, &symlink_map, 32); + assert_eq!(r, Some(rootfs.join("usr/lib64"))); - #[test] - fn resolve_symlink_ancestor_is_symlink() { - // lib64 -> usr/lib64, resolve "lib64/foo.so" where rootfs/usr/lib64/foo.so exists. - let tmp = tempfile::tempdir().unwrap(); - let rootfs = tmp.path(); - std::fs::create_dir_all(rootfs.join("usr/lib64")).unwrap(); - std::fs::write(rootfs.join("usr/lib64/foo.so"), b"elf").unwrap(); + // Chain: a -> b -> c + let r = resolve_symlink_in_rootfs(Path::new("a"), rootfs, &symlink_map, 32); + assert_eq!(r, Some(rootfs.join("c"))); - let mut symlink_map = HashMap::new(); - symlink_map.insert(PathBuf::from("lib64"), PathBuf::from("usr/lib64")); + // Absolute target: bin/sh -> /usr/bin/sh + let r = resolve_symlink_in_rootfs(Path::new("bin/sh"), rootfs, &symlink_map, 32); + assert_eq!(r, Some(rootfs.join("usr/bin/sh"))); - let resolved = - resolve_symlink_in_rootfs(Path::new("lib64/foo.so"), rootfs, &symlink_map, 32); - assert_eq!(resolved, Some(rootfs.join("usr/lib64/foo.so"))); - } + // Relative target: usr/lib64/libfoo.so -> ../lib/libfoo.so + let r = + resolve_symlink_in_rootfs(Path::new("usr/lib64/libfoo.so"), rootfs, &symlink_map, 32); + assert_eq!(r, Some(rootfs.join("usr/lib/libfoo.so"))); - #[test] - fn resolve_symlink_empty_path_returns_none() { - let tmp = tempfile::tempdir().unwrap(); - let symlink_map = HashMap::new(); - let resolved = resolve_symlink_in_rootfs(Path::new(""), tmp.path(), &symlink_map, 32); - assert!(resolved.is_none()); + // Ancestor is symlink: lib64/foo.so resolves via lib64 -> usr/lib64 + let r = resolve_symlink_in_rootfs(Path::new("lib64/foo.so"), rootfs, &symlink_map, 32); + assert_eq!(r, Some(rootfs.join("usr/lib64/foo.so"))); } #[test] - fn resolve_symlink_not_a_symlink_returns_host_path() { - // Regular file, not in symlink_map — should return host_path directly. + fn resolve_symlink_in_rootfs_edge_cases() { let tmp = tempfile::tempdir().unwrap(); let rootfs = tmp.path(); std::fs::write(rootfs.join("hello.txt"), b"hi").unwrap(); - let symlink_map = HashMap::new(); - let resolved = resolve_symlink_in_rootfs(Path::new("hello.txt"), rootfs, &symlink_map, 32); - assert_eq!(resolved, Some(rootfs.join("hello.txt"))); - } - - #[test] - fn resolve_symlink_nonexistent_returns_none() { - let tmp = tempfile::tempdir().unwrap(); - let symlink_map = HashMap::new(); - let resolved = - resolve_symlink_in_rootfs(Path::new("does/not/exist"), tmp.path(), &symlink_map, 32); - assert!(resolved.is_none()); - } - - // --- lookup_mode --- - - #[test] - fn lookup_mode_prefers_permissions_map() { - let tmp = tempfile::tempdir().unwrap(); - let rootfs = tmp.path(); - std::fs::write(rootfs.join("file.sh"), b"#!/bin/sh").unwrap(); + // Cycle: a -> b -> a + let mut cycle_map = HashMap::new(); + cycle_map.insert(PathBuf::from("a"), PathBuf::from("b")); + cycle_map.insert(PathBuf::from("b"), PathBuf::from("a")); + assert!(resolve_symlink_in_rootfs(Path::new("a"), rootfs, &cycle_map, 32).is_none()); - let mut permissions = HashMap::new(); - permissions.insert(PathBuf::from("file.sh"), 0o100755u32); + let empty_map = HashMap::new(); - // The permissions map value (masked) should win over filesystem metadata. - let mode = lookup_mode(Path::new("file.sh"), &rootfs.join("file.sh"), &permissions); - assert_eq!(mode, 0o755); - } + // Empty path + assert!(resolve_symlink_in_rootfs(Path::new(""), rootfs, &empty_map, 32).is_none()); - #[test] - fn lookup_mode_falls_back_to_filesystem() { - let tmp = tempfile::tempdir().unwrap(); - let rootfs = tmp.path(); - std::fs::write(rootfs.join("file.txt"), b"data").unwrap(); - - let permissions = HashMap::new(); // empty - let mode = lookup_mode( - Path::new("file.txt"), - &rootfs.join("file.txt"), - &permissions, + // Nonexistent path + assert!( + resolve_symlink_in_rootfs(Path::new("does/not/exist"), rootfs, &empty_map, 32) + .is_none() ); - // On Unix the file should have some mode; just check it's non-zero. - assert!(mode > 0); - } - #[test] - fn lookup_mode_defaults_to_644_when_nothing_available() { - let permissions = HashMap::new(); - let mode = lookup_mode( - Path::new("nonexistent"), - Path::new("/no/such/file"), - &permissions, - ); - assert_eq!(mode, 0o644); + // Regular file (not a symlink) returns host path directly + let r = resolve_symlink_in_rootfs(Path::new("hello.txt"), rootfs, &empty_map, 32); + assert_eq!(r, Some(rootfs.join("hello.txt"))); } }