#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use super::fd::ModelThreadId;
use super::fd::{FdError, FdNumber, FdOperation, KboxlikeFdSystem, ProcessId, ShadowInstallPlan};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use super::memory::read_proc_maps_for_pid;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use super::snapshot::{
read_thread_rseq_configuration, KboxlikeByteBackedRestoreSource,
KboxlikeMaterializedMemoryRestore, KboxlikeMaterializedMemoryRestoreAction,
KboxlikeSharedMappingKind, KboxlikeSharedMemoryDump, KboxlikeThreadKernelStateSnapshot,
KboxlikeThreadSnapshot,
};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use std::collections::{BTreeMap, BTreeSet};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use std::ffi::{c_void, CStr, CString};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use std::os::unix::ffi::OsStrExt;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use std::path::{Path, PathBuf};
const LINUX_X86_64_SYS_CLOSE: i64 = 3;
const LINUX_X86_64_SYS_DUP2: i64 = 33;
const LINUX_X86_64_SYS_FCNTL: i64 = 72;
const LINUX_X86_64_SYS_MMAP: i64 = 9;
const LINUX_X86_64_SYS_MPROTECT: i64 = 10;
const LINUX_X86_64_SYS_MUNMAP: i64 = 11;
const LINUX_X86_64_SYS_RECVMSG: i64 = 47;
const LINUX_X86_64_SYS_SOCKETPAIR: i64 = 53;
const LINUX_X86_64_SYS_PRCTL: i64 = 157;
const LINUX_X86_64_SYS_OPENAT: i64 = 257;
const LINUX_X86_64_SYS_SETGROUPS: i64 = 116;
const LINUX_X86_64_SYS_SETRESUID: i64 = 117;
const LINUX_X86_64_SYS_SETRESGID: i64 = 119;
const LINUX_X86_64_SYS_SET_TID_ADDRESS: i64 = 218;
const LINUX_X86_64_SYS_SET_ROBUST_LIST: i64 = 273;
const LINUX_X86_64_SYS_RSEQ: i64 = 334;
const LINUX_RSEQ_FLAG_UNREGISTER: u64 = 1;
const LINUX_AT_FDCWD: u64 = (-100i64) as u64;
const LINUX_AF_UNIX: u64 = 1;
const LINUX_SOCK_STREAM: u64 = 1;
const LINUX_O_RDWR: u64 = 0o2;
const LINUX_O_CLOEXEC: u64 = 0o2000000;
const LINUX_PROT_READ: u64 = 0x1;
const LINUX_PROT_WRITE: u64 = 0x2;
const LINUX_PROT_EXEC: u64 = 0x4;
const LINUX_MAP_SHARED: u64 = 0x01;
const LINUX_MAP_PRIVATE: u64 = 0x02;
const LINUX_MAP_FIXED: u64 = 0x10;
const LINUX_MAP_ANONYMOUS: u64 = 0x20;
const LINUX_MAP_FIXED_NOREPLACE: u64 = 0x100000;
const LINUX_F_DUPFD_CLOEXEC: u64 = 1030;
const LINUX_F_SETFD: u64 = 2;
const LINUX_FD_CLOEXEC: u64 = 1;
const LINUX_PR_SET_MM: u64 = 35;
const LINUX_PR_SET_MM_EXE_FILE: u64 = 13;
const LINUX_PR_SET_MM_START_BRK: u64 = 6;
const LINUX_PR_SET_MM_BRK: u64 = 7;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const LINUX_CLONE_VM: i32 = 0x0000_0100;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const LINUX_CLONE_FS: i32 = 0x0000_0200;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const LINUX_CLONE_FILES: i32 = 0x0000_0400;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const LINUX_CLONE_SIGHAND: i32 = 0x0000_0800;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const LINUX_CLONE_THREAD: i32 = 0x0001_0000;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const LINUX_CLONE_SYSVSEM: i32 = 0x0004_0000;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const KBOXLIKE_REPLACEMENT_SYSCALL_PAGE_ENV: &str =
"SUPERMACHINE_KBOXLIKE_REPLACEMENT_SYSCALL_PAGE";
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const LINUX_PTRACE_O_TRACESYSGOOD: u64 = 0x0000_0001;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const LINUX_PTRACE_O_TRACECLONE: u64 = 0x0000_0008;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const LINUX_PTRACE_EVENT_CLONE: i32 = 3;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const LINUX_PTRACE_EVENT_SHIFT: i32 = 16;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const REPLACEMENT_FOLLOWER_STACK_LEN: usize = 1024 * 1024;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct TraceeInstallPlan {
pub(crate) source_fd: i32,
pub(crate) target_fd: i32,
pub(crate) cloexec: bool,
pub(crate) close_source_after_install: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct TraceeSyscall {
pub(crate) nr: i64,
pub(crate) args: [u64; 6],
}
impl TraceeSyscall {
pub(crate) fn new3(nr: i64, arg0: u64, arg1: u64, arg2: u64) -> Self {
Self {
nr,
args: [arg0, arg1, arg2, 0, 0, 0],
}
}
pub(crate) fn new6(nr: i64, args: [u64; 6]) -> Self {
Self { nr, args }
}
}
pub(crate) trait TraceeSyscallExecutor {
fn syscall(&mut self, syscall: TraceeSyscall) -> Result<i64, FdError>;
fn tracee_pid(&self) -> Option<i32> {
None
}
fn kernel_synthetic_ranges(&self) -> Result<Vec<(u64, u64)>, FdError> {
Ok(Vec::new())
}
}
pub(crate) trait TraceeMemoryWriter {
fn write_bytes(&mut self, addr: u64, bytes: &[u8]) -> Result<(), FdError>;
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) trait TraceeRegisterWriter {
fn write_regs(&mut self, regs: &libc::user_regs_struct) -> Result<(), FdError>;
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct TraceeMemoryRestoreSummary {
pub(crate) byte_backed_applied: usize,
pub(crate) clean_file_remapped: usize,
pub(crate) shared_object_remapped: usize,
pub(crate) skipped_no_mapping: usize,
pub(crate) deferred_unsupported: usize,
pub(crate) other_tracee: usize,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct TraceeRegisterRestoreSummary {
pub(crate) registers_restored: usize,
pub(crate) other_tracee: usize,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct TraceeKernelStateRestoreSummary {
pub(crate) clear_child_tid_applied: usize,
pub(crate) robust_list_applied: usize,
pub(crate) rseq_applied: usize,
pub(crate) other_tracee: usize,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct TraceeRestoreSummary {
pub(crate) memory: TraceeMemoryRestoreSummary,
pub(crate) registers: TraceeRegisterRestoreSummary,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct TraceeRestoreSetSummary {
pub(crate) tracees_restored: usize,
pub(crate) shared_objects: TraceeSharedObjectRegistryBuildSummary,
pub(crate) memory: TraceeMemoryRestoreSummary,
pub(crate) registers: TraceeRegisterRestoreSummary,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct TraceeFullRestoreSetSummary {
pub(crate) tracees_restored: usize,
pub(crate) shared_objects: TraceeSharedObjectRegistryBuildSummary,
pub(crate) memory: TraceeMemoryRestoreSummary,
pub(crate) kernel_state: TraceeKernelStateRestoreSummary,
pub(crate) registers: TraceeRegisterRestoreSummary,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct TraceeResumeSetSummary {
pub(crate) tracees_resumed: usize,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct TraceeRestoreAndResumeSetSummary {
pub(crate) restore: TraceeRestoreSetSummary,
pub(crate) resume: TraceeResumeSetSummary,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct TraceeFullRestoreAndResumeSetSummary {
pub(crate) restore: TraceeFullRestoreSetSummary,
pub(crate) resume: TraceeResumeSetSummary,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct TraceeReplacementPidRemapSummary {
pub(crate) threads_remapped: usize,
pub(crate) memory_actions_remapped: usize,
pub(crate) shared_dumps_remapped: usize,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug)]
pub(crate) struct TraceeRemappedRestoreSet {
pub(crate) restore: KboxlikeMaterializedMemoryRestore,
pub(crate) threads: Vec<KboxlikeThreadSnapshot>,
pub(crate) shared_dumps: Vec<KboxlikeSharedMemoryDump>,
pub(crate) summary: TraceeReplacementPidRemapSummary,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct TraceeThreadGroupMemoryNormalizeSummary {
pub(crate) process_groups: usize,
pub(crate) memory_actions_retained: usize,
pub(crate) memory_actions_reassigned_to_owner: usize,
pub(crate) duplicate_memory_actions_removed: usize,
pub(crate) shared_dumps_retained: usize,
pub(crate) shared_dumps_reassigned_to_owner: usize,
pub(crate) duplicate_shared_dumps_removed: usize,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug)]
pub(crate) struct TraceeThreadGroupMemoryNormalizedRestoreSet {
pub(crate) restore: KboxlikeMaterializedMemoryRestore,
pub(crate) shared_dumps: Vec<KboxlikeSharedMemoryDump>,
pub(crate) memory_owner_host_pids: BTreeMap<ProcessId, i32>,
pub(crate) summary: TraceeThreadGroupMemoryNormalizeSummary,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct TraceeReplacementThreadGroupCreateSummary {
pub(crate) process_groups: usize,
pub(crate) tracees_requested: usize,
pub(crate) tracees_created: usize,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct TraceeReplacementThreadGroupCreateSet {
pub(crate) replacement_host_pids: BTreeMap<ModelThreadId, i32>,
pub(crate) summary: TraceeReplacementThreadGroupCreateSummary,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) trait StoppedTraceeReplacementFactory {
fn prepare_syscall_page(&mut self, _syscall_page: u64) -> Result<(), FdError> {
Ok(())
}
fn create_stopped_replacement_group(
&mut self,
model_pid: ProcessId,
threads: &[KboxlikeThreadSnapshot],
) -> Result<BTreeMap<ModelThreadId, i32>, FdError>;
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeMemoryRestoreSummary {
fn add_assign(&mut self, other: Self) {
self.byte_backed_applied += other.byte_backed_applied;
self.clean_file_remapped += other.clean_file_remapped;
self.shared_object_remapped += other.shared_object_remapped;
self.skipped_no_mapping += other.skipped_no_mapping;
self.deferred_unsupported += other.deferred_unsupported;
self.other_tracee += other.other_tracee;
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeRegisterRestoreSummary {
fn add_assign(&mut self, other: Self) {
self.registers_restored += other.registers_restored;
self.other_tracee += other.other_tracee;
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeKernelStateRestoreSummary {
fn add_assign(&mut self, other: Self) {
self.clear_child_tid_applied += other.clear_child_tid_applied;
self.robust_list_applied += other.robust_list_applied;
self.rseq_applied += other.rseq_applied;
self.other_tracee += other.other_tracee;
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) struct TraceeRestoreAccess<W, E, R> {
pub(crate) writer: W,
pub(crate) executor: E,
pub(crate) register_writer: R,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) trait TraceeRestoreAccessFactory {
type Writer: TraceeMemoryWriter;
type Executor: TraceeSyscallExecutor;
type RegisterWriter: TraceeRegisterWriter;
fn access_for_restore(
&mut self,
host_pid: i32,
) -> Result<TraceeRestoreAccess<Self::Writer, Self::Executor, Self::RegisterWriter>, FdError>;
fn mark_pids_already_cleared(&mut self, _pids: &BTreeSet<i32>) {}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) trait RestoredTraceeResumer {
fn resume_restored_tracee(&mut self, host_pid: i32) -> Result<(), FdError>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct TraceeByteBackedMapping {
pub(crate) addr: u64,
pub(crate) prot: i32,
pub(crate) bytes: Vec<u8>,
}
impl TraceeByteBackedMapping {
pub(crate) fn new(addr: u64, prot: i32, bytes: Vec<u8>) -> Result<Self, FdError> {
if addr == 0 {
return Err(FdError::TraceeInstallFailed(
"tracee mapping address is null",
));
}
if bytes.is_empty() {
return Err(FdError::TraceeInstallFailed(
"tracee mapping bytes are empty",
));
}
let valid_prot_bits = (LINUX_PROT_READ | LINUX_PROT_WRITE | LINUX_PROT_EXEC) as i32;
if prot < 0 || prot & !valid_prot_bits != 0 {
return Err(FdError::TraceeInstallFailed(
"tracee mapping protection is invalid",
));
}
Ok(Self { addr, prot, bytes })
}
pub(crate) fn apply(
&self,
writer: &mut impl TraceeMemoryWriter,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<(), FdError> {
let blocked = executor.kernel_synthetic_ranges()?;
let blocked = unmap_mappable_synthetic_collisions(
self.addr,
self.bytes.len() as u64,
&blocked,
executor,
)?;
let segments =
split_range_around_blocked_ranges(self.addr, self.bytes.len() as u64, &blocked)?;
if segments.is_empty() {
return Ok(());
}
let restored_len = segments.iter().map(|(_, len)| *len).sum::<u64>();
if (segments.len() > 1 || restored_len != self.bytes.len() as u64)
&& std::env::var_os("SUPERMACHINE_KBOXLIKE_TRACE_MEMORY_RESTORE").is_some()
{
eprintln!(
"KBOXLIVE_BYTE_MMAP_SPLIT start=0x{:x} len={} restored_len={} segments={} blocked={:?}",
self.addr,
self.bytes.len(),
restored_len,
segments.len(),
blocked,
);
}
for (addr, len) in segments {
self.apply_range(addr, len, writer, executor)?;
}
Ok(())
}
pub(crate) fn mmap_syscall(&self) -> TraceeSyscall {
self.mmap_range_syscall(self.addr, self.bytes.len() as u64)
}
pub(crate) fn mmap_range_syscall(&self, addr: u64, len: u64) -> TraceeSyscall {
TraceeSyscall::new6(
LINUX_X86_64_SYS_MMAP,
[
addr,
len,
LINUX_PROT_READ | LINUX_PROT_WRITE,
LINUX_MAP_PRIVATE | LINUX_MAP_FIXED | LINUX_MAP_ANONYMOUS,
(-1i64) as u64,
0,
],
)
}
pub(crate) fn mprotect_syscall(&self) -> TraceeSyscall {
self.mprotect_range_syscall(self.addr, self.bytes.len() as u64)
}
pub(crate) fn mprotect_range_syscall(&self, addr: u64, len: u64) -> TraceeSyscall {
TraceeSyscall::new3(LINUX_X86_64_SYS_MPROTECT, addr, len, self.prot as u64)
}
fn apply_range(
&self,
addr: u64,
len: u64,
writer: &mut impl TraceeMemoryWriter,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<(), FdError> {
let mapped = executor.syscall(self.mmap_range_syscall(addr, len))?;
if mapped < 0 {
return Err(FdError::TraceeInstallFailed("tracee fixed mmap failed"));
}
if mapped as u64 != addr {
return Err(FdError::TraceeInstallFailed(
"tracee fixed mmap returned wrong address",
));
}
let offset = usize::try_from(addr - self.addr)
.map_err(|_| FdError::TraceeInstallFailed("tracee mapping byte offset overflowed"))?;
let len_usize = usize::try_from(len)
.map_err(|_| FdError::TraceeInstallFailed("tracee mapping byte len overflowed"))?;
let end = offset
.checked_add(len_usize)
.ok_or(FdError::TraceeInstallFailed(
"tracee mapping byte slice overflowed",
))?;
writer.write_bytes(addr, &self.bytes[offset..end])?;
if self.needs_final_mprotect() {
let protected = executor.syscall(self.mprotect_range_syscall(addr, len))?;
if protected < 0 {
return Err(FdError::TraceeInstallFailed("tracee mprotect failed"));
}
}
Ok(())
}
fn needs_final_mprotect(&self) -> bool {
self.prot as u64 != (LINUX_PROT_READ | LINUX_PROT_WRITE)
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct TraceeReservedMapping {
pub(crate) addr: u64,
pub(crate) len: u64,
pub(crate) prot: i32,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeReservedMapping {
pub(crate) fn new(addr: u64, len: u64, prot: i32) -> Result<Self, FdError> {
if addr == 0 || len == 0 {
return Err(FdError::TraceeInstallFailed(
"tracee reserved mapping requires non-empty address range",
));
}
Ok(Self { addr, len, prot })
}
pub(crate) fn apply(&self, executor: &mut impl TraceeSyscallExecutor) -> Result<(), FdError> {
let blocked = executor.kernel_synthetic_ranges()?;
let blocked = unmap_mappable_synthetic_collisions(self.addr, self.len, &blocked, executor)?;
let segments = split_range_around_blocked_ranges(self.addr, self.len, &blocked)?;
for (addr, len) in segments {
let mapped = executor.syscall(self.mmap_range_syscall(addr, len))?;
if mapped < 0 {
return Err(FdError::TraceeInstallFailed(
"tracee fixed reserved mmap failed",
));
}
if mapped as u64 != addr {
return Err(FdError::TraceeInstallFailed(
"tracee fixed reserved mmap returned wrong address",
));
}
}
Ok(())
}
pub(crate) fn mmap_syscall(&self) -> TraceeSyscall {
self.mmap_range_syscall(self.addr, self.len)
}
pub(crate) fn mmap_range_syscall(&self, addr: u64, len: u64) -> TraceeSyscall {
TraceeSyscall::new6(
LINUX_X86_64_SYS_MMAP,
[
addr,
len,
self.prot as u64,
LINUX_MAP_PRIVATE | LINUX_MAP_FIXED | LINUX_MAP_ANONYMOUS,
(-1i64) as u64,
0,
],
)
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct TraceeCleanFileMapping {
pub(crate) addr: u64,
pub(crate) len: u64,
pub(crate) prot: i32,
pub(crate) path_bytes: Vec<u8>,
pub(crate) file_offset: u64,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeCleanFileMapping {
pub(crate) fn new(
addr: u64,
len: u64,
prot: i32,
path: &Path,
file_offset: u64,
) -> Result<Self, FdError> {
if addr == 0 {
return Err(FdError::TraceeInstallFailed(
"tracee file mapping address is null",
));
}
if len == 0 {
return Err(FdError::TraceeInstallFailed(
"tracee file mapping length is zero",
));
}
let valid_prot_bits = (LINUX_PROT_READ | LINUX_PROT_WRITE | LINUX_PROT_EXEC) as i32;
if prot < 0 || prot & !valid_prot_bits != 0 {
return Err(FdError::TraceeInstallFailed(
"tracee file mapping protection is invalid",
));
}
let raw_path = path.as_os_str().as_bytes();
if raw_path.is_empty() {
return Err(FdError::TraceeInstallFailed(
"tracee file mapping path is empty",
));
}
if raw_path.contains(&0) {
return Err(FdError::TraceeInstallFailed(
"tracee file mapping path contains nul",
));
}
let mut path_bytes = raw_path.to_vec();
path_bytes.push(0);
Ok(Self {
addr,
len,
prot,
path_bytes,
file_offset,
})
}
pub(crate) fn apply_with_scratch(
&self,
writer: &mut impl TraceeMemoryWriter,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<(), FdError> {
let scratch = TraceeScratchMapping::map(self.path_bytes.len(), executor)?;
let result = self.apply_at_path_addr(scratch.addr, writer, executor);
let unmap_result = scratch.unmap(executor);
match (result, unmap_result) {
(Err(err), _) => Err(err),
(Ok(()), Err(err)) => Err(err),
(Ok(()), Ok(())) => Ok(()),
}
}
pub(crate) fn apply_at_path_addr(
&self,
tracee_path_addr: u64,
writer: &mut impl TraceeMemoryWriter,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<(), FdError> {
if tracee_path_addr == 0 {
return Err(FdError::TraceeInstallFailed(
"tracee file mapping path address is null",
));
}
writer.write_bytes(tracee_path_addr, &self.path_bytes)?;
let file_fd = self.open(executor, tracee_path_addr)?;
let mmap_result = self.mmap_file(executor, file_fd);
let close_result = self.close_file(executor, file_fd);
match (mmap_result, close_result) {
(Err(err), _) => Err(err),
(Ok(()), Err(err)) => Err(err),
(Ok(()), Ok(())) => Ok(()),
}
}
pub(crate) fn open_syscall(&self, tracee_path_addr: u64) -> TraceeSyscall {
TraceeSyscall::new3(
LINUX_X86_64_SYS_OPENAT,
LINUX_AT_FDCWD,
tracee_path_addr,
LINUX_O_CLOEXEC,
)
}
pub(crate) fn mmap_syscall(&self, file_fd: i32) -> TraceeSyscall {
self.mmap_range_syscall(self.addr, self.len, self.file_offset, file_fd)
}
pub(crate) fn mmap_range_syscall(
&self,
addr: u64,
len: u64,
file_offset: u64,
file_fd: i32,
) -> TraceeSyscall {
TraceeSyscall::new6(
LINUX_X86_64_SYS_MMAP,
[
addr,
len,
self.prot as u64,
LINUX_MAP_PRIVATE | LINUX_MAP_FIXED,
file_fd as u64,
file_offset,
],
)
}
fn open(
&self,
executor: &mut impl TraceeSyscallExecutor,
tracee_path_addr: u64,
) -> Result<i32, FdError> {
let ret = executor.syscall(self.open_syscall(tracee_path_addr))?;
if ret < 0 {
return Err(FdError::TraceeInstallFailed(
"tracee open clean file failed",
));
}
i32::try_from(ret)
.map_err(|_| FdError::TraceeInstallFailed("tracee open clean file fd overflowed"))
}
fn mmap_file(
&self,
executor: &mut impl TraceeSyscallExecutor,
file_fd: i32,
) -> Result<(), FdError> {
let blocked = executor.kernel_synthetic_ranges()?;
let blocked = unmap_mappable_synthetic_collisions(self.addr, self.len, &blocked, executor)?;
let segments = split_range_around_blocked_ranges(self.addr, self.len, &blocked)?;
if segments.is_empty() {
return Ok(());
}
let restored_len = segments.iter().map(|(_, len)| *len).sum::<u64>();
if (segments.len() > 1 || restored_len != self.len)
&& std::env::var_os("SUPERMACHINE_KBOXLIKE_TRACE_MEMORY_RESTORE").is_some()
{
eprintln!(
"KBOXLIVE_CLEAN_MMAP_SPLIT start=0x{:x} len={} restored_len={} segments={} blocked={:?} path={}",
self.addr,
self.len,
restored_len,
segments.len(),
blocked,
String::from_utf8_lossy(&self.path_bytes[..self.path_bytes.len() - 1]),
);
}
for (addr, len) in segments {
let file_offset = self.file_offset.checked_add(addr - self.addr).ok_or(
FdError::TraceeInstallFailed("tracee fixed file mmap offset overflowed"),
)?;
self.mmap_file_range(executor, file_fd, addr, len, file_offset)?;
}
Ok(())
}
fn mmap_file_range(
&self,
executor: &mut impl TraceeSyscallExecutor,
file_fd: i32,
addr: u64,
len: u64,
file_offset: u64,
) -> Result<(), FdError> {
let mapped = executor.syscall(self.mmap_range_syscall(addr, len, file_offset, file_fd))?;
if mapped < 0 {
return Err(FdError::TraceeInstallFailed(
"tracee fixed file mmap failed",
));
}
if mapped as u64 != addr {
return Err(FdError::TraceeInstallFailed(
"tracee fixed file mmap returned wrong address",
));
}
Ok(())
}
fn close_file(
&self,
executor: &mut impl TraceeSyscallExecutor,
file_fd: i32,
) -> Result<(), FdError> {
let ret = executor.syscall(TraceeSyscall::new3(
LINUX_X86_64_SYS_CLOSE,
file_fd as u64,
0,
0,
))?;
if ret < 0 {
return Err(FdError::TraceeInstallFailed(
"tracee close clean file failed",
));
}
Ok(())
}
}
fn split_range_around_blocked_ranges(
start: u64,
len: u64,
blocked: &[(u64, u64)],
) -> Result<Vec<(u64, u64)>, FdError> {
let end = start.checked_add(len).ok_or(FdError::TraceeInstallFailed(
"tracee mapping range overflowed",
))?;
let mut segments = vec![(start, end)];
for &(block_start, block_end) in blocked {
if block_start >= block_end {
continue;
}
let mut next = Vec::new();
for (seg_start, seg_end) in segments {
if block_end <= seg_start || block_start >= seg_end {
next.push((seg_start, seg_end));
continue;
}
if block_start > seg_start {
next.push((seg_start, block_start));
}
if block_end < seg_end {
next.push((block_end, seg_end));
}
}
segments = next;
if segments.is_empty() {
break;
}
}
Ok(segments
.into_iter()
.filter_map(|(seg_start, seg_end)| {
(seg_end > seg_start).then_some((seg_start, seg_end - seg_start))
})
.collect())
}
fn unmap_mappable_synthetic_collisions(
start: u64,
len: u64,
blocked: &[(u64, u64)],
executor: &mut impl TraceeSyscallExecutor,
) -> Result<Vec<(u64, u64)>, FdError> {
let end = start.checked_add(len).ok_or(FdError::TraceeInstallFailed(
"tracee mapping range overflowed",
))?;
let mut still_blocked = Vec::new();
for &(block_start, block_end) in blocked {
if block_start >= block_end {
continue;
}
if block_end <= start || block_start >= end {
still_blocked.push((block_start, block_end));
continue;
}
if is_fixed_vsyscall_range(block_start, block_end) {
still_blocked.push((block_start, block_end));
continue;
}
let block_len = block_end - block_start;
let ret = executor.syscall(TraceeSyscall::new6(
LINUX_X86_64_SYS_MUNMAP,
[block_start, block_len, 0, 0, 0, 0],
))?;
if ret < 0 {
return Err(FdError::TraceeInstallFailed(
"tracee synthetic munmap failed",
));
}
if std::env::var_os("SUPERMACHINE_KBOXLIKE_TRACE_MEMORY_RESTORE").is_some() {
eprintln!(
"KBOXLIVE_SYNTHETIC_MUNMAP start=0x{:x} len={} mapping_start=0x{:x} mapping_len={}",
block_start, block_len, start, len,
);
}
}
Ok(still_blocked)
}
fn is_fixed_vsyscall_range(start: u64, end: u64) -> bool {
start >= 0xffff_ffff_ff00_0000 || end > 0xffff_ffff_ff00_0000
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct TraceeSharedObjectKey {
pub(crate) kind: super::snapshot::KboxlikeSharedMappingKind,
pub(crate) path: Option<String>,
pub(crate) dev: String,
pub(crate) inode: u64,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeSharedObjectKey {
pub(crate) fn from_action(action: &KboxlikeMaterializedMemoryRestoreAction) -> Option<Self> {
let KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject {
kind,
path,
dev,
inode,
..
} = action
else {
return None;
};
Some(Self {
kind: *kind,
path: path.clone(),
dev: dev.clone(),
inode: *inode,
})
}
pub(crate) fn from_shared_dump(dump: &KboxlikeSharedMemoryDump) -> Self {
Self {
kind: dump.kind,
path: dump.path.clone(),
dev: dump.dev.clone(),
inode: dump.inode,
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, Default)]
pub(crate) struct TraceeSharedObjectRegistry {
supervisor_fds: BTreeMap<TraceeSharedObjectKey, i32>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeSharedObjectRegistry {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn register(
&mut self,
key: TraceeSharedObjectKey,
supervisor_fd: i32,
) -> Result<(), FdError> {
if supervisor_fd < 0 {
return Err(FdError::InvalidFd(supervisor_fd));
}
if self.supervisor_fds.insert(key, supervisor_fd).is_some() {
return Err(FdError::TraceeInstallFailed(
"shared object already registered",
));
}
Ok(())
}
pub(crate) fn supervisor_fd(&self, key: &TraceeSharedObjectKey) -> Option<i32> {
self.supervisor_fds.get(key).copied()
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct TraceeSharedObjectRegistryBuildSummary {
pub(crate) created_objects: usize,
pub(crate) initialized_extents: usize,
pub(crate) skipped_unsupported_objects: usize,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Debug)]
pub(crate) struct TraceeOwnedSharedObjectRegistry {
registry: TraceeSharedObjectRegistry,
owned_fds: Vec<i32>,
close_on_drop: bool,
summary: TraceeSharedObjectRegistryBuildSummary,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeOwnedSharedObjectRegistry {
pub(crate) fn registry(&self) -> &TraceeSharedObjectRegistry {
&self.registry
}
pub(crate) fn summary(&self) -> TraceeSharedObjectRegistryBuildSummary {
self.summary
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl Drop for TraceeOwnedSharedObjectRegistry {
fn drop(&mut self) {
if self.close_on_drop {
for fd in self.owned_fds.drain(..) {
unsafe {
libc::close(fd);
}
}
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) trait TraceeSharedObjectBackingFactory {
fn create_memfd(&mut self, name: &str) -> Result<i32, FdError>;
fn set_len(&mut self, fd: i32, len: u64) -> Result<(), FdError>;
fn write_all_at(&mut self, fd: i32, offset: u64, bytes: &[u8]) -> Result<(), FdError>;
fn close_fds_on_drop(&self) -> bool {
true
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Debug, Default)]
pub(crate) struct LinuxTraceeSharedObjectBackingFactory;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeSharedObjectBackingFactory for LinuxTraceeSharedObjectBackingFactory {
fn create_memfd(&mut self, name: &str) -> Result<i32, FdError> {
let name = CString::new(name)
.map_err(|_| FdError::TraceeInstallFailed("shared object name contains nul"))?;
let fd = unsafe { libc::memfd_create(name.as_ptr(), libc::MFD_CLOEXEC) };
if fd < 0 {
return Err(FdError::TraceeInstallFailed("memfd_create failed"));
}
Ok(fd)
}
fn set_len(&mut self, fd: i32, len: u64) -> Result<(), FdError> {
let len = libc::off_t::try_from(len)
.map_err(|_| FdError::TraceeInstallFailed("shared object length overflows off_t"))?;
let ret = unsafe { libc::ftruncate(fd, len) };
if ret < 0 {
return Err(FdError::TraceeInstallFailed(
"shared object ftruncate failed",
));
}
Ok(())
}
fn write_all_at(&mut self, fd: i32, offset: u64, bytes: &[u8]) -> Result<(), FdError> {
let mut written = 0usize;
while written < bytes.len() {
let remote_offset =
offset
.checked_add(written as u64)
.ok_or(FdError::TraceeInstallFailed(
"shared object write offset overflows",
))?;
let remote_offset = libc::off_t::try_from(remote_offset).map_err(|_| {
FdError::TraceeInstallFailed("shared object write offset overflows off_t")
})?;
let ret = unsafe {
libc::pwrite(
fd,
bytes[written..].as_ptr() as *const libc::c_void,
bytes.len() - written,
remote_offset,
)
};
if ret < 0 {
return Err(FdError::TraceeInstallFailed("shared object pwrite failed"));
}
if ret == 0 {
return Err(FdError::TraceeInstallFailed(
"shared object pwrite returned zero",
));
}
written += ret as usize;
}
Ok(())
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn build_owned_shared_object_registry(
restore: &KboxlikeMaterializedMemoryRestore,
shared_dumps: &[KboxlikeSharedMemoryDump],
) -> Result<TraceeOwnedSharedObjectRegistry, FdError> {
let mut factory = LinuxTraceeSharedObjectBackingFactory;
build_owned_shared_object_registry_with(restore, shared_dumps, &mut factory)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn build_owned_shared_object_registry_with(
restore: &KboxlikeMaterializedMemoryRestore,
shared_dumps: &[KboxlikeSharedMemoryDump],
factory: &mut impl TraceeSharedObjectBackingFactory,
) -> Result<TraceeOwnedSharedObjectRegistry, FdError> {
let mut required_extents = BTreeSet::new();
let mut sizes = BTreeMap::<TraceeSharedObjectKey, u64>::new();
let mut unsupported_keys = BTreeSet::new();
for action in &restore.actions {
let KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject {
len, file_offset, ..
} = action
else {
continue;
};
let Some(key) = TraceeSharedObjectKey::from_action(action) else {
unreachable!("matched RecreateSharedObject action");
};
if !is_synthetic_shared_object_supported(key.kind) {
unsupported_keys.insert(key);
continue;
}
let end = file_offset
.checked_add(*len)
.ok_or(FdError::TraceeInstallFailed("shared object size overflows"))?;
sizes
.entry(key.clone())
.and_modify(|size| *size = (*size).max(end))
.or_insert(end);
required_extents.insert((key, *file_offset, *len));
}
let mut dumps_by_extent = BTreeMap::new();
for dump in shared_dumps {
let key = TraceeSharedObjectKey::from_shared_dump(dump);
if !sizes.contains_key(&key) {
continue;
}
let len = dump.bytes.len() as u64;
if dumps_by_extent
.insert((key, dump.file_offset, len), dump)
.is_some()
{
return Err(FdError::TraceeInstallFailed(
"duplicate shared memory dump extent",
));
}
}
for extent in &required_extents {
if !dumps_by_extent.contains_key(extent) {
return Err(FdError::TraceeInstallFailed(
"missing shared memory dump extent",
));
}
}
let mut registry = TraceeSharedObjectRegistry::new();
let mut owned_fds = Vec::with_capacity(sizes.len());
let mut summary = TraceeSharedObjectRegistryBuildSummary {
skipped_unsupported_objects: unsupported_keys.len(),
..TraceeSharedObjectRegistryBuildSummary::default()
};
for (key, len) in sizes {
let fd = factory.create_memfd(&shared_object_memfd_name(&key))?;
owned_fds.push(fd);
factory.set_len(fd, len)?;
registry.register(key.clone(), fd)?;
summary.created_objects += 1;
for ((extent_key, offset, _len), dump) in &dumps_by_extent {
if extent_key == &key {
factory.write_all_at(fd, *offset, &dump.bytes)?;
summary.initialized_extents += 1;
}
}
}
Ok(TraceeOwnedSharedObjectRegistry {
registry,
owned_fds,
close_on_drop: factory.close_fds_on_drop(),
summary,
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn is_synthetic_shared_object_supported(kind: KboxlikeSharedMappingKind) -> bool {
matches!(
kind,
KboxlikeSharedMappingKind::Memfd
| KboxlikeSharedMappingKind::Anonymous
| KboxlikeSharedMappingKind::Device
| KboxlikeSharedMappingKind::File
)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn shared_object_memfd_name(key: &TraceeSharedObjectKey) -> String {
let raw = key
.path
.as_deref()
.unwrap_or("kboxlike-anonymous-shared")
.trim_start_matches('/')
.trim_start_matches("memfd:")
.trim_end_matches(" (deleted)");
let name = raw
.chars()
.map(|ch| {
if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
ch
} else {
'_'
}
})
.take(240)
.collect::<String>();
if name.is_empty() {
"kboxlike-shared".to_string()
} else {
name
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct TraceeSharedFdMapping {
pub(crate) addr: u64,
pub(crate) len: u64,
pub(crate) prot: i32,
pub(crate) file_offset: u64,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeSharedFdMapping {
pub(crate) fn new(addr: u64, len: u64, prot: i32, file_offset: u64) -> Result<Self, FdError> {
if addr == 0 {
return Err(FdError::TraceeInstallFailed(
"tracee shared mapping address is null",
));
}
if len == 0 {
return Err(FdError::TraceeInstallFailed(
"tracee shared mapping length is zero",
));
}
let valid_prot_bits = (LINUX_PROT_READ | LINUX_PROT_WRITE | LINUX_PROT_EXEC) as i32;
if prot < 0 || prot & !valid_prot_bits != 0 {
return Err(FdError::TraceeInstallFailed(
"tracee shared mapping protection is invalid",
));
}
Ok(Self {
addr,
len,
prot,
file_offset,
})
}
pub(crate) fn apply_from_supervisor_fd_with_scratch(
&self,
supervisor_pid: i32,
supervisor_fd: i32,
writer: &mut impl TraceeMemoryWriter,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<(), FdError> {
let path_probe = TraceeProcFdSourcePlan::new(supervisor_pid, supervisor_fd, 1)?;
let scratch = TraceeScratchMapping::map(path_probe.path_bytes().len(), executor)?;
let source = TraceeProcFdSourcePlan::new(supervisor_pid, supervisor_fd, scratch.addr)?;
let result = self.apply_from_proc_fd_source(&source, writer, executor);
let unmap_result = scratch.unmap(executor);
match (result, unmap_result) {
(Err(err), _) => Err(err),
(Ok(()), Err(err)) => Err(err),
(Ok(()), Ok(())) => Ok(()),
}
}
pub(crate) fn apply_from_proc_fd_source(
&self,
source: &TraceeProcFdSourcePlan,
writer: &mut impl TraceeMemoryWriter,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<(), FdError> {
source.write_path(writer)?;
let tracee_fd = source.open_with_flags(executor, self.open_flags())?;
let mmap_result = self.mmap_shared(executor, tracee_fd);
let close_result = self.close_source(executor, tracee_fd);
match (mmap_result, close_result) {
(Err(err), _) => Err(err),
(Ok(()), Err(err)) => Err(err),
(Ok(()), Ok(())) => Ok(()),
}
}
pub(crate) fn open_flags(&self) -> u64 {
if self.prot & libc::PROT_WRITE != 0 {
LINUX_O_CLOEXEC | LINUX_O_RDWR
} else {
LINUX_O_CLOEXEC
}
}
pub(crate) fn mmap_syscall(&self, tracee_fd: i32) -> TraceeSyscall {
TraceeSyscall::new6(
LINUX_X86_64_SYS_MMAP,
[
self.addr,
self.len,
self.prot as u64,
LINUX_MAP_SHARED | LINUX_MAP_FIXED,
tracee_fd as u64,
self.file_offset,
],
)
}
fn mmap_shared(
&self,
executor: &mut impl TraceeSyscallExecutor,
tracee_fd: i32,
) -> Result<(), FdError> {
let mapped = executor.syscall(self.mmap_syscall(tracee_fd))?;
if mapped < 0 {
return Err(FdError::TraceeInstallFailed(
"tracee fixed shared mmap failed",
));
}
if mapped as u64 != self.addr {
return Err(FdError::TraceeInstallFailed(
"tracee fixed shared mmap returned wrong address",
));
}
Ok(())
}
fn close_source(
&self,
executor: &mut impl TraceeSyscallExecutor,
tracee_fd: i32,
) -> Result<(), FdError> {
let ret = executor.syscall(TraceeSyscall::new3(
LINUX_X86_64_SYS_CLOSE,
tracee_fd as u64,
0,
0,
))?;
if ret < 0 {
return Err(FdError::TraceeInstallFailed(
"tracee close shared fd failed",
));
}
Ok(())
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn apply_materialized_memory_restore_for_tracee(
restore: &KboxlikeMaterializedMemoryRestore,
tracee_pid: i32,
writer: &mut impl TraceeMemoryWriter,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<TraceeMemoryRestoreSummary, FdError> {
let shared = TraceeSharedObjectRegistry::new();
apply_materialized_memory_restore_for_tracee_with_shared_objects(
restore, tracee_pid, 0, &shared, writer, executor,
)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn apply_materialized_memory_restore_for_tracee_with_shared_objects(
restore: &KboxlikeMaterializedMemoryRestore,
tracee_pid: i32,
supervisor_pid: i32,
shared_objects: &TraceeSharedObjectRegistry,
writer: &mut impl TraceeMemoryWriter,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<TraceeMemoryRestoreSummary, FdError> {
if tracee_pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
let mut summary = TraceeMemoryRestoreSummary::default();
let trace_memory_restore =
std::env::var_os("SUPERMACHINE_KBOXLIKE_TRACE_MEMORY_RESTORE").is_some();
for (action_index, action) in restore.actions.iter().enumerate() {
if materialized_action_host_pid(action) != tracee_pid {
summary.other_tracee += 1;
continue;
}
match action {
KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
start,
prot,
bytes,
source,
..
} => {
if trace_memory_restore {
eprintln!(
"KBOXLIVE_MEMORY_ACTION pid={} index={} kind=byte start=0x{:x} len={} prot=0x{:x}",
tracee_pid,
action_index,
start,
bytes.len(),
prot,
);
}
let heap_end = matches!(
source,
KboxlikeByteBackedRestoreSource::AnonymousPrivate { label }
if label.as_deref() == Some("[heap]")
)
.then(|| start + bytes.len() as u64);
TraceeByteBackedMapping::new(*start, *prot, bytes.clone())?
.apply(writer, executor)?;
summary.byte_backed_applied += 1;
if let Some(heap_end) = heap_end {
executor.syscall(TraceeSyscall::new6(
LINUX_X86_64_SYS_PRCTL,
[LINUX_PR_SET_MM, LINUX_PR_SET_MM_START_BRK, *start, 0, 0, 0],
))?;
executor.syscall(TraceeSyscall::new6(
LINUX_X86_64_SYS_PRCTL,
[LINUX_PR_SET_MM, LINUX_PR_SET_MM_BRK, heap_end, 0, 0, 0],
))?;
}
}
KboxlikeMaterializedMemoryRestoreAction::RemapCleanFile {
start,
len,
prot,
path,
file_offset,
..
} => {
if trace_memory_restore {
eprintln!(
"KBOXLIVE_MEMORY_ACTION pid={} index={} kind=clean start=0x{:x} len={} prot=0x{:x} offset={} path={}",
tracee_pid,
action_index,
start,
len,
prot,
file_offset,
path.display(),
);
}
TraceeCleanFileMapping::new(*start, *len, *prot, path, *file_offset)?
.apply_with_scratch(writer, executor)?;
summary.clean_file_remapped += 1;
}
KboxlikeMaterializedMemoryRestoreAction::SkipKernelSynthetic { .. } => {
summary.skipped_no_mapping += 1;
}
KboxlikeMaterializedMemoryRestoreAction::SkipInaccessible {
start, len, prot, ..
} => {
if trace_memory_restore {
eprintln!(
"KBOXLIVE_MEMORY_ACTION pid={} index={} kind=reserve start=0x{:x} len={} prot=0x{:x}",
tracee_pid, action_index, start, len, prot,
);
}
TraceeReservedMapping::new(*start, *len, *prot)?.apply(executor)?;
summary.skipped_no_mapping += 1;
}
action @ KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject {
start,
len,
prot,
file_offset,
..
} => {
if trace_memory_restore {
eprintln!(
"KBOXLIVE_MEMORY_ACTION pid={} index={} kind=shared start=0x{:x} len={} prot=0x{:x} offset={}",
tracee_pid,
action_index,
start,
len,
prot,
file_offset,
);
}
let Some(key) = TraceeSharedObjectKey::from_action(action) else {
unreachable!("matched RecreateSharedObject action");
};
let Some(supervisor_fd) = shared_objects.supervisor_fd(&key) else {
summary.deferred_unsupported += 1;
continue;
};
TraceeSharedFdMapping::new(*start, *len, *prot, *file_offset)?
.apply_from_supervisor_fd_with_scratch(
supervisor_pid,
supervisor_fd,
writer,
executor,
)?;
summary.shared_object_remapped += 1;
}
}
}
Ok(summary)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn apply_thread_register_restore_for_tracee(
threads: &[KboxlikeThreadSnapshot],
tracee_pid: i32,
writer: &mut impl TraceeRegisterWriter,
) -> Result<TraceeRegisterRestoreSummary, FdError> {
if tracee_pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
let mut summary = TraceeRegisterRestoreSummary::default();
let mut matching_regs = None;
for thread in threads {
if thread.host_pid == tracee_pid {
if matching_regs.replace(thread.regs).is_some() {
return Err(FdError::TraceeInstallFailed(
"duplicate tracee register snapshot",
));
}
} else {
summary.other_tracee += 1;
}
}
let Some(regs) = matching_regs else {
return Err(FdError::TraceeInstallFailed(
"missing tracee register snapshot",
));
};
let regs = sanitize_restored_user_regs(regs);
writer.write_regs(®s)?;
summary.registers_restored = 1;
Ok(summary)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn sanitize_restored_user_regs(mut regs: libc::user_regs_struct) -> libc::user_regs_struct {
if is_linux_internal_restart_result(regs.rax) {
regs.rax = (-(libc::EINTR as i64)) as u64;
}
regs
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn is_linux_internal_restart_result(rax: u64) -> bool {
matches!(
rax as i64,
-512 | -513 | -514 | -516 )
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn apply_thread_kernel_state_restore_for_tracee(
states: &[KboxlikeThreadKernelStateSnapshot],
tracee_pid: i32,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<TraceeKernelStateRestoreSummary, FdError> {
if tracee_pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
let mut summary = TraceeKernelStateRestoreSummary::default();
let mut matching_state = None;
for state in states {
if state.host_pid == tracee_pid {
if matching_state.replace(*state).is_some() {
return Err(FdError::TraceeInstallFailed(
"duplicate tracee kernel state snapshot",
));
}
} else {
summary.other_tracee += 1;
}
}
let Some(state) = matching_state else {
return Err(FdError::TraceeInstallFailed(
"missing tracee kernel state snapshot",
));
};
if let Some(clear_child_tid) = state.clear_child_tid {
let ret = executor.syscall(TraceeSyscall::new3(
LINUX_X86_64_SYS_SET_TID_ADDRESS,
clear_child_tid,
0,
0,
))?;
if ret < 0 {
return Err(FdError::TraceeInstallFailed(
"tracee set_tid_address failed",
));
}
summary.clear_child_tid_applied = 1;
}
if let Some(robust_list_head) = state.robust_list_head {
let ret = executor.syscall(TraceeSyscall::new3(
LINUX_X86_64_SYS_SET_ROBUST_LIST,
robust_list_head,
state.robust_list_len,
0,
))?;
if ret < 0 {
return Err(FdError::TraceeInstallFailed(
"tracee set_robust_list failed",
));
}
summary.robust_list_applied = 1;
}
Ok(summary)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn apply_thread_rseq_restore_for_tracee(
states: &[KboxlikeThreadKernelStateSnapshot],
tracee_pid: i32,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<usize, FdError> {
if tracee_pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
let mut matching_rseq = None;
for state in states {
if state.host_pid == tracee_pid && matching_rseq.replace(state.rseq).is_some() {
return Err(FdError::TraceeInstallFailed(
"duplicate tracee kernel state snapshot",
));
}
}
let Some(Some(rseq)) = matching_rseq else {
return Ok(0);
};
let ret = executor.syscall(TraceeSyscall::new6(
LINUX_X86_64_SYS_RSEQ,
[
rseq.abi_pointer,
rseq.abi_size as u64,
0,
rseq.signature as u64,
0,
0,
],
))?;
if ret < 0 {
return Err(FdError::TraceeInstallFailed("tracee rseq register failed"));
}
Ok(1)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn unregister_tracee_rseq_if_registered(
host_pid: i32,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<bool, FdError> {
let Some(current) = read_thread_rseq_configuration(host_pid)? else {
return Ok(false);
};
let ret = executor.syscall(TraceeSyscall::new6(
LINUX_X86_64_SYS_RSEQ,
[
current.abi_pointer,
current.abi_size as u64,
LINUX_RSEQ_FLAG_UNREGISTER,
current.signature as u64,
0,
0,
],
))?;
if ret < 0 {
return Err(FdError::TraceeInstallFailed(
"tracee rseq unregister failed",
));
}
Ok(true)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn apply_stopped_tracee_restore(
restore: &KboxlikeMaterializedMemoryRestore,
threads: &[KboxlikeThreadSnapshot],
tracee_pid: i32,
writer: &mut impl TraceeMemoryWriter,
executor: &mut impl TraceeSyscallExecutor,
register_writer: &mut impl TraceeRegisterWriter,
) -> Result<TraceeRestoreSummary, FdError> {
let shared = TraceeSharedObjectRegistry::new();
apply_stopped_tracee_restore_with_shared_objects(
restore,
threads,
tracee_pid,
0,
&shared,
writer,
executor,
register_writer,
)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn apply_stopped_tracee_restore_with_shared_objects(
restore: &KboxlikeMaterializedMemoryRestore,
threads: &[KboxlikeThreadSnapshot],
tracee_pid: i32,
supervisor_pid: i32,
shared_objects: &TraceeSharedObjectRegistry,
writer: &mut impl TraceeMemoryWriter,
executor: &mut impl TraceeSyscallExecutor,
register_writer: &mut impl TraceeRegisterWriter,
) -> Result<TraceeRestoreSummary, FdError> {
let memory = apply_materialized_memory_restore_for_tracee_with_shared_objects(
restore,
tracee_pid,
supervisor_pid,
shared_objects,
writer,
executor,
)?;
let registers = apply_thread_register_restore_for_tracee(threads, tracee_pid, register_writer)?;
Ok(TraceeRestoreSummary { memory, registers })
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn apply_stopped_tracee_restore_set(
restore: &KboxlikeMaterializedMemoryRestore,
threads: &[KboxlikeThreadSnapshot],
shared_dumps: &[KboxlikeSharedMemoryDump],
supervisor_pid: i32,
access_factory: &mut impl TraceeRestoreAccessFactory,
) -> Result<TraceeRestoreSetSummary, FdError> {
let normalized = normalize_thread_group_memory_restore(restore, threads, shared_dumps)?;
let tracee_pids = validated_restore_tracee_pids(&normalized.restore, threads)?;
apply_stopped_tracee_restore_set_for_pids(
&normalized.restore,
threads,
&normalized.shared_dumps,
supervisor_pid,
tracee_pids,
access_factory,
)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn apply_and_resume_stopped_tracee_restore_set(
restore: &KboxlikeMaterializedMemoryRestore,
threads: &[KboxlikeThreadSnapshot],
shared_dumps: &[KboxlikeSharedMemoryDump],
supervisor_pid: i32,
access_factory: &mut impl TraceeRestoreAccessFactory,
resumer: &mut impl RestoredTraceeResumer,
) -> Result<TraceeRestoreAndResumeSetSummary, FdError> {
let normalized = normalize_thread_group_memory_restore(restore, threads, shared_dumps)?;
let tracee_pids = validated_restore_tracee_pids(&normalized.restore, threads)?;
let restore_summary = apply_stopped_tracee_restore_set_for_pids(
&normalized.restore,
threads,
&normalized.shared_dumps,
supervisor_pid,
tracee_pids.clone(),
access_factory,
)?;
let mut resume_summary = TraceeResumeSetSummary::default();
for tracee_pid in tracee_pids {
resumer.resume_restored_tracee(tracee_pid)?;
resume_summary.tracees_resumed += 1;
}
Ok(TraceeRestoreAndResumeSetSummary {
restore: restore_summary,
resume: resume_summary,
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn apply_and_resume_stopped_tracee_restore_set_with_replacement_pids(
restore: &KboxlikeMaterializedMemoryRestore,
threads: &[KboxlikeThreadSnapshot],
shared_dumps: &[KboxlikeSharedMemoryDump],
replacement_host_pids: &BTreeMap<ModelThreadId, i32>,
supervisor_pid: i32,
access_factory: &mut impl TraceeRestoreAccessFactory,
resumer: &mut impl RestoredTraceeResumer,
) -> Result<TraceeRestoreAndResumeSetSummary, FdError> {
let remapped = remap_stopped_tracee_restore_set_to_replacement_pids(
restore,
threads,
shared_dumps,
replacement_host_pids,
)?;
apply_and_resume_stopped_tracee_restore_set(
&remapped.restore,
&remapped.threads,
&remapped.shared_dumps,
supervisor_pid,
access_factory,
resumer,
)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn apply_stopped_tracee_full_restore_set(
restore: &KboxlikeMaterializedMemoryRestore,
threads: &[KboxlikeThreadSnapshot],
kernel_states: &[KboxlikeThreadKernelStateSnapshot],
shared_dumps: &[KboxlikeSharedMemoryDump],
supervisor_pid: i32,
access_factory: &mut impl TraceeRestoreAccessFactory,
) -> Result<TraceeFullRestoreSetSummary, FdError> {
let normalized = normalize_thread_group_memory_restore(restore, threads, shared_dumps)?;
let tracee_pids = validated_restore_tracee_pids(&normalized.restore, threads)?;
apply_stopped_tracee_full_restore_set_for_pids(
&normalized.restore,
threads,
kernel_states,
&normalized.shared_dumps,
supervisor_pid,
tracee_pids,
access_factory,
)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn apply_stopped_tracee_full_restore_set_with_replacement_pids(
restore: &KboxlikeMaterializedMemoryRestore,
threads: &[KboxlikeThreadSnapshot],
kernel_states: &[KboxlikeThreadKernelStateSnapshot],
shared_dumps: &[KboxlikeSharedMemoryDump],
replacement_host_pids: &BTreeMap<ModelThreadId, i32>,
supervisor_pid: i32,
access_factory: &mut impl TraceeRestoreAccessFactory,
) -> Result<TraceeFullRestoreSetSummary, FdError> {
let remapped = remap_stopped_tracee_restore_set_to_replacement_pids(
restore,
threads,
shared_dumps,
replacement_host_pids,
)?;
let remapped_kernel_states =
remap_thread_kernel_states_to_replacement_pids(kernel_states, replacement_host_pids)?;
let tracee_pids = validated_restore_tracee_pids(&remapped.restore, &remapped.threads)?;
apply_stopped_tracee_full_restore_set_for_pids(
&remapped.restore,
&remapped.threads,
&remapped_kernel_states,
&remapped.shared_dumps,
supervisor_pid,
tracee_pids,
access_factory,
)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn apply_tracee_credentials_restore_with_replacement_pids(
threads: &[KboxlikeThreadSnapshot],
replacement_host_pids: &BTreeMap<ModelThreadId, i32>,
access_factory: &mut impl TraceeRestoreAccessFactory,
) -> Result<(), FdError> {
let credentials_by_replacement =
source_credentials_by_replacement_pid(threads, replacement_host_pids)?;
for replacement_pid in credentials_by_replacement.keys().copied() {
let mut access = access_factory.access_for_restore(replacement_pid)?;
apply_tracee_credentials_restore_for_tracee(
&credentials_by_replacement,
replacement_pid,
&mut access.executor,
)?;
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn apply_and_resume_stopped_tracee_full_restore_set(
restore: &KboxlikeMaterializedMemoryRestore,
threads: &[KboxlikeThreadSnapshot],
kernel_states: &[KboxlikeThreadKernelStateSnapshot],
shared_dumps: &[KboxlikeSharedMemoryDump],
supervisor_pid: i32,
access_factory: &mut impl TraceeRestoreAccessFactory,
resumer: &mut impl RestoredTraceeResumer,
) -> Result<TraceeFullRestoreAndResumeSetSummary, FdError> {
let normalized = normalize_thread_group_memory_restore(restore, threads, shared_dumps)?;
let tracee_pids = validated_restore_tracee_pids(&normalized.restore, threads)?;
let restore_summary = apply_stopped_tracee_full_restore_set_for_pids(
&normalized.restore,
threads,
kernel_states,
&normalized.shared_dumps,
supervisor_pid,
tracee_pids.clone(),
access_factory,
)?;
let resume = resume_restored_tracee_set(tracee_pids, resumer)?;
Ok(TraceeFullRestoreAndResumeSetSummary {
restore: restore_summary,
resume,
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn apply_and_resume_stopped_tracee_full_restore_set_with_replacement_pids(
restore: &KboxlikeMaterializedMemoryRestore,
threads: &[KboxlikeThreadSnapshot],
kernel_states: &[KboxlikeThreadKernelStateSnapshot],
shared_dumps: &[KboxlikeSharedMemoryDump],
replacement_host_pids: &BTreeMap<ModelThreadId, i32>,
supervisor_pid: i32,
access_factory: &mut impl TraceeRestoreAccessFactory,
resumer: &mut impl RestoredTraceeResumer,
) -> Result<TraceeFullRestoreAndResumeSetSummary, FdError> {
let remapped = remap_stopped_tracee_restore_set_to_replacement_pids(
restore,
threads,
shared_dumps,
replacement_host_pids,
)?;
let remapped_kernel_states =
remap_thread_kernel_states_to_replacement_pids(kernel_states, replacement_host_pids)?;
apply_and_resume_stopped_tracee_full_restore_set(
&remapped.restore,
&remapped.threads,
&remapped_kernel_states,
&remapped.shared_dumps,
supervisor_pid,
access_factory,
resumer,
)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn remap_stopped_tracee_restore_set_to_replacement_pids(
restore: &KboxlikeMaterializedMemoryRestore,
threads: &[KboxlikeThreadSnapshot],
shared_dumps: &[KboxlikeSharedMemoryDump],
replacement_host_pids: &BTreeMap<ModelThreadId, i32>,
) -> Result<TraceeRemappedRestoreSet, FdError> {
let mut used_replacement_host_pids = BTreeSet::new();
for replacement_host_pid in replacement_host_pids.values().copied() {
if replacement_host_pid <= 0 {
return Err(FdError::TraceeInstallFailed(
"invalid replacement tracee pid",
));
}
if !used_replacement_host_pids.insert(replacement_host_pid) {
return Err(FdError::TraceeInstallFailed(
"duplicate replacement tracee pid",
));
}
}
let source_vdso_ranges = source_vdso_ranges_by_host_pid(restore);
let trace_memory_restore =
std::env::var_os("SUPERMACHINE_KBOXLIKE_TRACE_MEMORY_RESTORE").is_some();
let mut captured_host_to_model_thread = BTreeMap::new();
let mut seen_thread_model_ids = BTreeSet::new();
let mut remapped_threads = Vec::with_capacity(threads.len());
for thread in threads {
if !seen_thread_model_ids.insert(thread.model_thread_id) {
return Err(FdError::TraceeInstallFailed(
"duplicate tracee model thread id",
));
}
if captured_host_to_model_thread
.insert(thread.host_pid, thread.model_thread_id)
.is_some()
{
return Err(FdError::TraceeInstallFailed(
"duplicate captured tracee pid",
));
}
let replacement_host_pid =
replacement_host_pid_for_thread(replacement_host_pids, thread.model_thread_id)?;
let regs = remap_source_vdso_rip_to_replacement_vdso(
thread,
replacement_host_pid,
&source_vdso_ranges,
trace_memory_restore,
)?;
remapped_threads.push(KboxlikeThreadSnapshot {
host_pid: replacement_host_pid,
model_pid: thread.model_pid,
model_thread_id: thread.model_thread_id,
regs,
});
}
let mut remapped_actions = Vec::with_capacity(restore.actions.len());
for action in &restore.actions {
let model_thread_id = captured_host_to_model_thread_id(
&captured_host_to_model_thread,
materialized_action_host_pid(action),
)?;
let replacement_host_pid =
replacement_host_pid_for_thread(replacement_host_pids, model_thread_id)?;
remapped_actions.push(remap_materialized_action_host_pid(
action,
replacement_host_pid,
));
}
let mut remapped_shared_dumps = Vec::with_capacity(shared_dumps.len());
for dump in shared_dumps {
let model_thread_id =
captured_host_to_model_thread_id(&captured_host_to_model_thread, dump.host_pid)?;
let replacement_host_pid =
replacement_host_pid_for_thread(replacement_host_pids, model_thread_id)?;
let mut remapped_dump = dump.clone();
remapped_dump.host_pid = replacement_host_pid;
remapped_shared_dumps.push(remapped_dump);
}
let summary = TraceeReplacementPidRemapSummary {
threads_remapped: remapped_threads.len(),
memory_actions_remapped: remapped_actions.len(),
shared_dumps_remapped: remapped_shared_dumps.len(),
};
Ok(TraceeRemappedRestoreSet {
restore: KboxlikeMaterializedMemoryRestore {
actions: remapped_actions,
},
threads: remapped_threads,
shared_dumps: remapped_shared_dumps,
summary,
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn source_vdso_ranges_by_host_pid(
restore: &KboxlikeMaterializedMemoryRestore,
) -> BTreeMap<i32, (u64, u64)> {
let mut ranges = BTreeMap::new();
for action in &restore.actions {
if let KboxlikeMaterializedMemoryRestoreAction::SkipKernelSynthetic {
host_pid,
start,
len,
label,
..
} = action
{
if label == "[vdso]" {
ranges.insert(*host_pid, (*start, (*start).saturating_add(*len)));
}
}
}
ranges
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn remap_source_vdso_rip_to_replacement_vdso(
thread: &KboxlikeThreadSnapshot,
replacement_host_pid: i32,
source_vdso_ranges: &BTreeMap<i32, (u64, u64)>,
trace_memory_restore: bool,
) -> Result<libc::user_regs_struct, FdError> {
let mut regs = thread.regs;
let Some((source_start, source_end)) = source_vdso_ranges.get(&thread.host_pid).copied() else {
return Ok(regs);
};
if regs.rip < source_start || regs.rip >= source_end {
return Ok(regs);
}
let offset = regs.rip - source_start;
let (replacement_start, replacement_end) = replacement_vdso_range(replacement_host_pid)?
.ok_or(FdError::TraceeInstallFailed("replacement vdso missing"))?;
if replacement_start.saturating_add(offset) >= replacement_end {
return Err(FdError::TraceeInstallFailed(
"replacement vdso shorter than source rip offset",
));
}
let old_rip = regs.rip;
regs.rip = replacement_start + offset;
if trace_memory_restore {
eprintln!(
"KBOXLIVE_VDSO_RIP_REMAP captured_pid={} replacement_pid={} old_rip=0x{:x} new_rip=0x{:x} offset=0x{:x}",
thread.host_pid, replacement_host_pid, old_rip, regs.rip, offset,
);
}
Ok(regs)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn replacement_vdso_range(host_pid: i32) -> Result<Option<(u64, u64)>, FdError> {
Ok(read_proc_maps_for_pid(host_pid)?
.into_iter()
.find(|entry| entry.path.as_deref() == Some("[vdso]"))
.map(|entry| (entry.start, entry.end)))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn remap_thread_kernel_states_to_replacement_pids(
states: &[KboxlikeThreadKernelStateSnapshot],
replacement_host_pids: &BTreeMap<ModelThreadId, i32>,
) -> Result<Vec<KboxlikeThreadKernelStateSnapshot>, FdError> {
let mut seen_model_thread_ids = BTreeSet::new();
let mut remapped = Vec::with_capacity(states.len());
for state in states {
if !seen_model_thread_ids.insert(state.model_thread_id) {
return Err(FdError::TraceeInstallFailed(
"duplicate kernel state model thread id",
));
}
let replacement_host_pid =
replacement_host_pid_for_thread(replacement_host_pids, state.model_thread_id)?;
remapped.push(KboxlikeThreadKernelStateSnapshot {
host_pid: replacement_host_pid,
model_pid: state.model_pid,
model_thread_id: state.model_thread_id,
clear_child_tid: state.clear_child_tid,
robust_list_head: state.robust_list_head,
robust_list_len: state.robust_list_len,
rseq: state.rseq,
});
}
Ok(remapped)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn create_stopped_replacement_tracee_set(
threads: &[KboxlikeThreadSnapshot],
replacement_factory: &mut impl StoppedTraceeReplacementFactory,
) -> Result<TraceeReplacementThreadGroupCreateSet, FdError> {
if threads.is_empty() {
return Err(FdError::TraceeInstallFailed("no tracee register snapshots"));
}
let mut groups: BTreeMap<ProcessId, Vec<KboxlikeThreadSnapshot>> = BTreeMap::new();
let mut seen_model_thread_ids = BTreeSet::new();
for thread in threads {
if thread.host_pid <= 0 {
return Err(FdError::TraceeInstallFailed(
"invalid tracee register snapshot pid",
));
}
if !seen_model_thread_ids.insert(thread.model_thread_id) {
return Err(FdError::TraceeInstallFailed(
"duplicate tracee model thread id",
));
}
groups
.entry(thread.model_pid)
.or_default()
.push(thread.clone());
}
let mut replacement_host_pids = BTreeMap::new();
let mut used_replacement_host_pids = BTreeSet::new();
for (model_pid, group_threads) in groups.iter_mut() {
group_threads.sort_by_key(|thread| thread.model_thread_id);
let created = replacement_factory
.create_stopped_replacement_group(*model_pid, group_threads.as_slice())?;
validate_created_replacement_group(
group_threads,
&created,
&mut used_replacement_host_pids,
&mut replacement_host_pids,
)?;
}
Ok(TraceeReplacementThreadGroupCreateSet {
replacement_host_pids,
summary: TraceeReplacementThreadGroupCreateSummary {
process_groups: groups.len(),
tracees_requested: threads.len(),
tracees_created: threads.len(),
},
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn normalize_thread_group_memory_restore(
restore: &KboxlikeMaterializedMemoryRestore,
threads: &[KboxlikeThreadSnapshot],
shared_dumps: &[KboxlikeSharedMemoryDump],
) -> Result<TraceeThreadGroupMemoryNormalizedRestoreSet, FdError> {
if threads.is_empty() {
return Err(FdError::TraceeInstallFailed("no tracee register snapshots"));
}
let mut thread_by_host = BTreeMap::new();
let mut seen_model_thread_ids = BTreeSet::new();
let mut memory_owner_by_model = BTreeMap::new();
for thread in threads {
if thread.host_pid <= 0 {
return Err(FdError::TraceeInstallFailed(
"invalid tracee register snapshot pid",
));
}
if !seen_model_thread_ids.insert(thread.model_thread_id) {
return Err(FdError::TraceeInstallFailed(
"duplicate tracee model thread id",
));
}
if thread_by_host.insert(thread.host_pid, thread).is_some() {
return Err(FdError::TraceeInstallFailed(
"duplicate tracee register snapshot",
));
}
memory_owner_by_model
.entry(thread.model_pid)
.and_modify(|owner: &mut (ModelThreadId, i32)| {
if thread.model_thread_id < owner.0 {
*owner = (thread.model_thread_id, thread.host_pid);
}
})
.or_insert((thread.model_thread_id, thread.host_pid));
}
let mut summary = TraceeThreadGroupMemoryNormalizeSummary {
process_groups: memory_owner_by_model.len(),
..TraceeThreadGroupMemoryNormalizeSummary::default()
};
let mut seen_actions = BTreeSet::new();
let mut normalized_actions = Vec::with_capacity(restore.actions.len());
for action in &restore.actions {
let captured_host_pid = materialized_action_host_pid(action);
let captured_thread =
thread_by_host
.get(&captured_host_pid)
.ok_or(FdError::TraceeInstallFailed(
"memory restore action without tracee register snapshot",
))?;
let action_model_pid = materialized_action_model_pid(action);
if action_model_pid != captured_thread.model_pid {
return Err(FdError::TraceeInstallFailed(
"memory restore action model pid mismatch",
));
}
let owner_host_pid = memory_owner_host_pid(&memory_owner_by_model, action_model_pid)?;
let normalized_action = remap_materialized_action_host_pid(action, owner_host_pid);
if captured_host_pid != owner_host_pid {
summary.memory_actions_reassigned_to_owner += 1;
}
if seen_actions.insert(normalized_action.clone()) {
normalized_actions.push(normalized_action);
summary.memory_actions_retained += 1;
} else {
summary.duplicate_memory_actions_removed += 1;
}
}
let mut seen_shared_dumps = BTreeSet::new();
let mut normalized_shared_dumps = Vec::with_capacity(shared_dumps.len());
for dump in shared_dumps {
let captured_thread =
thread_by_host
.get(&dump.host_pid)
.ok_or(FdError::TraceeInstallFailed(
"shared memory dump without tracee register snapshot",
))?;
if dump.model_pid != captured_thread.model_pid {
return Err(FdError::TraceeInstallFailed(
"shared memory dump model pid mismatch",
));
}
let owner_host_pid = memory_owner_host_pid(&memory_owner_by_model, dump.model_pid)?;
let mut normalized_dump = dump.clone();
normalized_dump.host_pid = owner_host_pid;
if dump.host_pid != owner_host_pid {
summary.shared_dumps_reassigned_to_owner += 1;
}
if seen_shared_dumps.insert(normalized_dump.clone()) {
normalized_shared_dumps.push(normalized_dump);
summary.shared_dumps_retained += 1;
} else {
summary.duplicate_shared_dumps_removed += 1;
}
}
Ok(TraceeThreadGroupMemoryNormalizedRestoreSet {
restore: KboxlikeMaterializedMemoryRestore {
actions: normalized_actions,
},
shared_dumps: normalized_shared_dumps,
memory_owner_host_pids: memory_owner_by_model
.into_iter()
.map(|(model_pid, (_model_thread_id, host_pid))| (model_pid, host_pid))
.collect(),
summary,
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn validate_created_replacement_group(
threads: &[KboxlikeThreadSnapshot],
created: &BTreeMap<ModelThreadId, i32>,
used_replacement_host_pids: &mut BTreeSet<i32>,
replacement_host_pids: &mut BTreeMap<ModelThreadId, i32>,
) -> Result<(), FdError> {
for model_thread_id in created.keys().copied() {
if !threads
.iter()
.any(|thread| thread.model_thread_id == model_thread_id)
{
return Err(FdError::TraceeInstallFailed(
"replacement group returned unknown model thread id",
));
}
}
for thread in threads {
let replacement_host_pid =
created
.get(&thread.model_thread_id)
.copied()
.ok_or(FdError::TraceeInstallFailed(
"replacement group missing model thread id",
))?;
if replacement_host_pid <= 0 {
return Err(FdError::TraceeInstallFailed(
"invalid replacement tracee pid",
));
}
if !used_replacement_host_pids.insert(replacement_host_pid) {
return Err(FdError::TraceeInstallFailed(
"duplicate replacement tracee pid",
));
}
replacement_host_pids.insert(thread.model_thread_id, replacement_host_pid);
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn validated_restore_tracee_pids(
restore: &KboxlikeMaterializedMemoryRestore,
threads: &[KboxlikeThreadSnapshot],
) -> Result<BTreeSet<i32>, FdError> {
if threads.is_empty() {
return Err(FdError::TraceeInstallFailed("no tracee register snapshots"));
}
let mut tracee_pids = BTreeSet::new();
for thread in threads {
if thread.host_pid <= 0 {
return Err(FdError::TraceeInstallFailed(
"invalid tracee register snapshot pid",
));
}
tracee_pids.insert(thread.host_pid);
}
for action in &restore.actions {
if !tracee_pids.contains(&materialized_action_host_pid(action)) {
return Err(FdError::TraceeInstallFailed(
"memory restore action without tracee register snapshot",
));
}
}
Ok(tracee_pids)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn resume_restored_tracee_set(
tracee_pids: BTreeSet<i32>,
resumer: &mut impl RestoredTraceeResumer,
) -> Result<TraceeResumeSetSummary, FdError> {
let mut summary = TraceeResumeSetSummary::default();
for tracee_pid in tracee_pids {
resumer.resume_restored_tracee(tracee_pid)?;
summary.tracees_resumed += 1;
}
Ok(summary)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn memory_owner_host_pid(
memory_owner_by_model: &BTreeMap<ProcessId, (ModelThreadId, i32)>,
model_pid: ProcessId,
) -> Result<i32, FdError> {
memory_owner_by_model
.get(&model_pid)
.map(|(_model_thread_id, host_pid)| *host_pid)
.ok_or(FdError::TraceeInstallFailed(
"memory restore action without tracee register snapshot",
))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn apply_stopped_tracee_restore_set_for_pids(
restore: &KboxlikeMaterializedMemoryRestore,
threads: &[KboxlikeThreadSnapshot],
shared_dumps: &[KboxlikeSharedMemoryDump],
supervisor_pid: i32,
tracee_pids: BTreeSet<i32>,
access_factory: &mut impl TraceeRestoreAccessFactory,
) -> Result<TraceeRestoreSetSummary, FdError> {
let owned_shared_objects = build_owned_shared_object_registry(restore, shared_dumps)?;
let mut summary = TraceeRestoreSetSummary {
shared_objects: owned_shared_objects.summary(),
..TraceeRestoreSetSummary::default()
};
for tracee_pid in tracee_pids {
let mut access = access_factory.access_for_restore(tracee_pid)?;
let tracee_summary = apply_stopped_tracee_restore_with_shared_objects(
restore,
threads,
tracee_pid,
supervisor_pid,
owned_shared_objects.registry(),
&mut access.writer,
&mut access.executor,
&mut access.register_writer,
)?;
summary.tracees_restored += 1;
summary.memory.add_assign(tracee_summary.memory);
summary.registers.add_assign(tracee_summary.registers);
}
Ok(summary)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn apply_stopped_tracee_full_restore_set_for_pids(
restore: &KboxlikeMaterializedMemoryRestore,
threads: &[KboxlikeThreadSnapshot],
kernel_states: &[KboxlikeThreadKernelStateSnapshot],
shared_dumps: &[KboxlikeSharedMemoryDump],
supervisor_pid: i32,
tracee_pids: BTreeSet<i32>,
access_factory: &mut impl TraceeRestoreAccessFactory,
) -> Result<TraceeFullRestoreSetSummary, FdError> {
let normalized = normalize_thread_group_memory_restore(restore, threads, shared_dumps)?;
let owner_pids: BTreeSet<i32> = normalized
.memory_owner_host_pids
.values()
.copied()
.collect();
let non_owner_pids: BTreeSet<i32> = tracee_pids
.iter()
.copied()
.filter(|pid| !owner_pids.contains(pid))
.collect();
access_factory.mark_pids_already_cleared(&non_owner_pids);
let owned_shared_objects =
build_owned_shared_object_registry(&normalized.restore, &normalized.shared_dumps)?;
let mut summary = TraceeFullRestoreSetSummary {
shared_objects: owned_shared_objects.summary(),
..TraceeFullRestoreSetSummary::default()
};
for tracee_pid in tracee_pids.iter().copied() {
let mut access = access_factory.access_for_restore(tracee_pid)?;
let memory = apply_materialized_memory_restore_for_tracee_with_shared_objects(
&normalized.restore,
tracee_pid,
supervisor_pid,
owned_shared_objects.registry(),
&mut access.writer,
&mut access.executor,
)?;
let kernel_state = apply_thread_kernel_state_restore_for_tracee(
kernel_states,
tracee_pid,
&mut access.executor,
)?;
let registers = apply_thread_register_restore_for_tracee(
threads,
tracee_pid,
&mut access.register_writer,
)?;
summary.tracees_restored += 1;
summary.memory.add_assign(memory);
summary.kernel_state.add_assign(kernel_state);
summary.registers.add_assign(registers);
}
if kernel_states.iter().any(|state| state.rseq.is_some()) {
for tracee_pid in tracee_pids.iter().copied() {
let mut access = access_factory.access_for_restore(tracee_pid)?;
summary.kernel_state.rseq_applied += apply_thread_rseq_restore_for_tracee(
kernel_states,
tracee_pid,
&mut access.executor,
)?;
}
}
Ok(summary)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn source_credentials_by_replacement_pid(
threads: &[KboxlikeThreadSnapshot],
replacement_host_pids: &BTreeMap<ModelThreadId, i32>,
) -> Result<BTreeMap<i32, (u32, u32)>, FdError> {
let mut credentials = BTreeMap::new();
for thread in threads {
let Some(replacement_pid) = replacement_host_pids.get(&thread.model_thread_id).copied()
else {
continue;
};
let (uid, gid) = read_proc_status_uid_gid(thread.host_pid)?;
if uid == 0 && gid == 0 {
continue;
}
credentials.insert(replacement_pid, (uid, gid));
}
Ok(credentials)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn read_proc_status_uid_gid(host_pid: i32) -> Result<(u32, u32), FdError> {
let status = std::fs::read_to_string(format!("/proc/{host_pid}/status"))
.map_err(|_| FdError::TraceeInstallFailed("read proc status credentials failed"))?;
let mut uid = None;
let mut gid = None;
for line in status.lines() {
if let Some(rest) = line.strip_prefix("Uid:") {
uid = rest
.split_whitespace()
.next()
.and_then(|raw| raw.parse::<u32>().ok());
} else if let Some(rest) = line.strip_prefix("Gid:") {
gid = rest
.split_whitespace()
.next()
.and_then(|raw| raw.parse::<u32>().ok());
}
}
match (uid, gid) {
(Some(uid), Some(gid)) => Ok((uid, gid)),
_ => Err(FdError::TraceeInstallFailed(
"proc status credentials missing uid/gid",
)),
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn apply_tracee_credentials_restore_for_tracee(
credentials_by_pid: &BTreeMap<i32, (u32, u32)>,
tracee_pid: i32,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<(), FdError> {
let Some((uid, gid)) = credentials_by_pid.get(&tracee_pid).copied() else {
return Ok(());
};
let setgroups = executor.syscall(TraceeSyscall::new3(LINUX_X86_64_SYS_SETGROUPS, 0, 0, 0))?;
if setgroups < 0 {
return Err(FdError::TraceeInstallFailed(
"restore credentials setgroups failed",
));
}
let setresgid = executor.syscall(TraceeSyscall::new3(
LINUX_X86_64_SYS_SETRESGID,
gid as u64,
gid as u64,
gid as u64,
))?;
if setresgid < 0 {
return Err(FdError::TraceeInstallFailed(
"restore credentials setresgid failed",
));
}
let setresuid = executor.syscall(TraceeSyscall::new3(
LINUX_X86_64_SYS_SETRESUID,
uid as u64,
uid as u64,
uid as u64,
))?;
if setresuid < 0 {
return Err(FdError::TraceeInstallFailed(
"restore credentials setresuid failed",
));
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn materialized_action_host_pid(action: &KboxlikeMaterializedMemoryRestoreAction) -> i32 {
match action {
KboxlikeMaterializedMemoryRestoreAction::ByteBacked { host_pid, .. }
| KboxlikeMaterializedMemoryRestoreAction::RemapCleanFile { host_pid, .. }
| KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject { host_pid, .. }
| KboxlikeMaterializedMemoryRestoreAction::SkipKernelSynthetic { host_pid, .. }
| KboxlikeMaterializedMemoryRestoreAction::SkipInaccessible { host_pid, .. } => *host_pid,
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn materialized_action_model_pid(action: &KboxlikeMaterializedMemoryRestoreAction) -> ProcessId {
match action {
KboxlikeMaterializedMemoryRestoreAction::ByteBacked { model_pid, .. }
| KboxlikeMaterializedMemoryRestoreAction::RemapCleanFile { model_pid, .. }
| KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject { model_pid, .. }
| KboxlikeMaterializedMemoryRestoreAction::SkipKernelSynthetic { model_pid, .. }
| KboxlikeMaterializedMemoryRestoreAction::SkipInaccessible { model_pid, .. } => *model_pid,
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn replacement_host_pid_for_thread(
replacement_host_pids: &BTreeMap<ModelThreadId, i32>,
model_thread_id: ModelThreadId,
) -> Result<i32, FdError> {
replacement_host_pids
.get(&model_thread_id)
.copied()
.ok_or(FdError::TraceeInstallFailed(
"missing replacement tracee pid",
))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn captured_host_to_model_thread_id(
captured_host_to_model_thread: &BTreeMap<i32, ModelThreadId>,
captured_host_pid: i32,
) -> Result<ModelThreadId, FdError> {
captured_host_to_model_thread
.get(&captured_host_pid)
.copied()
.ok_or(FdError::TraceeInstallFailed(
"restore action without captured tracee thread",
))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn remap_materialized_action_host_pid(
action: &KboxlikeMaterializedMemoryRestoreAction,
replacement_host_pid: i32,
) -> KboxlikeMaterializedMemoryRestoreAction {
match action {
KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
model_pid,
start,
prot,
bytes,
source,
..
} => KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: replacement_host_pid,
model_pid: *model_pid,
start: *start,
prot: *prot,
bytes: bytes.clone(),
source: source.clone(),
},
KboxlikeMaterializedMemoryRestoreAction::RemapCleanFile {
model_pid,
start,
len,
prot,
path,
file_offset,
dev,
inode,
..
} => KboxlikeMaterializedMemoryRestoreAction::RemapCleanFile {
host_pid: replacement_host_pid,
model_pid: *model_pid,
start: *start,
len: *len,
prot: *prot,
path: path.clone(),
file_offset: *file_offset,
dev: dev.clone(),
inode: *inode,
},
KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject {
model_pid,
start,
len,
prot,
kind,
path,
file_offset,
dev,
inode,
..
} => KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject {
host_pid: replacement_host_pid,
model_pid: *model_pid,
start: *start,
len: *len,
prot: *prot,
kind: *kind,
path: path.clone(),
file_offset: *file_offset,
dev: dev.clone(),
inode: *inode,
},
KboxlikeMaterializedMemoryRestoreAction::SkipKernelSynthetic {
model_pid,
start,
len,
label,
..
} => KboxlikeMaterializedMemoryRestoreAction::SkipKernelSynthetic {
host_pid: replacement_host_pid,
model_pid: *model_pid,
start: *start,
len: *len,
label: label.clone(),
},
KboxlikeMaterializedMemoryRestoreAction::SkipInaccessible {
model_pid,
start,
len,
prot,
..
} => KboxlikeMaterializedMemoryRestoreAction::SkipInaccessible {
host_pid: replacement_host_pid,
model_pid: *model_pid,
start: *start,
len: *len,
prot: *prot,
},
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct TraceeOperationInstallRequest {
pub(crate) model_pid: ProcessId,
pub(crate) tracee_pid: i32,
pub(crate) fd: FdNumber,
pub(crate) operation: FdOperation,
}
impl TraceeOperationInstallRequest {
pub(crate) fn new(
model_pid: ProcessId,
tracee_pid: i32,
fd: FdNumber,
operation: FdOperation,
) -> Result<Self, FdError> {
if tracee_pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
Ok(Self {
model_pid,
tracee_pid,
fd,
operation,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct TraceeScratchMapping {
pub(crate) addr: u64,
pub(crate) len: usize,
}
impl TraceeScratchMapping {
pub(crate) fn map(
len: usize,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<Self, FdError> {
if len == 0 {
return Err(FdError::TraceeInstallFailed(
"tracee scratch length is zero",
));
}
let addr = executor.syscall(Self::mmap_syscall(len))?;
if addr < 0 {
return Err(FdError::TraceeInstallFailed("tracee mmap failed"));
}
if addr == 0 {
return Err(FdError::TraceeInstallFailed("tracee mmap returned null"));
}
Ok(Self {
addr: addr as u64,
len,
})
}
pub(crate) fn unmap(self, executor: &mut impl TraceeSyscallExecutor) -> Result<(), FdError> {
let ret = executor.syscall(self.munmap_syscall())?;
if ret < 0 {
return Err(FdError::TraceeInstallFailed("tracee munmap failed"));
}
Ok(())
}
pub(crate) fn mmap_syscall(len: usize) -> TraceeSyscall {
TraceeSyscall::new6(
LINUX_X86_64_SYS_MMAP,
[
0,
len as u64,
LINUX_PROT_READ | LINUX_PROT_WRITE,
LINUX_MAP_PRIVATE | LINUX_MAP_ANONYMOUS,
(-1i64) as u64,
0,
],
)
}
pub(crate) fn munmap_syscall(&self) -> TraceeSyscall {
TraceeSyscall::new6(
LINUX_X86_64_SYS_MUNMAP,
[self.addr, self.len as u64, 0, 0, 0, 0],
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct ProcessVmMemoryWriter {
pid: i32,
}
impl ProcessVmMemoryWriter {
pub(crate) fn new(pid: i32) -> Result<Self, FdError> {
if pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
Ok(Self { pid })
}
pub(crate) fn pid(&self) -> i32 {
self.pid
}
}
#[cfg(target_os = "linux")]
impl TraceeMemoryWriter for ProcessVmMemoryWriter {
fn write_bytes(&mut self, addr: u64, bytes: &[u8]) -> Result<(), FdError> {
if addr == 0 {
return Err(FdError::TraceeInstallFailed(
"tracee memory address is null",
));
}
if bytes.is_empty() {
return Ok(());
}
let local = libc::iovec {
iov_base: bytes.as_ptr() as *mut libc::c_void,
iov_len: bytes.len(),
};
let remote = libc::iovec {
iov_base: addr as *mut libc::c_void,
iov_len: bytes.len(),
};
let written = unsafe { libc::process_vm_writev(self.pid, &local, 1, &remote, 1, 0) };
if written < 0 {
return Err(FdError::TraceeInstallFailed("process_vm_writev failed"));
}
if written as usize != bytes.len() {
return Err(FdError::TraceeInstallFailed(
"process_vm_writev partial write",
));
}
Ok(())
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct PtraceSyscallExecutor {
pid: i32,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl PtraceSyscallExecutor {
pub(crate) fn new(pid: i32) -> Result<Self, FdError> {
if pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
Ok(Self { pid })
}
pub(crate) fn pid(&self) -> i32 {
self.pid
}
fn getregs(&self) -> Result<libc::user_regs_struct, FdError> {
let mut regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
let ret = unsafe {
libc::ptrace(
libc::PTRACE_GETREGS,
self.pid,
std::ptr::null_mut::<c_void>(),
&mut regs as *mut _ as *mut c_void,
)
};
if ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace getregs failed"));
}
Ok(regs)
}
fn setregs(&self, regs: &libc::user_regs_struct) -> Result<(), FdError> {
let ret = unsafe {
libc::ptrace(
libc::PTRACE_SETREGS,
self.pid,
std::ptr::null_mut::<c_void>(),
regs as *const _ as *mut c_void,
)
};
if ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace setregs failed"));
}
Ok(())
}
fn peek_word(&self, addr: u64) -> Result<u64, FdError> {
unsafe {
*libc::__errno_location() = 0;
}
let word = unsafe {
libc::ptrace(
libc::PTRACE_PEEKTEXT,
self.pid,
addr as *mut c_void,
std::ptr::null_mut::<c_void>(),
)
};
let errno = unsafe { *libc::__errno_location() };
if word == -1 && errno != 0 {
return Err(FdError::TraceeInstallFailed("ptrace peektext failed"));
}
Ok(word as u64)
}
fn poke_word(&self, addr: u64, word: u64) -> Result<(), FdError> {
let ret = unsafe {
libc::ptrace(
libc::PTRACE_POKETEXT,
self.pid,
addr as *mut c_void,
word as usize as *mut c_void,
)
};
if ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace poketext failed"));
}
Ok(())
}
fn continue_tracee(&self) -> Result<(), FdError> {
let ret = unsafe {
libc::ptrace(
libc::PTRACE_CONT,
self.pid,
std::ptr::null_mut::<c_void>(),
std::ptr::null_mut::<c_void>(),
)
};
if ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace cont failed"));
}
Ok(())
}
fn resume_to_syscall_stop(&self) -> Result<(), FdError> {
let ret = unsafe {
libc::ptrace(
libc::PTRACE_SYSCALL,
self.pid,
std::ptr::null_mut::<c_void>(),
std::ptr::null_mut::<c_void>(),
)
};
if ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace syscall resume failed"));
}
Ok(())
}
fn wait_for_ptrace_syscall_stop(&self) -> Result<(), FdError> {
loop {
let status = waitpid_tracee_blocking(self.pid)?;
if libc::WIFSTOPPED(status) {
let signal = libc::WSTOPSIG(status);
if signal == (libc::SIGTRAP | 0x80) || signal == libc::SIGTRAP {
return Ok(());
}
if signal == libc::SIGURG {
self.resume_to_syscall_stop()?;
continue;
}
let reason = match signal {
libc::SIGSEGV => "tracee stopped with SIGSEGV during remote syscall",
libc::SIGBUS => "tracee stopped with SIGBUS during remote syscall",
libc::SIGILL => "tracee stopped with SIGILL during remote syscall",
libc::SIGABRT => "tracee stopped with SIGABRT during remote syscall",
libc::SIGSYS => "tracee stopped with SIGSYS during remote syscall",
_ => "tracee stopped with unexpected signal during remote syscall",
};
self.log_signal_info("ptrace-syscall", signal);
return Err(FdError::TraceeInstallFailed(reason));
}
return Err(FdError::TraceeInstallFailed(
"tracee exited during remote syscall",
));
}
}
fn wait_for_syscall_trap(&self) -> Result<(), FdError> {
loop {
let status = waitpid_tracee_blocking(self.pid)?;
if libc::WIFSTOPPED(status) {
let signal = libc::WSTOPSIG(status);
if signal == libc::SIGTRAP {
return Ok(());
}
if signal == libc::SIGURG {
self.continue_tracee()?;
continue;
}
let reason = match signal {
libc::SIGSEGV => "tracee stopped with SIGSEGV during remote syscall",
libc::SIGBUS => "tracee stopped with SIGBUS during remote syscall",
libc::SIGILL => "tracee stopped with SIGILL during remote syscall",
libc::SIGABRT => "tracee stopped with SIGABRT during remote syscall",
libc::SIGSYS => "tracee stopped with SIGSYS during remote syscall",
_ => "tracee stopped with unexpected signal during remote syscall",
};
self.log_signal_info("int3-syscall", signal);
return Err(FdError::TraceeInstallFailed(reason));
}
return Err(FdError::TraceeInstallFailed(
"tracee exited during remote syscall",
));
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn waitpid_tracee_blocking(pid: i32) -> Result<i32, FdError> {
waitpid_tracee_blocking_with(pid, |status| unsafe {
libc::waitpid(pid, status, libc::__WALL)
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn waitpid_tracee_blocking_with<F>(pid: i32, mut wait: F) -> Result<i32, FdError>
where
F: FnMut(*mut i32) -> i32,
{
if pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
loop {
let mut status = 0;
let waited = wait(&mut status);
if waited == pid {
return Ok(status);
}
if waited < 0 {
let errno = std::io::Error::last_os_error().raw_os_error();
if errno == Some(libc::EINTR) {
continue;
}
return Err(FdError::TraceeInstallFailed(match errno {
Some(libc::ECHILD) => "waitpid failed: ECHILD",
Some(libc::EINVAL) => "waitpid failed: EINVAL",
Some(libc::ESRCH) => "waitpid failed: ESRCH",
_ => "waitpid failed",
}));
}
return Err(FdError::TraceeInstallFailed(
"waitpid returned unexpected pid",
));
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeSyscallExecutor for PtraceSyscallExecutor {
fn syscall(&mut self, syscall: TraceeSyscall) -> Result<i64, FdError> {
self.syscall_from_current_rip(syscall)
}
fn tracee_pid(&self) -> Option<i32> {
Some(self.pid())
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl PtraceSyscallExecutor {
fn syscall_from_current_rip(&self, syscall: TraceeSyscall) -> Result<i64, FdError> {
let saved_regs = self.getregs()?;
let rip = saved_regs.rip;
let saved_word = self.peek_word(rip)?;
let injected_word = (saved_word & !0x00ff_ffff) | 0x00cc_050f;
self.poke_word(rip, injected_word)?;
let mut syscall_regs = saved_regs;
syscall_regs.rax = syscall.nr as u64;
syscall_regs.rdi = syscall.args[0];
syscall_regs.rsi = syscall.args[1];
syscall_regs.rdx = syscall.args[2];
syscall_regs.r10 = syscall.args[3];
syscall_regs.r8 = syscall.args[4];
syscall_regs.r9 = syscall.args[5];
let syscall_result = (|| {
self.setregs(&syscall_regs)?;
self.continue_tracee()?;
self.wait_for_syscall_trap()?;
let result_regs = self.getregs()?;
Ok(result_regs.rax as i64)
})();
let restore_word_result = self.poke_word(rip, saved_word);
let restore_regs_result = self.setregs(&saved_regs);
match (syscall_result, restore_word_result, restore_regs_result) {
(Err(err), _, _) => Err(err),
(Ok(_), Err(err), _) => Err(err),
(Ok(_), Ok(()), Err(err)) => Err(err),
(Ok(ret), Ok(()), Ok(())) => Ok(ret),
}
}
fn syscall_from_addr(&self, syscall_addr: u64, syscall: TraceeSyscall) -> Result<i64, FdError> {
self.syscall_from_addr_with_stack(syscall_addr, None, syscall)
}
fn syscall_from_addr_with_stack(
&self,
syscall_addr: u64,
stack_top: Option<u64>,
syscall: TraceeSyscall,
) -> Result<i64, FdError> {
if syscall_addr == 0 {
return Err(FdError::TraceeInstallFailed("syscall page address is null"));
}
let saved_regs = self.getregs()?;
let mut syscall_regs = saved_regs;
syscall_regs.rip = syscall_addr;
syscall_regs.rax = syscall.nr as u64;
syscall_regs.rdi = syscall.args[0];
syscall_regs.rsi = syscall.args[1];
syscall_regs.rdx = syscall.args[2];
syscall_regs.r10 = syscall.args[3];
syscall_regs.r8 = syscall.args[4];
syscall_regs.r9 = syscall.args[5];
if let Some(stack_top) = stack_top {
syscall_regs.rsp = stack_top;
}
let syscall_result = (|| {
self.setregs(&syscall_regs)?;
self.continue_tracee()?;
self.wait_for_syscall_trap()?;
let result_regs = self.getregs()?;
Ok(result_regs.rax as i64)
})();
let restore_regs_result = self.setregs(&saved_regs);
match (syscall_result, restore_regs_result) {
(Err(err), _) => Err(err),
(Ok(_), Err(err)) => Err(err),
(Ok(ret), Ok(())) => Ok(ret),
}
}
fn syscall_from_addr_stopping_at_exit(
&self,
syscall_addr: u64,
stack_top: Option<u64>,
syscall: TraceeSyscall,
) -> Result<i64, FdError> {
if syscall_addr == 0 {
return Err(FdError::TraceeInstallFailed("syscall page address is null"));
}
let saved_regs = self.getregs()?;
let mut syscall_regs = saved_regs;
syscall_regs.rip = syscall_addr;
syscall_regs.rax = syscall.nr as u64;
syscall_regs.rdi = syscall.args[0];
syscall_regs.rsi = syscall.args[1];
syscall_regs.rdx = syscall.args[2];
syscall_regs.r10 = syscall.args[3];
syscall_regs.r8 = syscall.args[4];
syscall_regs.r9 = syscall.args[5];
if let Some(stack_top) = stack_top {
syscall_regs.rsp = stack_top;
}
let syscall_result = (|| {
self.setregs(&syscall_regs)?;
self.resume_to_syscall_stop()?;
self.wait_for_ptrace_syscall_stop()?;
self.resume_to_syscall_stop()?;
self.wait_for_ptrace_syscall_stop()?;
let result_regs = self.getregs()?;
Ok(result_regs.rax as i64)
})();
let restore_regs_result = self.setregs(&saved_regs);
match (syscall_result, restore_regs_result) {
(Err(err), _) => Err(err),
(Ok(_), Err(err)) => Err(err),
(Ok(ret), Ok(())) => Ok(ret),
}
}
fn log_signal_info(&self, context: &str, signal: i32) {
if std::env::var_os("SUPERMACHINE_KBOXLIKE_TRACE_REMOTE_SYSCALLS").is_none() {
return;
}
let mut siginfo: libc::siginfo_t = unsafe { std::mem::zeroed() };
let ret = unsafe {
libc::ptrace(
libc::PTRACE_GETSIGINFO,
self.pid,
std::ptr::null_mut::<c_void>(),
&mut siginfo as *mut _ as *mut c_void,
)
};
if ret < 0 {
eprintln!(
"KBOXLIVE_REMOTE_SIGNAL pid={} context={context} signal={} siginfo=unavailable",
self.pid, signal,
);
return;
}
let addr = unsafe { siginfo.si_addr() as usize };
eprintln!(
"KBOXLIVE_REMOTE_SIGNAL pid={} context={context} signal={} signo={} code={} addr=0x{:x}",
self.pid, signal, siginfo.si_signo, siginfo.si_code, addr,
);
log_remote_proc_maps(self.pid);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn log_remote_proc_maps(pid: i32) {
if let Ok(maps) = std::fs::read_to_string(format!("/proc/{pid}/maps")) {
eprintln!("KBOXLIVE_REMOTE_MAPS_BEGIN pid={pid}");
for line in maps.lines() {
eprintln!("KBOXLIVE_REMOTE_MAP pid={pid} {line}");
}
eprintln!("KBOXLIVE_REMOTE_MAPS_END pid={pid}");
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct PtraceScratchSyscallExecutor {
inner: PtraceSyscallExecutor,
syscall_addr: u64,
stack_top: u64,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl PtraceScratchSyscallExecutor {
pub(crate) fn new(pid: i32, syscall_addr: u64) -> Result<Self, FdError> {
if syscall_addr == 0 {
return Err(FdError::TraceeInstallFailed("syscall page address is null"));
}
if syscall_addr % 4096 != 0 {
return Err(FdError::TraceeInstallFailed(
"syscall page address is not page aligned",
));
}
let inner = PtraceSyscallExecutor::new(pid)?;
let mapped = inner.syscall_from_current_rip(TraceeSyscall::new6(
LINUX_X86_64_SYS_MMAP,
[
syscall_addr,
8192,
LINUX_PROT_READ | LINUX_PROT_WRITE | LINUX_PROT_EXEC,
LINUX_MAP_PRIVATE | LINUX_MAP_ANONYMOUS | LINUX_MAP_FIXED_NOREPLACE,
(-1i64) as u64,
0,
],
))?;
if mapped < 0 && mapped != -(libc::EEXIST as i64) {
return Err(FdError::TraceeInstallFailed(
"tracee syscall scratch mmap failed",
));
}
if mapped == -(libc::EEXIST as i64)
&& !tracee_mapping_has_execute(inner.pid(), syscall_addr, 8192)
{
let protected = inner.syscall_from_current_rip(TraceeSyscall::new3(
LINUX_X86_64_SYS_MPROTECT,
syscall_addr,
8192,
LINUX_PROT_READ | LINUX_PROT_WRITE | LINUX_PROT_EXEC,
))?;
if protected < 0 {
return Err(FdError::TraceeInstallFailed(
"tracee syscall scratch mprotect failed",
));
}
}
if mapped >= 0 && mapped as u64 != syscall_addr {
return Err(FdError::TraceeInstallFailed(
"tracee syscall scratch mmap returned wrong address",
));
}
inner.poke_word(syscall_addr, 0x00cc_050f)?;
set_ptrace_tracesysgood(pid)?;
Ok(Self {
inner,
syscall_addr,
stack_top: syscall_addr + 8192 - 16,
})
}
pub(crate) fn pid(&self) -> i32 {
self.inner.pid()
}
pub(crate) fn unmap_existing_mappings_except_scratch(&mut self) -> Result<usize, FdError> {
const HIGH_ASLR_REGION_START: u64 = 0x7000_0000_0000;
let maps = std::fs::read_to_string(format!("/proc/{}/maps", self.inner.pid()))
.map_err(|_| FdError::TraceeInstallFailed("failed to read tracee maps"))?;
let scratch_start = self.syscall_addr;
let scratch_len = 8192;
let mut unmapped = 0usize;
for line in maps.lines() {
if line.contains("[vvar]") || line.contains("[vdso]") || line.contains("[vsyscall]") {
continue;
}
let Some((start, end)) = parse_proc_maps_range(line) else {
continue;
};
if start >= end || ranges_overlap_u64(start, end - start, scratch_start, scratch_len) {
continue;
}
if start >= HIGH_ASLR_REGION_START {
continue;
}
let ret = self.syscall(TraceeSyscall::new3(
LINUX_X86_64_SYS_MUNMAP,
start,
end - start,
0,
))?;
if ret < 0 {
return Err(FdError::TraceeInstallFailed(
"tracee inherited mapping munmap failed",
));
}
unmapped += 1;
}
Ok(unmapped)
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeSyscallExecutor for PtraceScratchSyscallExecutor {
fn syscall(&mut self, syscall: TraceeSyscall) -> Result<i64, FdError> {
let result = self.inner.syscall_from_addr_with_stack(
self.syscall_addr,
Some(self.stack_top),
syscall,
);
if let Some(trace_mode) = std::env::var_os("SUPERMACHINE_KBOXLIKE_TRACE_REMOTE_SYSCALLS") {
let trace_all = trace_mode == "all";
match &result {
Ok(ret) if *ret < 0 => {
eprintln!(
"KBOXLIVE_REMOTE_SYSCALL_NEGATIVE pid={} scratch=0x{:x} nr={} args={:?} ret={ret}",
self.inner.pid(),
self.syscall_addr,
syscall.nr,
syscall.args,
);
log_remote_proc_maps(self.inner.pid());
}
Ok(ret) if trace_all => {
eprintln!(
"KBOXLIVE_REMOTE_SYSCALL pid={} scratch=0x{:x} nr={} args={:?} ret={ret}",
self.inner.pid(),
self.syscall_addr,
syscall.nr,
syscall.args,
);
}
Err(err) => {
eprintln!(
"KBOXLIVE_REMOTE_SYSCALL_FAILURE pid={} scratch=0x{:x} nr={} args={:?} err={err:?}",
self.inner.pid(),
self.syscall_addr,
syscall.nr,
syscall.args,
);
}
_ => {}
}
}
result
}
fn tracee_pid(&self) -> Option<i32> {
Some(self.pid())
}
fn kernel_synthetic_ranges(&self) -> Result<Vec<(u64, u64)>, FdError> {
let maps = std::fs::read_to_string(format!("/proc/{}/maps", self.inner.pid()))
.map_err(|_| FdError::TraceeInstallFailed("failed to read tracee maps"))?;
let mut ranges = Vec::new();
for line in maps.lines() {
if !(line.contains("[vvar]")
|| line.contains("[vvar_vclock]")
|| line.contains("[vdso]")
|| line.contains("[vsyscall]"))
{
continue;
}
let Some((start, end)) = parse_proc_maps_range(line) else {
continue;
};
ranges.push((start, end));
}
Ok(ranges)
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct PtraceRegisterWriter {
pid: i32,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl PtraceRegisterWriter {
pub(crate) fn new(pid: i32) -> Result<Self, FdError> {
if pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
Ok(Self { pid })
}
pub(crate) fn pid(&self) -> i32 {
self.pid
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeRegisterWriter for PtraceRegisterWriter {
fn write_regs(&mut self, regs: &libc::user_regs_struct) -> Result<(), FdError> {
let ret = unsafe {
libc::ptrace(
libc::PTRACE_SETREGS,
self.pid,
std::ptr::null_mut::<c_void>(),
regs as *const _ as *mut c_void,
)
};
if ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace setregs failed"));
}
Ok(())
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Debug, Default)]
pub(crate) struct LinuxTraceeRestoreAccessFactory;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeRestoreAccessFactory for LinuxTraceeRestoreAccessFactory {
type Writer = ProcessVmMemoryWriter;
type Executor = PtraceSyscallExecutor;
type RegisterWriter = PtraceRegisterWriter;
fn access_for_restore(
&mut self,
host_pid: i32,
) -> Result<TraceeRestoreAccess<Self::Writer, Self::Executor, Self::RegisterWriter>, FdError>
{
Ok(TraceeRestoreAccess {
writer: ProcessVmMemoryWriter::new(host_pid)?,
executor: PtraceSyscallExecutor::new(host_pid)?,
register_writer: PtraceRegisterWriter::new(host_pid)?,
})
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Debug, Default)]
pub(crate) struct PtraceDetachRestoredTraceeResumer;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl RestoredTraceeResumer for PtraceDetachRestoredTraceeResumer {
fn resume_restored_tracee(&mut self, host_pid: i32) -> Result<(), FdError> {
if host_pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
let detach = |pid: i32| unsafe {
libc::ptrace(
libc::PTRACE_DETACH,
pid,
std::ptr::null_mut::<c_void>(),
std::ptr::null_mut::<c_void>(),
)
};
if detach(host_pid) >= 0 {
return Ok(());
}
if std::io::Error::last_os_error().raw_os_error() != Some(libc::ESRCH) {
return Err(FdError::TraceeInstallFailed("ptrace detach failed"));
}
for _ in 0..100 {
if unsafe { libc::kill(host_pid, libc::SIGSTOP) } < 0 {
if std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH) {
return Ok(());
}
return Err(FdError::TraceeInstallFailed("ptrace detach failed"));
}
let status = waitpid_tracee_blocking(host_pid)?;
if !libc::WIFSTOPPED(status) {
return Ok(());
}
let detach_ret = unsafe {
libc::ptrace(
libc::PTRACE_DETACH,
host_pid,
std::ptr::null_mut::<c_void>(),
libc::SIGCONT as usize as *mut c_void,
)
};
if detach_ret >= 0 {
return Ok(());
}
if std::io::Error::last_os_error().raw_os_error() != Some(libc::ESRCH) {
return Err(FdError::TraceeInstallFailed("ptrace detach failed"));
}
}
Err(FdError::TraceeInstallFailed("ptrace detach failed"))
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Debug, Default)]
pub(crate) struct LinuxStoppedTraceeReplacementFactory {
created_host_pids: Vec<i32>,
static_replacement_path: Option<PathBuf>,
syscall_page: Option<u64>,
mount_ns_pid: Option<i32>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl LinuxStoppedTraceeReplacementFactory {
pub(crate) fn with_static_replacement_path(path: impl Into<PathBuf>) -> Self {
Self {
created_host_pids: Vec::new(),
static_replacement_path: Some(path.into()),
syscall_page: None,
mount_ns_pid: None,
}
}
pub(crate) fn set_mount_ns_pid(&mut self, pid: Option<i32>) {
self.mount_ns_pid = pid;
}
pub(crate) fn created_host_pids(&self) -> &[i32] {
&self.created_host_pids
}
pub(crate) fn kill_created_tracees(&mut self) {
let mut pending: BTreeSet<i32> = self.created_host_pids.drain(..).collect();
for host_pid in pending.iter().copied() {
unsafe {
libc::kill(host_pid, libc::SIGKILL);
libc::ptrace(
libc::PTRACE_DETACH,
host_pid,
std::ptr::null_mut::<c_void>(),
libc::SIGKILL as usize as *mut c_void,
);
}
}
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
while !pending.is_empty() && std::time::Instant::now() <= deadline {
let mut progressed = false;
for host_pid in pending.iter().copied().collect::<Vec<i32>>() {
let mut status = 0;
let waited =
unsafe { libc::waitpid(host_pid, &mut status, libc::__WALL | libc::WNOHANG) };
if waited != 0 {
pending.remove(&host_pid);
progressed = true;
}
}
if !progressed {
std::thread::sleep(std::time::Duration::from_millis(1));
}
}
}
fn fork_stopped_replacement_leader(&mut self, thread_count: usize) -> Result<i32, FdError> {
if let Some(path) = self.static_replacement_path.clone() {
if std::env::var_os("SUPERMACHINE_KBOXLIKE_TRACE_MEMORY_RESTORE").is_some() {
eprintln!(
"KBOXLIVE_STATIC_REPLACEMENT path={} threads={thread_count}",
path.display(),
);
}
if thread_count == 1 {
return self.fork_stopped_static_replacement_leader(&path);
}
}
let previous_syscall_page_env = std::env::var_os(KBOXLIKE_REPLACEMENT_SYSCALL_PAGE_ENV);
if let Some(syscall_page) = self.syscall_page {
std::env::set_var(
KBOXLIKE_REPLACEMENT_SYSCALL_PAGE_ENV,
format!("0x{syscall_page:x}"),
);
} else {
std::env::remove_var(KBOXLIKE_REPLACEMENT_SYSCALL_PAGE_ENV);
}
let mount_ns_path = self.mount_ns_pid.and_then(mount_ns_path_cstring);
let child = unsafe { libc::fork() };
match previous_syscall_page_env {
Some(value) => std::env::set_var(KBOXLIKE_REPLACEMENT_SYSCALL_PAGE_ENV, value),
None => std::env::remove_var(KBOXLIKE_REPLACEMENT_SYSCALL_PAGE_ENV),
}
if child < 0 {
return Err(FdError::TraceeInstallFailed(
"fork replacement tracee failed",
));
}
if child == 0 {
replacement_group_child_main(thread_count, mount_ns_path.as_deref());
}
let mut status = 0;
let waited = unsafe { libc::waitpid(child, &mut status, 0) };
if waited != child {
unsafe {
libc::kill(child, libc::SIGKILL);
libc::waitpid(child, &mut status, 0);
}
return Err(FdError::TraceeInstallFailed(
"replacement tracee wait failed",
));
}
if !libc::WIFSTOPPED(status) || libc::WSTOPSIG(status) != libc::SIGSTOP {
unsafe {
libc::kill(child, libc::SIGKILL);
libc::waitpid(child, &mut status, 0);
}
return Err(FdError::TraceeInstallFailed(
"replacement tracee did not stop at initial SIGSTOP",
));
}
self.created_host_pids.push(child);
Ok(child)
}
fn fork_stopped_static_replacement_leader(&mut self, path: &Path) -> Result<i32, FdError> {
let mount_ns_path = self.mount_ns_pid.and_then(mount_ns_path_cstring);
let child = unsafe { libc::fork() };
if child < 0 {
return Err(FdError::TraceeInstallFailed(
"fork static replacement tracee failed",
));
}
if child == 0 {
static_replacement_child_main(path, mount_ns_path.as_deref());
}
let mut status = 0;
let waited = unsafe { libc::waitpid(child, &mut status, 0) };
if waited != child {
unsafe {
libc::kill(child, libc::SIGKILL);
libc::waitpid(child, &mut status, 0);
}
return Err(FdError::TraceeInstallFailed(
"static replacement tracee wait failed",
));
}
if !libc::WIFSTOPPED(status)
|| !matches!(libc::WSTOPSIG(status), libc::SIGTRAP | libc::SIGSTOP)
{
unsafe {
libc::kill(child, libc::SIGKILL);
libc::waitpid(child, &mut status, 0);
}
return Err(FdError::TraceeInstallFailed(
"static replacement tracee did not stop after exec",
));
}
self.created_host_pids.push(child);
Ok(child)
}
fn create_stopped_replacement_followers(
&mut self,
leader_pid: i32,
thread_count: usize,
) -> Result<Vec<i32>, FdError> {
if thread_count <= 1 {
return Ok(Vec::new());
}
set_ptrace_traceclone(leader_pid)?;
ptrace_cont(leader_pid)?;
let mut follower_tids = Vec::new();
let mut stopped_follower_tids = BTreeSet::new();
let mut leader_final_stop = false;
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while !leader_final_stop || stopped_follower_tids.len() < thread_count - 1 {
if std::time::Instant::now() > deadline {
return Err(FdError::TraceeInstallFailed(
"replacement thread bootstrap timed out",
));
}
let mut status = 0;
let mut waited = 0;
let mut candidates: Vec<i32> = Vec::with_capacity(1 + follower_tids.len());
if !leader_final_stop {
candidates.push(leader_pid);
}
candidates.extend(
follower_tids
.iter()
.copied()
.filter(|tid| !stopped_follower_tids.contains(tid)),
);
for pid in candidates {
let w = unsafe { libc::waitpid(pid, &mut status, libc::__WALL | libc::WNOHANG) };
if w < 0 {
return Err(FdError::TraceeInstallFailed(
"replacement tracee wait failed",
));
}
if w > 0 {
waited = w;
break;
}
}
if waited == 0 {
std::thread::sleep(std::time::Duration::from_millis(1));
continue;
}
if !libc::WIFSTOPPED(status) {
return Err(FdError::TraceeInstallFailed(
"replacement tracee exited during thread bootstrap",
));
}
let event = status >> LINUX_PTRACE_EVENT_SHIFT;
if waited == leader_pid && event == LINUX_PTRACE_EVENT_CLONE {
let follower_tid = ptrace_geteventmsg_pid(leader_pid)?;
if follower_tid <= 0 {
return Err(FdError::TraceeInstallFailed(
"replacement clone event returned invalid tid",
));
}
if follower_tids.len() >= thread_count - 1 {
return Err(FdError::TraceeInstallFailed(
"replacement clone event returned too many tids",
));
}
follower_tids.push(follower_tid);
self.created_host_pids.push(follower_tid);
signal_thread(leader_pid, follower_tid, libc::SIGURG)?;
ptrace_cont(leader_pid)?;
continue;
}
if waited == leader_pid && libc::WSTOPSIG(status) == libc::SIGTRAP {
leader_final_stop = true;
continue;
}
if follower_tids.contains(&waited) {
stopped_follower_tids.insert(waited);
continue;
}
return Err(FdError::TraceeInstallFailed(
"unexpected replacement tracee bootstrap stop",
));
}
Ok(follower_tids)
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl StoppedTraceeReplacementFactory for LinuxStoppedTraceeReplacementFactory {
fn prepare_syscall_page(&mut self, syscall_page: u64) -> Result<(), FdError> {
if syscall_page == 0 || syscall_page % 4096 != 0 {
return Err(FdError::TraceeInstallFailed(
"syscall page address is not page aligned",
));
}
self.syscall_page = Some(syscall_page);
Ok(())
}
fn create_stopped_replacement_group(
&mut self,
_model_pid: ProcessId,
threads: &[KboxlikeThreadSnapshot],
) -> Result<BTreeMap<ModelThreadId, i32>, FdError> {
if threads.is_empty() {
return Err(FdError::TraceeInstallFailed(
"replacement group has no threads",
));
}
let leader_pid = self.fork_stopped_replacement_leader(threads.len())?;
let follower_tids = self.create_stopped_replacement_followers(leader_pid, threads.len())?;
if follower_tids.len() + 1 != threads.len() {
return Err(FdError::TraceeInstallFailed(
"replacement group created wrong thread count",
));
}
let mut host_pids = Vec::with_capacity(threads.len());
host_pids.push(leader_pid);
host_pids.extend(follower_tids);
Ok(threads
.iter()
.zip(host_pids)
.map(|(thread, host_pid)| (thread.model_thread_id, host_pid))
.collect())
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
extern "C" fn replacement_follower_main(_arg: *mut c_void) -> i32 {
unsafe {
loop {
libc::pause();
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn mount_ns_path_cstring(pid: i32) -> Option<CString> {
CString::new(format!("/proc/{pid}/ns/mnt")).ok()
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
unsafe fn replacement_child_join_mount_ns(mount_ns_path: Option<&CStr>, exit_code: i32) {
unsafe {
let Some(path) = mount_ns_path else {
return;
};
let fd = libc::open(path.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC);
if fd < 0 {
libc::_exit(exit_code);
}
if libc::setns(fd, libc::CLONE_NEWNS) != 0 {
libc::_exit(exit_code);
}
libc::close(fd);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn replacement_group_child_main(thread_count: usize, mount_ns_path: Option<&CStr>) -> ! {
unsafe {
replacement_child_join_mount_ns(mount_ns_path, 115);
if let Some(syscall_page) = replacement_syscall_page_from_env() {
let mapped = libc::mmap(
syscall_page as *mut c_void,
8192,
libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_FIXED_NOREPLACE,
-1,
0,
);
if mapped == libc::MAP_FAILED || mapped as u64 != syscall_page {
libc::_exit(114);
}
let code = mapped as *mut u8;
*code.add(0) = 0x0f;
*code.add(1) = 0x05;
*code.add(2) = 0xcc;
}
let ret = libc::ptrace(
libc::PTRACE_TRACEME,
0,
std::ptr::null_mut::<c_void>(),
std::ptr::null_mut::<c_void>(),
);
if ret < 0 {
libc::_exit(111);
}
libc::raise(libc::SIGSTOP);
for _ in 1..thread_count {
let stack = libc::mmap(
std::ptr::null_mut(),
REPLACEMENT_FOLLOWER_STACK_LEN,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
-1,
0,
);
if stack == libc::MAP_FAILED {
libc::_exit(112);
}
let stack_top = (stack as *mut u8).add(REPLACEMENT_FOLLOWER_STACK_LEN) as *mut c_void;
let flags = LINUX_CLONE_VM
| LINUX_CLONE_FS
| LINUX_CLONE_FILES
| LINUX_CLONE_SIGHAND
| LINUX_CLONE_THREAD
| LINUX_CLONE_SYSVSEM;
let tid = libc::clone(
replacement_follower_main,
stack_top,
flags,
std::ptr::null_mut::<c_void>(),
);
if tid < 0 {
libc::_exit(113);
}
}
libc::raise(libc::SIGTRAP);
loop {
libc::pause();
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn replacement_syscall_page_from_env() -> Option<u64> {
let raw = std::env::var(KBOXLIKE_REPLACEMENT_SYSCALL_PAGE_ENV).ok()?;
let trimmed = raw.trim();
let parsed = if let Some(hex) = trimmed.strip_prefix("0x") {
u64::from_str_radix(hex, 16).ok()?
} else {
trimmed.parse::<u64>().ok()?
};
(parsed != 0 && parsed % 4096 == 0).then_some(parsed)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn static_replacement_child_main(path: &Path, mount_ns_path: Option<&CStr>) -> ! {
unsafe {
replacement_child_join_mount_ns(mount_ns_path, 124);
let ret = libc::ptrace(
libc::PTRACE_TRACEME,
0,
std::ptr::null_mut::<c_void>(),
std::ptr::null_mut::<c_void>(),
);
if ret < 0 {
libc::_exit(121);
}
let path_c = match CString::new(path.as_os_str().as_bytes()) {
Ok(path) => path,
Err(_) => libc::_exit(122),
};
let argv0 = c"busybox";
let applet = c"sleep";
let seconds = c"1000000";
let argv = [
argv0.as_ptr(),
applet.as_ptr(),
seconds.as_ptr(),
std::ptr::null(),
];
let envp = [std::ptr::null()];
libc::execve(path_c.as_ptr(), argv.as_ptr(), envp.as_ptr());
libc::_exit(123);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn set_ptrace_tracesysgood(pid: i32) -> Result<(), FdError> {
let ret = unsafe {
libc::ptrace(
libc::PTRACE_SETOPTIONS,
pid,
std::ptr::null_mut::<c_void>(),
LINUX_PTRACE_O_TRACESYSGOOD as usize as *mut c_void,
)
};
if ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace setoptions failed"));
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn parse_proc_maps_range(line: &str) -> Option<(u64, u64)> {
let range = line.split_whitespace().next()?;
let (start, end) = range.split_once('-')?;
Some((
u64::from_str_radix(start, 16).ok()?,
u64::from_str_radix(end, 16).ok()?,
))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn tracee_mapping_has_execute(pid: i32, addr: u64, len: u64) -> bool {
let Ok(maps) = std::fs::read_to_string(format!("/proc/{pid}/maps")) else {
return false;
};
maps.lines().any(|line| {
let mut fields = line.split_whitespace();
let Some(range) = fields.next() else {
return false;
};
let Some(perms) = fields.next() else {
return false;
};
let Some((start, end)) = range.split_once('-') else {
return false;
};
let Ok(start) = u64::from_str_radix(start, 16) else {
return false;
};
let Ok(end) = u64::from_str_radix(end, 16) else {
return false;
};
start <= addr && addr.saturating_add(len) <= end && perms.as_bytes().get(2) == Some(&b'x')
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn ranges_overlap_u64(start_a: u64, len_a: u64, start_b: u64, len_b: u64) -> bool {
let end_a = start_a.saturating_add(len_a);
let end_b = start_b.saturating_add(len_b);
start_a < end_b && start_b < end_a
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn set_ptrace_traceclone(pid: i32) -> Result<(), FdError> {
let ret = unsafe {
libc::ptrace(
libc::PTRACE_SETOPTIONS,
pid,
std::ptr::null_mut::<c_void>(),
LINUX_PTRACE_O_TRACECLONE as usize as *mut c_void,
)
};
if ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace setoptions failed"));
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn ptrace_cont(pid: i32) -> Result<(), FdError> {
let ret = unsafe {
libc::ptrace(
libc::PTRACE_CONT,
pid,
std::ptr::null_mut::<c_void>(),
std::ptr::null_mut::<c_void>(),
)
};
if ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace cont failed"));
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn ptrace_geteventmsg_pid(pid: i32) -> Result<i32, FdError> {
let mut event_msg = 0u64;
let ret = unsafe {
libc::ptrace(
libc::PTRACE_GETEVENTMSG,
pid,
std::ptr::null_mut::<c_void>(),
&mut event_msg as *mut _ as *mut c_void,
)
};
if ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace geteventmsg failed"));
}
if event_msg > i32::MAX as u64 {
return Err(FdError::TraceeInstallFailed(
"ptrace geteventmsg pid overflowed",
));
}
Ok(event_msg as i32)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn signal_thread(tgid: i32, tid: i32, signal: i32) -> Result<(), FdError> {
let ret = unsafe { libc::syscall(libc::SYS_tgkill, tgid, tid, signal) };
if ret < 0 {
return Err(FdError::TraceeInstallFailed("tgkill tracee signal failed"));
}
Ok(())
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct TraceeProcFdSourcePlan {
pub(crate) supervisor_pid: i32,
pub(crate) supervisor_fd: i32,
pub(crate) tracee_path_addr: u64,
}
impl TraceeProcFdSourcePlan {
pub(crate) fn new(
supervisor_pid: i32,
supervisor_fd: i32,
tracee_path_addr: u64,
) -> Result<Self, FdError> {
if supervisor_pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid supervisor pid"));
}
if supervisor_fd < 0 {
return Err(FdError::InvalidFd(supervisor_fd));
}
if tracee_path_addr == 0 {
return Err(FdError::TraceeInstallFailed(
"tracee source path address is null",
));
}
Ok(Self {
supervisor_pid,
supervisor_fd,
tracee_path_addr,
})
}
pub(crate) fn proc_fd_path(&self) -> String {
format!("/proc/{}/fd/{}", self.supervisor_pid, self.supervisor_fd)
}
pub(crate) fn path_bytes(&self) -> Vec<u8> {
let mut bytes = self.proc_fd_path().into_bytes();
bytes.push(0);
bytes
}
pub(crate) fn open_syscall(&self) -> TraceeSyscall {
self.open_syscall_with_flags(LINUX_O_CLOEXEC)
}
pub(crate) fn open_syscall_with_flags(&self, flags: u64) -> TraceeSyscall {
TraceeSyscall::new3(
LINUX_X86_64_SYS_OPENAT,
LINUX_AT_FDCWD,
self.tracee_path_addr,
flags,
)
}
pub(crate) fn write_path(&self, writer: &mut impl TraceeMemoryWriter) -> Result<(), FdError> {
writer.write_bytes(self.tracee_path_addr, &self.path_bytes())
}
pub(crate) fn open(&self, executor: &mut impl TraceeSyscallExecutor) -> Result<i32, FdError> {
self.open_with_flags(executor, LINUX_O_CLOEXEC)
}
pub(crate) fn open_with_flags(
&self,
executor: &mut impl TraceeSyscallExecutor,
flags: u64,
) -> Result<i32, FdError> {
let fd = executor.syscall(self.open_syscall_with_flags(flags))?;
if fd < 0 {
return Err(FdError::TraceeInstallFailed(
"tracee open supervisor fd failed",
));
}
if fd > i32::MAX as i64 {
return Err(FdError::TraceeInstallFailed(
"tracee open supervisor fd overflowed",
));
}
Ok(fd as i32)
}
}
impl TraceeInstallPlan {
pub(crate) fn from_shadow_install(
plan: ShadowInstallPlan,
tracee_source_fd: i32,
close_source_after_install: bool,
) -> Result<Self, FdError> {
Self::new(
tracee_source_fd,
plan.target_fd,
plan.cloexec,
close_source_after_install,
)
}
pub(crate) fn new(
source_fd: i32,
target_fd: i32,
cloexec: bool,
close_source_after_install: bool,
) -> Result<Self, FdError> {
if source_fd < 0 {
return Err(FdError::InvalidFd(source_fd));
}
if target_fd < 0 {
return Err(FdError::InvalidFd(target_fd));
}
if source_fd == target_fd {
return Err(FdError::TraceeInstallFailed(
"tracee source fd aliases target fd",
));
}
Ok(Self {
source_fd,
target_fd,
cloexec,
close_source_after_install,
})
}
pub(crate) fn syscalls(&self) -> Vec<TraceeSyscall> {
let mut calls = vec![
TraceeSyscall::new3(
LINUX_X86_64_SYS_DUP2,
self.source_fd as u64,
self.target_fd as u64,
0,
),
TraceeSyscall::new3(
LINUX_X86_64_SYS_FCNTL,
self.target_fd as u64,
LINUX_F_SETFD,
if self.cloexec { LINUX_FD_CLOEXEC } else { 0 },
),
];
if self.close_source_after_install {
calls.push(TraceeSyscall::new3(
LINUX_X86_64_SYS_CLOSE,
self.source_fd as u64,
0,
0,
));
}
calls
}
pub(crate) fn apply(&self, executor: &mut impl TraceeSyscallExecutor) -> Result<(), FdError> {
let dup_ret = executor.syscall(TraceeSyscall::new3(
LINUX_X86_64_SYS_DUP2,
self.source_fd as u64,
self.target_fd as u64,
0,
))?;
if dup_ret < 0 {
return Err(FdError::TraceeInstallFailed("tracee dup2 failed"));
}
if dup_ret != self.target_fd as i64 {
return Err(FdError::TraceeInstallFailed(
"tracee dup2 returned wrong fd",
));
}
let fcntl_ret = executor.syscall(TraceeSyscall::new3(
LINUX_X86_64_SYS_FCNTL,
self.target_fd as u64,
LINUX_F_SETFD,
if self.cloexec { LINUX_FD_CLOEXEC } else { 0 },
))?;
if fcntl_ret < 0 {
return Err(FdError::TraceeInstallFailed("tracee fcntl failed"));
}
if self.close_source_after_install {
let close_ret = executor.syscall(TraceeSyscall::new3(
LINUX_X86_64_SYS_CLOSE,
self.source_fd as u64,
0,
0,
))?;
if close_ret < 0 {
return Err(FdError::TraceeInstallFailed("tracee close failed"));
}
}
Ok(())
}
}
pub(crate) fn apply_proc_fd_shadow_install(
source: &TraceeProcFdSourcePlan,
shadow_install: ShadowInstallPlan,
writer: &mut impl TraceeMemoryWriter,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<(), FdError> {
source.write_path(writer)?;
let source_fd = source.open(executor)?;
if source_fd == shadow_install.target_fd {
let fcntl_ret = executor.syscall(TraceeSyscall::new3(
LINUX_X86_64_SYS_FCNTL,
shadow_install.target_fd as u64,
LINUX_F_SETFD,
if shadow_install.cloexec {
LINUX_FD_CLOEXEC
} else {
0
},
))?;
if fcntl_ret < 0 {
return Err(FdError::TraceeInstallFailed("tracee fcntl failed"));
}
return Ok(());
}
let plan = TraceeInstallPlan::from_shadow_install(shadow_install, source_fd, true)?;
plan.apply(executor)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn apply_tracee_mm_exe_file_with_scratch(
exe_path: &Path,
writer: &mut impl TraceeMemoryWriter,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<(), FdError> {
let path_bytes = nul_terminated_path_bytes(exe_path)?;
let scratch = TraceeScratchMapping::map(path_bytes.len(), executor)?;
let install_result = (|| {
writer.write_bytes(scratch.addr, &path_bytes)?;
let exe_fd = executor.syscall(TraceeSyscall::new3(
LINUX_X86_64_SYS_OPENAT,
LINUX_AT_FDCWD,
scratch.addr,
LINUX_O_CLOEXEC,
))?;
if exe_fd < 0 {
return Err(FdError::TraceeInstallFailed("tracee open exe file failed"));
}
let prctl_result = executor.syscall(TraceeSyscall::new6(
LINUX_X86_64_SYS_PRCTL,
[
LINUX_PR_SET_MM,
LINUX_PR_SET_MM_EXE_FILE,
exe_fd as u64,
0,
0,
0,
],
));
let close_result = executor.syscall(TraceeSyscall::new3(
LINUX_X86_64_SYS_CLOSE,
exe_fd as u64,
0,
0,
));
let prctl_ret = prctl_result?;
if prctl_ret < 0 {
const EBUSY: i64 = -16;
if prctl_ret != EBUSY {
return Err(FdError::TraceeInstallFailed(
"tracee prctl set mm exe file failed",
));
}
}
if close_result? < 0 {
return Err(FdError::TraceeInstallFailed("tracee close exe file failed"));
}
Ok(())
})();
let unmap_result = scratch.unmap(executor);
match (install_result, unmap_result) {
(Err(err), _) => Err(err),
(Ok(()), Err(err)) => Err(err),
(Ok(()), Ok(())) => Ok(()),
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn nul_terminated_path_bytes(path: &Path) -> Result<Vec<u8>, FdError> {
let bytes = path.as_os_str().as_bytes();
if bytes.is_empty() || bytes.contains(&0) {
return Err(FdError::TraceeInstallFailed("invalid tracee path bytes"));
}
let mut out = bytes.to_vec();
out.push(0);
Ok(out)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct TraceeEndpointInstall {
source_fd: i32,
target_fd: i32,
cloexec: bool,
}
pub(crate) fn apply_tracee_socketpair_install_with_scratch(
first: ShadowInstallPlan,
second: ShadowInstallPlan,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<(), FdError> {
let scratch = TraceeScratchMapping::map(std::mem::size_of::<[i32; 2]>(), executor)?;
let install_result = (|| {
let socketpair_ret = executor.syscall(TraceeSyscall::new6(
LINUX_X86_64_SYS_SOCKETPAIR,
[LINUX_AF_UNIX, LINUX_SOCK_STREAM, 0, scratch.addr, 0, 0],
))?;
if socketpair_ret < 0 {
return Err(FdError::TraceeInstallFailed("tracee socketpair failed"));
}
let pid = executor
.tracee_pid()
.ok_or(FdError::TraceeInstallFailed("missing tracee pid"))?;
let [first_source_fd, second_source_fd] = read_tracee_i32_pair(pid, scratch.addr)?;
apply_tracee_socketpair_endpoint_installs(
TraceeEndpointInstall {
source_fd: first_source_fd,
target_fd: first.target_fd,
cloexec: first.cloexec,
},
TraceeEndpointInstall {
source_fd: second_source_fd,
target_fd: second.target_fd,
cloexec: second.cloexec,
},
executor,
)
})();
let unmap_result = scratch.unmap(executor);
match (install_result, unmap_result) {
(Err(err), _) => Err(err),
(Ok(()), Err(err)) => Err(err),
(Ok(()), Ok(())) => Ok(()),
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn apply_supervisor_fd_scm_rights_install_with_scratch(
supervisor_fd: i32,
shadow_install: ShadowInstallPlan,
writer: &mut impl TraceeMemoryWriter,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<(), FdError> {
if supervisor_fd < 0 {
return Err(FdError::InvalidFd(supervisor_fd));
}
let scratch = TraceeScratchMapping::map(512, executor)?;
let install_result = (|| {
let socketpair_ret = executor.syscall(TraceeSyscall::new6(
LINUX_X86_64_SYS_SOCKETPAIR,
[LINUX_AF_UNIX, LINUX_SOCK_STREAM, 0, scratch.addr, 0, 0],
))?;
if socketpair_ret < 0 {
return Err(FdError::TraceeInstallFailed("tracee scm socketpair failed"));
}
let pid = executor
.tracee_pid()
.ok_or(FdError::TraceeInstallFailed("missing tracee pid"))?;
let [tracee_send_fd, tracee_recv_fd] = read_tracee_i32_pair(pid, scratch.addr)?;
let supervisor_control_fd = duplicate_tracee_fd_to_supervisor(pid, tracee_send_fd)?;
let send_result = send_fd_over_unix_socket(supervisor_control_fd, supervisor_fd);
let close_control_result = close_host_fd(supervisor_control_fd);
send_result?;
close_control_result?;
let recv_fd = recv_supervisor_fd_in_tracee(tracee_recv_fd, scratch.addr, writer, executor)?;
let close_send_result = close_tracee_fd(tracee_send_fd, executor);
let install_result = apply_tracee_fd_install(
recv_fd,
shadow_install.target_fd,
shadow_install.cloexec,
true,
executor,
);
let close_recv_result = if tracee_recv_fd == shadow_install.target_fd {
Ok(())
} else {
close_tracee_fd(tracee_recv_fd, executor)
};
close_send_result?;
install_result?;
close_recv_result
})();
let unmap_result = scratch.unmap(executor);
match (install_result, unmap_result) {
(Err(err), _) => Err(err),
(Ok(()), Err(err)) => Err(err),
(Ok(()), Ok(())) => Ok(()),
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn recv_supervisor_fd_in_tracee(
tracee_recv_fd: i32,
scratch_addr: u64,
writer: &mut impl TraceeMemoryWriter,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<i32, FdError> {
let data_addr = scratch_addr + 64;
let iov_addr = scratch_addr + 80;
let control_addr = scratch_addr + 128;
let msghdr_addr = scratch_addr + 256;
let data = [0u8; 1];
writer.write_bytes(data_addr, &data)?;
let iov = libc::iovec {
iov_base: data_addr as *mut c_void,
iov_len: data.len(),
};
writer.write_bytes(iov_addr, plain_old_bytes(&iov))?;
let control = [0u8; 64];
writer.write_bytes(control_addr, &control)?;
let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
msg.msg_iov = iov_addr as *mut libc::iovec;
msg.msg_iovlen = 1;
msg.msg_control = control_addr as *mut c_void;
msg.msg_controllen = control.len();
writer.write_bytes(msghdr_addr, plain_old_bytes(&msg))?;
let recv_ret = executor.syscall(TraceeSyscall::new3(
LINUX_X86_64_SYS_RECVMSG,
tracee_recv_fd as u64,
msghdr_addr,
0,
))?;
if recv_ret < 0 {
return Err(FdError::TraceeInstallFailed("tracee recvmsg failed"));
}
let pid = executor
.tracee_pid()
.ok_or(FdError::TraceeInstallFailed("missing tracee pid"))?;
read_tracee_i32(
pid,
control_addr + std::mem::size_of::<libc::cmsghdr>() as u64,
)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn duplicate_tracee_fd_to_supervisor(pid: i32, tracee_fd: i32) -> Result<i32, FdError> {
const SYS_PIDFD_OPEN: libc::c_long = 434;
const SYS_PIDFD_GETFD: libc::c_long = 438;
let pidfd = unsafe { libc::syscall(SYS_PIDFD_OPEN, pid, 0) };
if pidfd < 0 || pidfd > i32::MAX as libc::c_long {
return Err(FdError::TraceeInstallFailed("pidfd_open failed"));
}
let dup_fd = unsafe { libc::syscall(SYS_PIDFD_GETFD, pidfd as i32, tracee_fd, 0) };
let close_result = close_host_fd(pidfd as i32);
if dup_fd < 0 || dup_fd > i32::MAX as libc::c_long {
close_result?;
return Err(FdError::TraceeInstallFailed("pidfd_getfd failed"));
}
close_result?;
Ok(dup_fd as i32)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn send_fd_over_unix_socket(socket_fd: i32, fd_to_send: i32) -> Result<(), FdError> {
let mut data = [0u8; 1];
let mut iov = libc::iovec {
iov_base: data.as_mut_ptr() as *mut c_void,
iov_len: data.len(),
};
let mut control = [0u8; 64];
let control_len = cmsg_space_for_i32();
let cmsg = control.as_mut_ptr() as *mut libc::cmsghdr;
unsafe {
(*cmsg).cmsg_len = (std::mem::size_of::<libc::cmsghdr>() + std::mem::size_of::<i32>()) as _;
(*cmsg).cmsg_level = libc::SOL_SOCKET;
(*cmsg).cmsg_type = libc::SCM_RIGHTS;
let data_ptr = control
.as_mut_ptr()
.add(std::mem::size_of::<libc::cmsghdr>()) as *mut i32;
*data_ptr = fd_to_send;
}
let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
msg.msg_iov = &mut iov;
msg.msg_iovlen = 1;
msg.msg_control = control.as_mut_ptr() as *mut c_void;
msg.msg_controllen = control_len;
let sent = unsafe { libc::sendmsg(socket_fd, &msg, 0) };
if sent != 1 {
let errno = std::io::Error::last_os_error()
.raw_os_error()
.unwrap_or_default();
eprintln!(
"KBOXLIVE_SCM_SENDMSG_FAILED socket_fd={socket_fd} fd_to_send={fd_to_send} sent={sent} errno={errno}"
);
return Err(FdError::TraceeInstallFailed("sendmsg scm_rights failed"));
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn cmsg_space_for_i32() -> usize {
align_to_usize(std::mem::size_of::<libc::cmsghdr>())
+ align_to_usize(std::mem::size_of::<i32>())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn align_to_usize(value: usize) -> usize {
let align = std::mem::size_of::<usize>();
(value + align - 1) & !(align - 1)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn close_host_fd(fd: i32) -> Result<(), FdError> {
let ret = unsafe { libc::close(fd) };
if ret < 0 {
return Err(FdError::TraceeInstallFailed("host close failed"));
}
Ok(())
}
fn apply_tracee_socketpair_endpoint_installs(
first: TraceeEndpointInstall,
second: TraceeEndpointInstall,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<(), FdError> {
if first.source_fd == second.target_fd && second.source_fd == first.target_fd {
let temp_fd = executor.syscall(TraceeSyscall::new3(
LINUX_X86_64_SYS_FCNTL,
first.source_fd as u64,
LINUX_F_DUPFD_CLOEXEC,
0,
))?;
if temp_fd < 0 || temp_fd > i32::MAX as i64 {
return Err(FdError::TraceeInstallFailed("tracee temp dup failed"));
}
apply_tracee_fd_install(
second.source_fd,
second.target_fd,
second.cloexec,
false,
executor,
)?;
apply_tracee_fd_install(
temp_fd as i32,
first.target_fd,
first.cloexec,
true,
executor,
)?;
return Ok(());
}
if second.source_fd == first.target_fd {
apply_tracee_fd_install(
second.source_fd,
second.target_fd,
second.cloexec,
true,
executor,
)?;
apply_tracee_fd_install(
first.source_fd,
first.target_fd,
first.cloexec,
true,
executor,
)?;
return Ok(());
}
if first.source_fd == second.target_fd {
apply_tracee_fd_install(
first.source_fd,
first.target_fd,
first.cloexec,
true,
executor,
)?;
apply_tracee_fd_install(
second.source_fd,
second.target_fd,
second.cloexec,
true,
executor,
)?;
return Ok(());
}
apply_tracee_fd_install(
first.source_fd,
first.target_fd,
first.cloexec,
true,
executor,
)?;
apply_tracee_fd_install(
second.source_fd,
second.target_fd,
second.cloexec,
true,
executor,
)
}
fn close_tracee_fd(fd: i32, executor: &mut impl TraceeSyscallExecutor) -> Result<(), FdError> {
let ret = executor.syscall(TraceeSyscall::new3(LINUX_X86_64_SYS_CLOSE, fd as u64, 0, 0))?;
if ret < 0 {
return Err(FdError::TraceeInstallFailed("tracee close failed"));
}
Ok(())
}
fn apply_tracee_fd_install(
source_fd: i32,
target_fd: i32,
cloexec: bool,
close_source_after_install: bool,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<(), FdError> {
if source_fd == target_fd {
let fcntl_ret = executor.syscall(TraceeSyscall::new3(
LINUX_X86_64_SYS_FCNTL,
target_fd as u64,
LINUX_F_SETFD,
if cloexec { LINUX_FD_CLOEXEC } else { 0 },
))?;
if fcntl_ret < 0 {
return Err(FdError::TraceeInstallFailed("tracee fcntl failed"));
}
return Ok(());
}
TraceeInstallPlan::new(source_fd, target_fd, cloexec, close_source_after_install)?
.apply(executor)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn read_tracee_i32_pair(pid: i32, addr: u64) -> Result<[i32; 2], FdError> {
let mut values = [0i32; 2];
let local = libc::iovec {
iov_base: values.as_mut_ptr() as *mut c_void,
iov_len: std::mem::size_of_val(&values),
};
let remote = libc::iovec {
iov_base: addr as *mut c_void,
iov_len: std::mem::size_of_val(&values),
};
let read = unsafe { libc::process_vm_readv(pid, &local, 1, &remote, 1, 0) };
if read != std::mem::size_of_val(&values) as isize {
return Err(FdError::TraceeInstallFailed(
"process_vm_readv socketpair fds failed",
));
}
Ok(values)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn read_tracee_i32(pid: i32, addr: u64) -> Result<i32, FdError> {
let mut value = 0i32;
let local = libc::iovec {
iov_base: &mut value as *mut _ as *mut c_void,
iov_len: std::mem::size_of::<i32>(),
};
let remote = libc::iovec {
iov_base: addr as *mut c_void,
iov_len: std::mem::size_of::<i32>(),
};
let read = unsafe { libc::process_vm_readv(pid, &local, 1, &remote, 1, 0) };
if read != std::mem::size_of::<i32>() as isize {
return Err(FdError::TraceeInstallFailed(
"process_vm_readv received fd failed",
));
}
Ok(value)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn plain_old_bytes<T>(value: &T) -> &[u8] {
unsafe { std::slice::from_raw_parts(value as *const T as *const u8, std::mem::size_of::<T>()) }
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
fn read_tracee_i32_pair(_pid: i32, _addr: u64) -> Result<[i32; 2], FdError> {
Err(FdError::TraceeInstallFailed(
"tracee socketpair fd read unsupported on this host",
))
}
pub(crate) fn apply_proc_fd_shadow_install_with_scratch(
supervisor_pid: i32,
supervisor_fd: i32,
shadow_install: ShadowInstallPlan,
writer: &mut impl TraceeMemoryWriter,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<(), FdError> {
let path_probe = TraceeProcFdSourcePlan::new(supervisor_pid, supervisor_fd, 1)?;
let scratch = TraceeScratchMapping::map(path_probe.path_bytes().len(), executor)?;
let source = TraceeProcFdSourcePlan::new(supervisor_pid, supervisor_fd, scratch.addr)?;
let install_result = apply_proc_fd_shadow_install(&source, shadow_install, writer, executor);
let unmap_result = scratch.unmap(executor);
match (install_result, unmap_result) {
(Err(err), _) => Err(err),
(Ok(()), Err(err)) => Err(err),
(Ok(()), Ok(())) => Ok(()),
}
}
pub(crate) fn apply_shadow_install_plan_with_scratch(
supervisor_pid: i32,
shadow_install: ShadowInstallPlan,
writer: &mut impl TraceeMemoryWriter,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<(), FdError> {
apply_proc_fd_shadow_install_with_scratch(
supervisor_pid,
shadow_install.shadow.supervisor_fd,
shadow_install,
writer,
executor,
)
}
pub(crate) fn install_operation_shadow_if_needed(
fd_system: &KboxlikeFdSystem,
pid: ProcessId,
fd: FdNumber,
operation: FdOperation,
supervisor_pid: i32,
writer: &mut impl TraceeMemoryWriter,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<bool, FdError> {
let Some(plan) = fd_system.plan_operation_shadow_install(pid, fd, operation)? else {
return Ok(false);
};
apply_shadow_install_plan_with_scratch(supervisor_pid, plan, writer, executor)?;
Ok(true)
}
pub(crate) fn install_requested_operation_shadow_if_needed(
fd_system: &KboxlikeFdSystem,
request: TraceeOperationInstallRequest,
supervisor_pid: i32,
writer: &mut impl TraceeMemoryWriter,
executor: &mut impl TraceeSyscallExecutor,
) -> Result<bool, FdError> {
install_operation_shadow_if_needed(
fd_system,
request.model_pid,
request.fd,
request.operation,
supervisor_pid,
writer,
executor,
)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn install_requested_operation_shadow_in_stopped_tracee(
fd_system: &KboxlikeFdSystem,
request: TraceeOperationInstallRequest,
supervisor_pid: i32,
) -> Result<bool, FdError> {
let mut writer = ProcessVmMemoryWriter::new(request.tracee_pid)?;
let mut executor = PtraceSyscallExecutor::new(request.tracee_pid)?;
install_requested_operation_shadow_if_needed(
fd_system,
request,
supervisor_pid,
&mut writer,
&mut executor,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kboxlike::fd::{ShadowInstallPlan, ShadowKind, ShadowObject};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use crate::kboxlike::snapshot::{
KboxlikeByteBackedRestoreSource, KboxlikeMaterializedMemoryRestore,
KboxlikeMaterializedMemoryRestoreAction, KboxlikeSharedMappingKind,
KboxlikeSharedMemoryDump, KboxlikeThreadKernelStateSnapshot, KboxlikeThreadSnapshot,
};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use std::collections::BTreeMap;
use std::collections::VecDeque;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use std::path::PathBuf;
#[derive(Default)]
struct FakeTracee {
calls: Vec<TraceeSyscall>,
returns: VecDeque<i64>,
synthetic_ranges: Vec<(u64, u64)>,
}
#[derive(Default)]
struct FakeMemory {
writes: Vec<(u64, Vec<u8>)>,
fail: bool,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Default)]
struct FakeRegisterWriter {
writes: Vec<libc::user_regs_struct>,
fail: bool,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Default)]
struct FakeSharedObjectBackingFactory {
next_fd: i32,
created: Vec<String>,
truncated: Vec<(i32, u64)>,
writes: Vec<(i32, u64, Vec<u8>)>,
fail_create: bool,
fail_set_len: bool,
fail_write: bool,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Default)]
struct FakeRestoreAccessFactory {
returns_by_pid: BTreeMap<i32, Vec<i64>>,
requested: Vec<i32>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Default)]
struct FakeRestoredTraceeResumer {
resumed: Vec<i32>,
fail_pid: Option<i32>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Default)]
struct FakeStoppedTraceeReplacementFactory {
calls: Vec<(ProcessId, Vec<ModelThreadId>)>,
returns: VecDeque<Result<BTreeMap<ModelThreadId, i32>, FdError>>,
}
impl FakeTracee {
fn with_returns(returns: impl IntoIterator<Item = i64>) -> Self {
Self {
calls: Vec::new(),
returns: returns.into_iter().collect(),
synthetic_ranges: Vec::new(),
}
}
fn with_returns_and_synthetic_ranges(
returns: impl IntoIterator<Item = i64>,
synthetic_ranges: Vec<(u64, u64)>,
) -> Self {
Self {
calls: Vec::new(),
returns: returns.into_iter().collect(),
synthetic_ranges,
}
}
}
impl TraceeSyscallExecutor for FakeTracee {
fn syscall(&mut self, syscall: TraceeSyscall) -> Result<i64, FdError> {
self.calls.push(syscall);
Ok(self.returns.pop_front().unwrap_or(0))
}
fn kernel_synthetic_ranges(&self) -> Result<Vec<(u64, u64)>, FdError> {
Ok(self.synthetic_ranges.clone())
}
}
impl TraceeMemoryWriter for FakeMemory {
fn write_bytes(&mut self, addr: u64, bytes: &[u8]) -> Result<(), FdError> {
if self.fail {
return Err(FdError::TraceeInstallFailed(
"tracee source path write failed",
));
}
self.writes.push((addr, bytes.to_vec()));
Ok(())
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeRegisterWriter for FakeRegisterWriter {
fn write_regs(&mut self, regs: &libc::user_regs_struct) -> Result<(), FdError> {
if self.fail {
return Err(FdError::TraceeInstallFailed("ptrace setregs failed"));
}
self.writes.push(*regs);
Ok(())
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeSharedObjectBackingFactory for FakeSharedObjectBackingFactory {
fn create_memfd(&mut self, name: &str) -> Result<i32, FdError> {
if self.fail_create {
return Err(FdError::TraceeInstallFailed("memfd_create failed"));
}
let fd = self.next_fd;
self.next_fd += 1;
self.created.push(name.to_string());
Ok(fd)
}
fn set_len(&mut self, fd: i32, len: u64) -> Result<(), FdError> {
if self.fail_set_len {
return Err(FdError::TraceeInstallFailed(
"shared object ftruncate failed",
));
}
self.truncated.push((fd, len));
Ok(())
}
fn write_all_at(&mut self, fd: i32, offset: u64, bytes: &[u8]) -> Result<(), FdError> {
if self.fail_write {
return Err(FdError::TraceeInstallFailed("shared object pwrite failed"));
}
self.writes.push((fd, offset, bytes.to_vec()));
Ok(())
}
fn close_fds_on_drop(&self) -> bool {
false
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeRestoreAccessFactory for FakeRestoreAccessFactory {
type Writer = FakeMemory;
type Executor = FakeTracee;
type RegisterWriter = FakeRegisterWriter;
fn access_for_restore(
&mut self,
host_pid: i32,
) -> Result<TraceeRestoreAccess<Self::Writer, Self::Executor, Self::RegisterWriter>, FdError>
{
self.requested.push(host_pid);
let returns = self
.returns_by_pid
.remove(&host_pid)
.ok_or(FdError::TraceeInstallFailed("missing fake tracee returns"))?;
Ok(TraceeRestoreAccess {
writer: FakeMemory::default(),
executor: FakeTracee::with_returns(returns),
register_writer: FakeRegisterWriter::default(),
})
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl RestoredTraceeResumer for FakeRestoredTraceeResumer {
fn resume_restored_tracee(&mut self, host_pid: i32) -> Result<(), FdError> {
if self.fail_pid == Some(host_pid) {
return Err(FdError::TraceeInstallFailed("resume failed"));
}
self.resumed.push(host_pid);
Ok(())
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl StoppedTraceeReplacementFactory for FakeStoppedTraceeReplacementFactory {
fn create_stopped_replacement_group(
&mut self,
model_pid: ProcessId,
threads: &[KboxlikeThreadSnapshot],
) -> Result<BTreeMap<ModelThreadId, i32>, FdError> {
self.calls.push((
model_pid,
threads
.iter()
.map(|thread| thread.model_thread_id)
.collect(),
));
self.returns
.pop_front()
.unwrap_or_else(|| Err(FdError::TraceeInstallFailed("missing fake replacement")))
}
}
fn shadow_install(target_fd: i32, cloexec: bool) -> ShadowInstallPlan {
ShadowInstallPlan {
source_fd: 33033,
target_fd,
shadow: ShadowObject {
kind: ShadowKind::LocalOnly,
supervisor_fd: 700,
},
cloexec,
}
}
#[test]
fn tracee_plan_from_shadow_install_emits_dup2_and_fcntl() {
let plan =
TraceeInstallPlan::from_shadow_install(shadow_install(104, true), 900, false).unwrap();
assert_eq!(
plan.syscalls(),
vec![
TraceeSyscall::new3(LINUX_X86_64_SYS_DUP2, 900, 104, 0),
TraceeSyscall::new3(LINUX_X86_64_SYS_FCNTL, 104, LINUX_F_SETFD, LINUX_FD_CLOEXEC),
]
);
}
#[test]
fn tracee_plan_can_clear_cloexec_and_close_source() {
let plan = TraceeInstallPlan::new(901, 6, false, true).unwrap();
assert_eq!(
plan.syscalls(),
vec![
TraceeSyscall::new3(LINUX_X86_64_SYS_DUP2, 901, 6, 0),
TraceeSyscall::new3(LINUX_X86_64_SYS_FCNTL, 6, LINUX_F_SETFD, 0),
TraceeSyscall::new3(LINUX_X86_64_SYS_CLOSE, 901, 0, 0),
]
);
}
#[test]
fn socketpair_endpoint_install_preserves_overlapping_second_source() {
let mut tracee = FakeTracee::with_returns([5, 0, 0, 4, 0, 0]);
apply_tracee_socketpair_endpoint_installs(
TraceeEndpointInstall {
source_fd: 3,
target_fd: 4,
cloexec: false,
},
TraceeEndpointInstall {
source_fd: 4,
target_fd: 5,
cloexec: false,
},
&mut tracee,
)
.unwrap();
assert_eq!(
tracee.calls,
vec![
TraceeSyscall::new3(LINUX_X86_64_SYS_DUP2, 4, 5, 0),
TraceeSyscall::new3(LINUX_X86_64_SYS_FCNTL, 5, LINUX_F_SETFD, 0),
TraceeSyscall::new3(LINUX_X86_64_SYS_CLOSE, 4, 0, 0),
TraceeSyscall::new3(LINUX_X86_64_SYS_DUP2, 3, 4, 0),
TraceeSyscall::new3(LINUX_X86_64_SYS_FCNTL, 4, LINUX_F_SETFD, 0),
TraceeSyscall::new3(LINUX_X86_64_SYS_CLOSE, 3, 0, 0),
]
);
}
#[test]
fn tracee_syscall_can_encode_six_linux_args() {
assert_eq!(
TraceeSyscall::new6(9, [1, 2, 3, 4, 5, 6]),
TraceeSyscall {
nr: 9,
args: [1, 2, 3, 4, 5, 6],
}
);
}
#[test]
fn tracee_scratch_mapping_uses_remote_mmap_and_munmap() {
let mut tracee = FakeTracee::with_returns([0x7f00_1000, 0]);
let mapping = TraceeScratchMapping::map(4096, &mut tracee).unwrap();
mapping.unmap(&mut tracee).unwrap();
assert_eq!(
mapping,
TraceeScratchMapping {
addr: 0x7f00_1000,
len: 4096,
}
);
assert_eq!(
tracee.calls,
vec![
TraceeSyscall::new6(
LINUX_X86_64_SYS_MMAP,
[
0,
4096,
LINUX_PROT_READ | LINUX_PROT_WRITE,
LINUX_MAP_PRIVATE | LINUX_MAP_ANONYMOUS,
(-1i64) as u64,
0,
],
),
TraceeSyscall::new6(LINUX_X86_64_SYS_MUNMAP, [0x7f00_1000, 4096, 0, 0, 0, 0]),
]
);
}
#[test]
fn tracee_scratch_mapping_rejects_zero_length_before_syscall() {
let mut tracee = FakeTracee::with_returns([0x7f00_1000]);
assert_eq!(
TraceeScratchMapping::map(0, &mut tracee).unwrap_err(),
FdError::TraceeInstallFailed("tracee scratch length is zero")
);
assert!(tracee.calls.is_empty());
}
#[test]
fn tracee_scratch_mapping_rejects_failed_mmap_and_munmap() {
let mut mmap_fail = FakeTracee::with_returns([-12]);
assert_eq!(
TraceeScratchMapping::map(4096, &mut mmap_fail).unwrap_err(),
FdError::TraceeInstallFailed("tracee mmap failed")
);
let mut null_mmap = FakeTracee::with_returns([0]);
assert_eq!(
TraceeScratchMapping::map(4096, &mut null_mmap).unwrap_err(),
FdError::TraceeInstallFailed("tracee mmap returned null")
);
let mapping = TraceeScratchMapping {
addr: 0x7f00_1000,
len: 4096,
};
let mut munmap_fail = FakeTracee::with_returns([-22]);
assert_eq!(
mapping.unmap(&mut munmap_fail).unwrap_err(),
FdError::TraceeInstallFailed("tracee munmap failed")
);
}
#[test]
fn byte_backed_mapping_maps_writes_and_applies_final_protection() {
let mapping = TraceeByteBackedMapping::new(
0x7f00_0000,
libc::PROT_READ | libc::PROT_EXEC,
b"abcd".to_vec(),
)
.unwrap();
let mut tracee = FakeTracee::with_returns([0x7f00_0000, 0]);
let mut memory = FakeMemory::default();
mapping.apply(&mut memory, &mut tracee).unwrap();
assert_eq!(
tracee.calls,
vec![
TraceeSyscall::new6(
LINUX_X86_64_SYS_MMAP,
[
0x7f00_0000,
4,
LINUX_PROT_READ | LINUX_PROT_WRITE,
LINUX_MAP_PRIVATE | LINUX_MAP_FIXED | LINUX_MAP_ANONYMOUS,
(-1i64) as u64,
0,
],
),
TraceeSyscall::new3(
LINUX_X86_64_SYS_MPROTECT,
0x7f00_0000,
4,
(libc::PROT_READ | libc::PROT_EXEC) as u64,
),
]
);
assert_eq!(memory.writes, vec![(0x7f00_0000, b"abcd".to_vec())]);
}
#[test]
fn byte_backed_mapping_skips_mprotect_when_final_mapping_stays_writable() {
let mapping = TraceeByteBackedMapping::new(
0x7f00_1000,
libc::PROT_READ | libc::PROT_WRITE,
b"xy".to_vec(),
)
.unwrap();
let mut tracee = FakeTracee::with_returns([0x7f00_1000]);
let mut memory = FakeMemory::default();
mapping.apply(&mut memory, &mut tracee).unwrap();
assert_eq!(tracee.calls, vec![mapping.mmap_syscall()]);
assert_eq!(memory.writes, vec![(0x7f00_1000, b"xy".to_vec())]);
}
#[test]
fn byte_backed_mapping_unmaps_mappable_kernel_synthetic_collisions() {
let mapping = TraceeByteBackedMapping::new(
0x7f00_0000,
libc::PROT_READ | libc::PROT_EXEC,
b"abcdefghijkl".to_vec(),
)
.unwrap();
let mut tracee = FakeTracee::with_returns_and_synthetic_ranges(
[0, 0x7f00_0000, 0],
vec![(0x7f00_0004, 0x7f00_0008)],
);
let mut memory = FakeMemory::default();
mapping.apply(&mut memory, &mut tracee).unwrap();
assert_eq!(
tracee.calls,
vec![
TraceeSyscall::new6(LINUX_X86_64_SYS_MUNMAP, [0x7f00_0004, 4, 0, 0, 0, 0],),
mapping.mmap_syscall(),
mapping.mprotect_syscall(),
]
);
assert_eq!(memory.writes, vec![(0x7f00_0000, b"abcdefghijkl".to_vec())]);
}
#[test]
fn byte_backed_mapping_rejects_invalid_inputs_before_syscalls() {
assert_eq!(
TraceeByteBackedMapping::new(0, libc::PROT_READ, b"x".to_vec()).unwrap_err(),
FdError::TraceeInstallFailed("tracee mapping address is null")
);
assert_eq!(
TraceeByteBackedMapping::new(0x1000, libc::PROT_READ, Vec::new()).unwrap_err(),
FdError::TraceeInstallFailed("tracee mapping bytes are empty")
);
assert_eq!(
TraceeByteBackedMapping::new(0x1000, -1, b"x".to_vec()).unwrap_err(),
FdError::TraceeInstallFailed("tracee mapping protection is invalid")
);
}
#[test]
fn byte_backed_mapping_rejects_failed_syscalls_and_writes() {
let mapping =
TraceeByteBackedMapping::new(0x7f00_0000, libc::PROT_READ, b"abcd".to_vec()).unwrap();
let mut mmap_fail = FakeTracee::with_returns([-12]);
let mut memory = FakeMemory::default();
assert_eq!(
mapping.apply(&mut memory, &mut mmap_fail).unwrap_err(),
FdError::TraceeInstallFailed("tracee fixed mmap failed")
);
assert!(memory.writes.is_empty());
let mut wrong_addr = FakeTracee::with_returns([0x7f00_1000]);
let mut memory = FakeMemory::default();
assert_eq!(
mapping.apply(&mut memory, &mut wrong_addr).unwrap_err(),
FdError::TraceeInstallFailed("tracee fixed mmap returned wrong address")
);
assert!(memory.writes.is_empty());
let mut write_fail = FakeTracee::with_returns([0x7f00_0000]);
let mut memory = FakeMemory {
writes: Vec::new(),
fail: true,
};
assert_eq!(
mapping.apply(&mut memory, &mut write_fail).unwrap_err(),
FdError::TraceeInstallFailed("tracee source path write failed")
);
let mut mprotect_fail = FakeTracee::with_returns([0x7f00_0000, -22]);
let mut memory = FakeMemory::default();
assert_eq!(
mapping.apply(&mut memory, &mut mprotect_fail).unwrap_err(),
FdError::TraceeInstallFailed("tracee mprotect failed")
);
assert_eq!(memory.writes, vec![(0x7f00_0000, b"abcd".to_vec())]);
}
#[test]
fn synthetic_collision_helper_keeps_fixed_vsyscall_blocked() {
let mut tracee = FakeTracee::default();
let blocked = unmap_mappable_synthetic_collisions(
0xffff_ffff_ff5f_f000,
8192,
&[(0xffff_ffff_ff60_0000, 0xffff_ffff_ff60_1000)],
&mut tracee,
)
.unwrap();
assert_eq!(
blocked,
vec![(0xffff_ffff_ff60_0000, 0xffff_ffff_ff60_1000)]
);
assert!(tracee.calls.is_empty());
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn clean_file_mapping_stages_path_maps_file_and_cleans_up() {
let mapping = TraceeCleanFileMapping::new(
0x7f00_2000,
4096,
libc::PROT_READ | libc::PROT_EXEC,
Path::new("/lib/libc.so.6"),
0x1000,
)
.unwrap();
let mut tracee = FakeTracee::with_returns([0x9000, 901, 0x7f00_2000, 0, 0]);
let mut memory = FakeMemory::default();
mapping
.apply_with_scratch(&mut memory, &mut tracee)
.unwrap();
assert_eq!(memory.writes, vec![(0x9000, b"/lib/libc.so.6\0".to_vec())]);
assert_eq!(
tracee.calls,
vec![
TraceeScratchMapping::mmap_syscall(15),
TraceeSyscall::new3(
LINUX_X86_64_SYS_OPENAT,
LINUX_AT_FDCWD,
0x9000,
LINUX_O_CLOEXEC,
),
TraceeSyscall::new6(
LINUX_X86_64_SYS_MMAP,
[
0x7f00_2000,
4096,
(libc::PROT_READ | libc::PROT_EXEC) as u64,
LINUX_MAP_PRIVATE | LINUX_MAP_FIXED,
901,
0x1000,
],
),
TraceeSyscall::new3(LINUX_X86_64_SYS_CLOSE, 901, 0, 0),
TraceeSyscall::new6(LINUX_X86_64_SYS_MUNMAP, [0x9000, 15, 0, 0, 0, 0]),
]
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn clean_file_mapping_rejects_invalid_inputs_and_failed_syscalls() {
assert_eq!(
TraceeCleanFileMapping::new(0, 1, libc::PROT_READ, Path::new("/x"), 0).unwrap_err(),
FdError::TraceeInstallFailed("tracee file mapping address is null")
);
assert_eq!(
TraceeCleanFileMapping::new(0x1000, 0, libc::PROT_READ, Path::new("/x"), 0)
.unwrap_err(),
FdError::TraceeInstallFailed("tracee file mapping length is zero")
);
assert_eq!(
TraceeCleanFileMapping::new(0x1000, 1, -1, Path::new("/x"), 0).unwrap_err(),
FdError::TraceeInstallFailed("tracee file mapping protection is invalid")
);
assert_eq!(
TraceeCleanFileMapping::new(0x1000, 1, libc::PROT_READ, Path::new(""), 0).unwrap_err(),
FdError::TraceeInstallFailed("tracee file mapping path is empty")
);
let mapping =
TraceeCleanFileMapping::new(0x7f00_2000, 4096, libc::PROT_READ, Path::new("/x"), 0)
.unwrap();
let mut open_fail = FakeTracee::with_returns([0x9000, -2, 0]);
let mut memory = FakeMemory::default();
assert_eq!(
mapping
.apply_with_scratch(&mut memory, &mut open_fail)
.unwrap_err(),
FdError::TraceeInstallFailed("tracee open clean file failed")
);
let mut mmap_fail = FakeTracee::with_returns([0x9000, 901, -12, 0, 0]);
let mut memory = FakeMemory::default();
assert_eq!(
mapping
.apply_with_scratch(&mut memory, &mut mmap_fail)
.unwrap_err(),
FdError::TraceeInstallFailed("tracee fixed file mmap failed")
);
let mut wrong_addr = FakeTracee::with_returns([0x9000, 901, 0x7f00_3000, 0, 0]);
let mut memory = FakeMemory::default();
assert_eq!(
mapping
.apply_with_scratch(&mut memory, &mut wrong_addr)
.unwrap_err(),
FdError::TraceeInstallFailed("tracee fixed file mmap returned wrong address")
);
let mut close_fail = FakeTracee::with_returns([0x9000, 901, 0x7f00_2000, -9, 0]);
let mut memory = FakeMemory::default();
assert_eq!(
mapping
.apply_with_scratch(&mut memory, &mut close_fail)
.unwrap_err(),
FdError::TraceeInstallFailed("tracee close clean file failed")
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn owned_shared_object_registry_builds_memfds_and_initializes_extents() {
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![
KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject {
host_pid: 4321,
model_pid: 7,
start: 0x7f00_3000,
len: 4,
prot: libc::PROT_READ | libc::PROT_WRITE,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:chrome shared (deleted)".to_string()),
file_offset: 0,
dev: "00:01".to_string(),
inode: 2,
},
KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject {
host_pid: 5000,
model_pid: 8,
start: 0x7f00_9000,
len: 4,
prot: libc::PROT_READ | libc::PROT_WRITE,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:chrome shared (deleted)".to_string()),
file_offset: 0,
dev: "00:01".to_string(),
inode: 2,
},
KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject {
host_pid: 4321,
model_pid: 7,
start: 0x7f00_4000,
len: 2,
prot: libc::PROT_READ,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:chrome shared (deleted)".to_string()),
file_offset: 4,
dev: "00:01".to_string(),
inode: 2,
},
KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject {
host_pid: 4321,
model_pid: 7,
start: 0x7f00_5000,
len: 4096,
prot: libc::PROT_READ,
kind: KboxlikeSharedMappingKind::Device,
path: Some("/dev/shm/chrome".to_string()),
file_offset: 0,
dev: "00:05".to_string(),
inode: 10,
},
],
};
let dumps = vec![
KboxlikeSharedMemoryDump {
host_pid: 4321,
model_pid: 7,
start: 0x7f00_3000,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:chrome shared (deleted)".to_string()),
file_offset: 0,
dev: "00:01".to_string(),
inode: 2,
bytes: b"abcd".to_vec(),
},
KboxlikeSharedMemoryDump {
host_pid: 4321,
model_pid: 7,
start: 0x7f00_4000,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:chrome shared (deleted)".to_string()),
file_offset: 4,
dev: "00:01".to_string(),
inode: 2,
bytes: b"ef".to_vec(),
},
KboxlikeSharedMemoryDump {
host_pid: 4321,
model_pid: 7,
start: 0x7f00_5000,
kind: KboxlikeSharedMappingKind::Device,
path: Some("/dev/shm/chrome".to_string()),
file_offset: 0,
dev: "00:05".to_string(),
inode: 10,
bytes: vec![0xaa; 4096],
},
];
let mut factory = FakeSharedObjectBackingFactory {
next_fd: 700,
..FakeSharedObjectBackingFactory::default()
};
let owned =
build_owned_shared_object_registry_with(&restore, &dumps, &mut factory).unwrap();
assert_eq!(
owned.summary(),
TraceeSharedObjectRegistryBuildSummary {
created_objects: 2,
initialized_extents: 3,
skipped_unsupported_objects: 0,
}
);
let key = TraceeSharedObjectKey {
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:chrome shared (deleted)".to_string()),
dev: "00:01".to_string(),
inode: 2,
};
assert_eq!(owned.registry().supervisor_fd(&key), Some(700));
let device_key = TraceeSharedObjectKey {
kind: KboxlikeSharedMappingKind::Device,
path: Some("/dev/shm/chrome".to_string()),
dev: "00:05".to_string(),
inode: 10,
};
assert_eq!(owned.registry().supervisor_fd(&device_key), Some(701));
assert_eq!(
factory.created,
vec!["chrome_shared".to_string(), "dev_shm_chrome".to_string()]
);
assert_eq!(factory.truncated, vec![(700, 6), (701, 4096)]);
assert_eq!(
factory.writes,
vec![
(700, 0, b"abcd".to_vec()),
(700, 4, b"ef".to_vec()),
(701, 0, vec![0xaa; 4096])
]
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn owned_shared_object_registry_rejects_missing_or_duplicate_extents() {
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![
KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject {
host_pid: 4321,
model_pid: 7,
start: 0x7f00_3000,
len: 4,
prot: libc::PROT_READ | libc::PROT_WRITE,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:shared".to_string()),
file_offset: 0,
dev: "00:01".to_string(),
inode: 2,
},
],
};
let mut factory = FakeSharedObjectBackingFactory {
next_fd: 700,
..FakeSharedObjectBackingFactory::default()
};
assert_eq!(
build_owned_shared_object_registry_with(&restore, &[], &mut factory).unwrap_err(),
FdError::TraceeInstallFailed("missing shared memory dump extent")
);
let dump = KboxlikeSharedMemoryDump {
host_pid: 4321,
model_pid: 7,
start: 0x7f00_3000,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:shared".to_string()),
file_offset: 0,
dev: "00:01".to_string(),
inode: 2,
bytes: b"abcd".to_vec(),
};
let mut factory = FakeSharedObjectBackingFactory {
next_fd: 700,
..FakeSharedObjectBackingFactory::default()
};
assert_eq!(
build_owned_shared_object_registry_with(&restore, &[dump.clone(), dump], &mut factory)
.unwrap_err(),
FdError::TraceeInstallFailed("duplicate shared memory dump extent")
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn shared_fd_mapping_opens_supervisor_fd_maps_shared_and_cleans_up() {
let mapping = TraceeSharedFdMapping::new(
0x7f00_3000,
4096,
libc::PROT_READ | libc::PROT_WRITE,
0x2000,
)
.unwrap();
let mut tracee = FakeTracee::with_returns([0x9000, 901, 0x7f00_3000, 0, 0]);
let mut memory = FakeMemory::default();
mapping
.apply_from_supervisor_fd_with_scratch(1234, 700, &mut memory, &mut tracee)
.unwrap();
assert_eq!(
memory.writes,
vec![(0x9000, b"/proc/1234/fd/700\0".to_vec())]
);
assert_eq!(
tracee.calls,
vec![
TraceeScratchMapping::mmap_syscall(18),
TraceeSyscall::new3(
LINUX_X86_64_SYS_OPENAT,
LINUX_AT_FDCWD,
0x9000,
LINUX_O_CLOEXEC | LINUX_O_RDWR,
),
TraceeSyscall::new6(
LINUX_X86_64_SYS_MMAP,
[
0x7f00_3000,
4096,
(libc::PROT_READ | libc::PROT_WRITE) as u64,
LINUX_MAP_SHARED | LINUX_MAP_FIXED,
901,
0x2000,
],
),
TraceeSyscall::new3(LINUX_X86_64_SYS_CLOSE, 901, 0, 0),
TraceeSyscall::new6(LINUX_X86_64_SYS_MUNMAP, [0x9000, 18, 0, 0, 0, 0]),
]
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn shared_fd_mapping_rejects_invalid_inputs_and_failed_syscalls() {
assert_eq!(
TraceeSharedFdMapping::new(0, 1, libc::PROT_READ, 0).unwrap_err(),
FdError::TraceeInstallFailed("tracee shared mapping address is null")
);
assert_eq!(
TraceeSharedFdMapping::new(0x1000, 0, libc::PROT_READ, 0).unwrap_err(),
FdError::TraceeInstallFailed("tracee shared mapping length is zero")
);
assert_eq!(
TraceeSharedFdMapping::new(0x1000, 1, -1, 0).unwrap_err(),
FdError::TraceeInstallFailed("tracee shared mapping protection is invalid")
);
let mapping = TraceeSharedFdMapping::new(0x7f00_3000, 4096, libc::PROT_READ, 0).unwrap();
let mut open_fail = FakeTracee::with_returns([0x9000, -2, 0]);
let mut memory = FakeMemory::default();
assert_eq!(
mapping
.apply_from_supervisor_fd_with_scratch(1234, 700, &mut memory, &mut open_fail)
.unwrap_err(),
FdError::TraceeInstallFailed("tracee open supervisor fd failed")
);
let mut mmap_fail = FakeTracee::with_returns([0x9000, 901, -12, 0, 0]);
let mut memory = FakeMemory::default();
assert_eq!(
mapping
.apply_from_supervisor_fd_with_scratch(1234, 700, &mut memory, &mut mmap_fail)
.unwrap_err(),
FdError::TraceeInstallFailed("tracee fixed shared mmap failed")
);
let mut wrong_addr = FakeTracee::with_returns([0x9000, 901, 0x7f00_4000, 0, 0]);
let mut memory = FakeMemory::default();
assert_eq!(
mapping
.apply_from_supervisor_fd_with_scratch(1234, 700, &mut memory, &mut wrong_addr)
.unwrap_err(),
FdError::TraceeInstallFailed("tracee fixed shared mmap returned wrong address")
);
let mut close_fail = FakeTracee::with_returns([0x9000, 901, 0x7f00_3000, -9, 0]);
let mut memory = FakeMemory::default();
assert_eq!(
mapping
.apply_from_supervisor_fd_with_scratch(1234, 700, &mut memory, &mut close_fail)
.unwrap_err(),
FdError::TraceeInstallFailed("tracee close shared fd failed")
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn materialized_memory_restore_remaps_registered_shared_objects() {
let shared_action = KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject {
host_pid: 4321,
model_pid: 7,
start: 0x7f00_3000,
len: 4096,
prot: libc::PROT_READ | libc::PROT_WRITE,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:shared".to_string()),
file_offset: 0x2000,
dev: "00:00".to_string(),
inode: 2,
};
let key = TraceeSharedObjectKey::from_action(&shared_action).unwrap();
let mut shared = TraceeSharedObjectRegistry::new();
shared.register(key, 700).unwrap();
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![shared_action],
};
let mut tracee = FakeTracee::with_returns([0x9000, 901, 0x7f00_3000, 0, 0]);
let mut memory = FakeMemory::default();
let summary = apply_materialized_memory_restore_for_tracee_with_shared_objects(
&restore,
4321,
1234,
&shared,
&mut memory,
&mut tracee,
)
.unwrap();
assert_eq!(
summary,
TraceeMemoryRestoreSummary {
byte_backed_applied: 0,
clean_file_remapped: 0,
shared_object_remapped: 1,
skipped_no_mapping: 0,
deferred_unsupported: 0,
other_tracee: 0,
}
);
assert_eq!(
memory.writes,
vec![(0x9000, b"/proc/1234/fd/700\0".to_vec())]
);
assert_eq!(
tracee.calls,
vec![
TraceeScratchMapping::mmap_syscall(18),
TraceeSyscall::new3(
LINUX_X86_64_SYS_OPENAT,
LINUX_AT_FDCWD,
0x9000,
LINUX_O_CLOEXEC | LINUX_O_RDWR,
),
TraceeSyscall::new6(
LINUX_X86_64_SYS_MMAP,
[
0x7f00_3000,
4096,
(libc::PROT_READ | libc::PROT_WRITE) as u64,
LINUX_MAP_SHARED | LINUX_MAP_FIXED,
901,
0x2000,
],
),
TraceeSyscall::new3(LINUX_X86_64_SYS_CLOSE, 901, 0, 0),
TraceeSyscall::new6(LINUX_X86_64_SYS_MUNMAP, [0x9000, 18, 0, 0, 0, 0]),
]
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn materialized_memory_restore_applies_byte_backed_actions_for_one_tracee() {
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![
KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: 4321,
model_pid: 7,
start: 0x7f00_0000,
prot: libc::PROT_READ,
bytes: b"abcd".to_vec(),
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate { label: None },
},
KboxlikeMaterializedMemoryRestoreAction::SkipInaccessible {
host_pid: 4321,
model_pid: 7,
start: 0x7f00_1000,
len: 4096,
prot: libc::PROT_NONE,
},
KboxlikeMaterializedMemoryRestoreAction::RemapCleanFile {
host_pid: 4321,
model_pid: 7,
start: 0x7f00_2000,
len: 4096,
prot: libc::PROT_READ,
path: PathBuf::from("/lib/libc.so.6"),
file_offset: 0,
dev: "00:00".to_string(),
inode: 1,
},
KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject {
host_pid: 4321,
model_pid: 7,
start: 0x7f00_3000,
len: 4096,
prot: libc::PROT_READ | libc::PROT_WRITE,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:shared".to_string()),
file_offset: 0,
dev: "00:00".to_string(),
inode: 2,
},
KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: 5000,
model_pid: 9,
start: 0x7f00_4000,
prot: libc::PROT_READ,
bytes: b"other".to_vec(),
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate { label: None },
},
],
};
let mut tracee =
FakeTracee::with_returns([0x7f00_0000, 0, 0x7f00_1000, 0x9000, 901, 0x7f00_2000, 0, 0]);
let mut memory = FakeMemory::default();
let summary =
apply_materialized_memory_restore_for_tracee(&restore, 4321, &mut memory, &mut tracee)
.unwrap();
assert_eq!(
summary,
TraceeMemoryRestoreSummary {
byte_backed_applied: 1,
clean_file_remapped: 1,
shared_object_remapped: 0,
skipped_no_mapping: 1,
deferred_unsupported: 1,
other_tracee: 1,
}
);
assert_eq!(
memory.writes,
vec![
(0x7f00_0000, b"abcd".to_vec()),
(0x9000, b"/lib/libc.so.6\0".to_vec()),
]
);
assert_eq!(
tracee.calls,
vec![
TraceeSyscall::new6(
LINUX_X86_64_SYS_MMAP,
[
0x7f00_0000,
4,
LINUX_PROT_READ | LINUX_PROT_WRITE,
LINUX_MAP_PRIVATE | LINUX_MAP_FIXED | LINUX_MAP_ANONYMOUS,
(-1i64) as u64,
0,
],
),
TraceeSyscall::new3(
LINUX_X86_64_SYS_MPROTECT,
0x7f00_0000,
4,
libc::PROT_READ as u64
),
TraceeSyscall::new6(
LINUX_X86_64_SYS_MMAP,
[
0x7f00_1000,
4096,
libc::PROT_NONE as u64,
LINUX_MAP_PRIVATE | LINUX_MAP_FIXED | LINUX_MAP_ANONYMOUS,
(-1i64) as u64,
0,
],
),
TraceeScratchMapping::mmap_syscall(15),
TraceeSyscall::new3(
LINUX_X86_64_SYS_OPENAT,
LINUX_AT_FDCWD,
0x9000,
LINUX_O_CLOEXEC,
),
TraceeSyscall::new6(
LINUX_X86_64_SYS_MMAP,
[
0x7f00_2000,
4096,
libc::PROT_READ as u64,
LINUX_MAP_PRIVATE | LINUX_MAP_FIXED,
901,
0,
],
),
TraceeSyscall::new3(LINUX_X86_64_SYS_CLOSE, 901, 0, 0),
TraceeSyscall::new6(LINUX_X86_64_SYS_MUNMAP, [0x9000, 15, 0, 0, 0, 0]),
]
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn materialized_memory_restore_rejects_invalid_tracee_and_replay_failures() {
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: 4321,
model_pid: 7,
start: 0x7f00_0000,
prot: libc::PROT_READ,
bytes: b"abcd".to_vec(),
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate { label: None },
}],
};
let mut tracee = FakeTracee::default();
let mut memory = FakeMemory::default();
assert_eq!(
apply_materialized_memory_restore_for_tracee(&restore, 0, &mut memory, &mut tracee)
.unwrap_err(),
FdError::TraceeInstallFailed("invalid tracee pid")
);
let mut tracee = FakeTracee::with_returns([-12]);
let mut memory = FakeMemory::default();
assert_eq!(
apply_materialized_memory_restore_for_tracee(&restore, 4321, &mut memory, &mut tracee)
.unwrap_err(),
FdError::TraceeInstallFailed("tracee fixed mmap failed")
);
assert!(memory.writes.is_empty());
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn thread_register_restore_writes_matching_tracee_regs() {
let mut target_regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
target_regs.rip = 0x7f00_3000;
target_regs.rsp = 0x7fff_0000;
let mut other_regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
other_regs.rip = 0x1234;
let threads = vec![
KboxlikeThreadSnapshot {
host_pid: 1111,
model_pid: 1,
model_thread_id: 1,
regs: other_regs,
},
KboxlikeThreadSnapshot {
host_pid: 4321,
model_pid: 7,
model_thread_id: 7,
regs: target_regs,
},
];
let mut writer = FakeRegisterWriter::default();
let summary =
apply_thread_register_restore_for_tracee(&threads, 4321, &mut writer).unwrap();
assert_eq!(
summary,
TraceeRegisterRestoreSummary {
registers_restored: 1,
other_tracee: 1,
}
);
assert_eq!(writer.writes.len(), 1);
assert_eq!(writer.writes[0].rip, 0x7f00_3000);
assert_eq!(writer.writes[0].rsp, 0x7fff_0000);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn thread_register_restore_converts_kernel_restart_result_to_eintr() {
let mut regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
regs.rip = 0x7f00_3000;
regs.rax = (-512i64) as u64;
let threads = vec![KboxlikeThreadSnapshot {
host_pid: 4321,
model_pid: 7,
model_thread_id: 7,
regs,
}];
let mut writer = FakeRegisterWriter::default();
apply_thread_register_restore_for_tracee(&threads, 4321, &mut writer).unwrap();
assert_eq!(writer.writes.len(), 1);
assert_eq!(writer.writes[0].rip, 0x7f00_3000);
assert_eq!(writer.writes[0].rax, (-(libc::EINTR as i64)) as u64);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn thread_register_restore_rejects_missing_duplicate_and_writer_failure() {
let regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
let threads = vec![KboxlikeThreadSnapshot {
host_pid: 4321,
model_pid: 7,
model_thread_id: 1,
regs,
}];
let mut writer = FakeRegisterWriter::default();
assert_eq!(
apply_thread_register_restore_for_tracee(&threads, 0, &mut writer).unwrap_err(),
FdError::TraceeInstallFailed("invalid tracee pid")
);
assert_eq!(
apply_thread_register_restore_for_tracee(&threads, 5000, &mut writer).unwrap_err(),
FdError::TraceeInstallFailed("missing tracee register snapshot")
);
let duplicate_threads = vec![
KboxlikeThreadSnapshot {
host_pid: 4321,
model_pid: 7,
model_thread_id: 1,
regs,
},
KboxlikeThreadSnapshot {
host_pid: 4321,
model_pid: 7,
model_thread_id: 2,
regs,
},
];
assert_eq!(
apply_thread_register_restore_for_tracee(&duplicate_threads, 4321, &mut writer)
.unwrap_err(),
FdError::TraceeInstallFailed("duplicate tracee register snapshot")
);
let mut failing_writer = FakeRegisterWriter {
fail: true,
..FakeRegisterWriter::default()
};
assert_eq!(
apply_thread_register_restore_for_tracee(&threads, 4321, &mut failing_writer)
.unwrap_err(),
FdError::TraceeInstallFailed("ptrace setregs failed")
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn thread_kernel_state_restore_applies_matching_tracee_state() {
let states = vec![
KboxlikeThreadKernelStateSnapshot {
host_pid: 1111,
model_pid: 7,
model_thread_id: 1,
clear_child_tid: None,
robust_list_head: Some(0x7100),
robust_list_len: 24,
rseq: None,
},
KboxlikeThreadKernelStateSnapshot {
host_pid: 4321,
model_pid: 7,
model_thread_id: 2,
clear_child_tid: Some(0x7000),
robust_list_head: Some(0x8000),
robust_list_len: 24,
rseq: None,
},
];
let mut tracee = FakeTracee::default();
let summary =
apply_thread_kernel_state_restore_for_tracee(&states, 4321, &mut tracee).unwrap();
assert_eq!(
summary,
TraceeKernelStateRestoreSummary {
clear_child_tid_applied: 1,
robust_list_applied: 1,
rseq_applied: 0,
other_tracee: 1,
}
);
assert_eq!(
tracee.calls,
vec![
TraceeSyscall::new3(LINUX_X86_64_SYS_SET_TID_ADDRESS, 0x7000, 0, 0),
TraceeSyscall::new3(LINUX_X86_64_SYS_SET_ROBUST_LIST, 0x8000, 24, 0),
]
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn thread_kernel_state_restore_rejects_missing_duplicate_and_failed_syscall() {
let states = vec![KboxlikeThreadKernelStateSnapshot {
host_pid: 4321,
model_pid: 7,
model_thread_id: 1,
clear_child_tid: Some(0x7000),
robust_list_head: Some(0x8000),
robust_list_len: 24,
rseq: None,
}];
let mut tracee = FakeTracee::default();
assert_eq!(
apply_thread_kernel_state_restore_for_tracee(&states, 0, &mut tracee).unwrap_err(),
FdError::TraceeInstallFailed("invalid tracee pid")
);
assert_eq!(
apply_thread_kernel_state_restore_for_tracee(&states, 5000, &mut tracee).unwrap_err(),
FdError::TraceeInstallFailed("missing tracee kernel state snapshot")
);
let duplicate_states = vec![
KboxlikeThreadKernelStateSnapshot {
host_pid: 4321,
model_pid: 7,
model_thread_id: 1,
clear_child_tid: Some(0x7000),
robust_list_head: None,
robust_list_len: 0,
rseq: None,
},
KboxlikeThreadKernelStateSnapshot {
host_pid: 4321,
model_pid: 7,
model_thread_id: 2,
clear_child_tid: None,
robust_list_head: Some(0x8000),
robust_list_len: 24,
rseq: None,
},
];
assert_eq!(
apply_thread_kernel_state_restore_for_tracee(&duplicate_states, 4321, &mut tracee)
.unwrap_err(),
FdError::TraceeInstallFailed("duplicate tracee kernel state snapshot")
);
let mut clear_tid_fails = FakeTracee::with_returns([-22]);
assert_eq!(
apply_thread_kernel_state_restore_for_tracee(&states, 4321, &mut clear_tid_fails)
.unwrap_err(),
FdError::TraceeInstallFailed("tracee set_tid_address failed")
);
let mut robust_fails = FakeTracee::with_returns([4321, -22]);
assert_eq!(
apply_thread_kernel_state_restore_for_tracee(&states, 4321, &mut robust_fails)
.unwrap_err(),
FdError::TraceeInstallFailed("tracee set_robust_list failed")
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn stopped_tracee_restore_applies_memory_before_registers() {
let mut regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
regs.rip = 0x7f00_3000;
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: 4321,
model_pid: 7,
start: 0x7f00_3000,
prot: libc::PROT_READ,
bytes: b"code".to_vec(),
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate { label: None },
}],
};
let threads = vec![KboxlikeThreadSnapshot {
host_pid: 4321,
model_pid: 7,
model_thread_id: 1,
regs,
}];
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::with_returns([0x7f00_3000, 0]);
let mut register_writer = FakeRegisterWriter::default();
let summary = apply_stopped_tracee_restore(
&restore,
&threads,
4321,
&mut memory,
&mut tracee,
&mut register_writer,
)
.unwrap();
assert_eq!(
summary,
TraceeRestoreSummary {
memory: TraceeMemoryRestoreSummary {
byte_backed_applied: 1,
clean_file_remapped: 0,
shared_object_remapped: 0,
skipped_no_mapping: 0,
deferred_unsupported: 0,
other_tracee: 0,
},
registers: TraceeRegisterRestoreSummary {
registers_restored: 1,
other_tracee: 0,
},
}
);
assert_eq!(memory.writes, vec![(0x7f00_3000, b"code".to_vec())]);
assert_eq!(register_writer.writes.len(), 1);
assert_eq!(register_writer.writes[0].rip, 0x7f00_3000);
assert_eq!(
tracee.calls,
vec![
TraceeByteBackedMapping::new(0x7f00_3000, libc::PROT_READ, b"code".to_vec())
.unwrap()
.mmap_syscall(),
TraceeByteBackedMapping::new(0x7f00_3000, libc::PROT_READ, b"code".to_vec())
.unwrap()
.mprotect_syscall(),
]
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn stopped_tracee_restore_does_not_write_registers_after_memory_failure() {
let regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: 4321,
model_pid: 7,
start: 0x7f00_3000,
prot: libc::PROT_READ,
bytes: b"code".to_vec(),
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate { label: None },
}],
};
let threads = vec![KboxlikeThreadSnapshot {
host_pid: 4321,
model_pid: 7,
model_thread_id: 1,
regs,
}];
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::with_returns([-12]);
let mut register_writer = FakeRegisterWriter::default();
assert_eq!(
apply_stopped_tracee_restore(
&restore,
&threads,
4321,
&mut memory,
&mut tracee,
&mut register_writer,
)
.unwrap_err(),
FdError::TraceeInstallFailed("tracee fixed mmap failed")
);
assert!(memory.writes.is_empty());
assert!(register_writer.writes.is_empty());
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn stopped_tracee_restore_set_builds_shared_objects_once_and_restores_in_pid_order() {
let mut regs_a: libc::user_regs_struct = unsafe { std::mem::zeroed() };
regs_a.rip = 0x7f00_3000;
let mut regs_b: libc::user_regs_struct = unsafe { std::mem::zeroed() };
regs_b.rip = 0x7f00_3000;
let shared_action_for =
|host_pid| KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject {
host_pid,
model_pid: host_pid as u64,
start: 0x7f00_3000,
len: 4,
prot: libc::PROT_READ,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:kboxlike-set-shared".to_string()),
file_offset: 0,
dev: "00:00".to_string(),
inode: 99,
};
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![shared_action_for(2002), shared_action_for(2001)],
};
let threads = vec![
KboxlikeThreadSnapshot {
host_pid: 2002,
model_pid: 2002,
model_thread_id: 2002,
regs: regs_b,
},
KboxlikeThreadSnapshot {
host_pid: 2001,
model_pid: 2001,
model_thread_id: 2001,
regs: regs_a,
},
];
let dumps = vec![KboxlikeSharedMemoryDump {
host_pid: 2001,
model_pid: 2001,
start: 0x7f00_3000,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:kboxlike-set-shared".to_string()),
file_offset: 0,
dev: "00:00".to_string(),
inode: 99,
bytes: b"code".to_vec(),
}];
let mut factory = FakeRestoreAccessFactory::default();
factory
.returns_by_pid
.insert(2001, vec![0x9000, 901, 0x7f00_3000, 0, 0]);
factory
.returns_by_pid
.insert(2002, vec![0x9000, 902, 0x7f00_3000, 0, 0]);
let summary =
apply_stopped_tracee_restore_set(&restore, &threads, &dumps, 1234, &mut factory)
.unwrap();
assert_eq!(factory.requested, vec![2001, 2002]);
assert_eq!(
summary,
TraceeRestoreSetSummary {
tracees_restored: 2,
shared_objects: TraceeSharedObjectRegistryBuildSummary {
created_objects: 1,
initialized_extents: 1,
skipped_unsupported_objects: 0,
},
memory: TraceeMemoryRestoreSummary {
byte_backed_applied: 0,
clean_file_remapped: 0,
shared_object_remapped: 2,
skipped_no_mapping: 0,
deferred_unsupported: 0,
other_tracee: 2,
},
registers: TraceeRegisterRestoreSummary {
registers_restored: 2,
other_tracee: 2,
},
}
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn replacement_tracee_set_creation_groups_threads_by_model_process() {
let regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
let threads = vec![
KboxlikeThreadSnapshot {
host_pid: 2222,
model_pid: 7,
model_thread_id: 2,
regs,
},
KboxlikeThreadSnapshot {
host_pid: 3333,
model_pid: 8,
model_thread_id: 3,
regs,
},
KboxlikeThreadSnapshot {
host_pid: 1111,
model_pid: 7,
model_thread_id: 1,
regs,
},
];
let mut factory = FakeStoppedTraceeReplacementFactory {
returns: VecDeque::from([
Ok(BTreeMap::from([(1, 7001), (2, 7002)])),
Ok(BTreeMap::from([(3, 8001)])),
]),
..FakeStoppedTraceeReplacementFactory::default()
};
let created = create_stopped_replacement_tracee_set(&threads, &mut factory).unwrap();
assert_eq!(factory.calls, vec![(7, vec![1, 2]), (8, vec![3])]);
assert_eq!(
created,
TraceeReplacementThreadGroupCreateSet {
replacement_host_pids: BTreeMap::from([(1, 7001), (2, 7002), (3, 8001)]),
summary: TraceeReplacementThreadGroupCreateSummary {
process_groups: 2,
tracees_requested: 3,
tracees_created: 3,
},
}
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn replacement_tracee_set_creation_rejects_incomplete_or_ambiguous_groups() {
let regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
let threads = vec![
KboxlikeThreadSnapshot {
host_pid: 1111,
model_pid: 7,
model_thread_id: 1,
regs,
},
KboxlikeThreadSnapshot {
host_pid: 2222,
model_pid: 7,
model_thread_id: 2,
regs,
},
];
let mut missing = FakeStoppedTraceeReplacementFactory {
returns: VecDeque::from([Ok(BTreeMap::from([(1, 7001)]))]),
..FakeStoppedTraceeReplacementFactory::default()
};
assert_eq!(
create_stopped_replacement_tracee_set(&threads, &mut missing).unwrap_err(),
FdError::TraceeInstallFailed("replacement group missing model thread id")
);
let mut unknown = FakeStoppedTraceeReplacementFactory {
returns: VecDeque::from([Ok(BTreeMap::from([(1, 7001), (2, 7002), (9, 9001)]))]),
..FakeStoppedTraceeReplacementFactory::default()
};
assert_eq!(
create_stopped_replacement_tracee_set(&threads, &mut unknown).unwrap_err(),
FdError::TraceeInstallFailed("replacement group returned unknown model thread id")
);
let mut invalid_pid = FakeStoppedTraceeReplacementFactory {
returns: VecDeque::from([Ok(BTreeMap::from([(1, 7001), (2, 0)]))]),
..FakeStoppedTraceeReplacementFactory::default()
};
assert_eq!(
create_stopped_replacement_tracee_set(&threads, &mut invalid_pid).unwrap_err(),
FdError::TraceeInstallFailed("invalid replacement tracee pid")
);
let mut duplicate_pid = FakeStoppedTraceeReplacementFactory {
returns: VecDeque::from([Ok(BTreeMap::from([(1, 7001), (2, 7001)]))]),
..FakeStoppedTraceeReplacementFactory::default()
};
assert_eq!(
create_stopped_replacement_tracee_set(&threads, &mut duplicate_pid).unwrap_err(),
FdError::TraceeInstallFailed("duplicate replacement tracee pid")
);
let duplicate_threads = vec![
KboxlikeThreadSnapshot {
host_pid: 1111,
model_pid: 7,
model_thread_id: 1,
regs,
},
KboxlikeThreadSnapshot {
host_pid: 2222,
model_pid: 8,
model_thread_id: 1,
regs,
},
];
let mut factory = FakeStoppedTraceeReplacementFactory::default();
assert_eq!(
create_stopped_replacement_tracee_set(&duplicate_threads, &mut factory).unwrap_err(),
FdError::TraceeInstallFailed("duplicate tracee model thread id")
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn linux_replacement_factory_rejects_empty_groups_before_forking() {
let mut factory = LinuxStoppedTraceeReplacementFactory::default();
assert_eq!(
factory
.create_stopped_replacement_group(7, &[])
.unwrap_err(),
FdError::TraceeInstallFailed("replacement group has no threads")
);
assert!(factory.created_host_pids().is_empty());
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn tracee_wait_retries_interrupted_waitpid() {
let mut calls = 0;
let status = waitpid_tracee_blocking_with(1234, |status| {
calls += 1;
if calls == 1 {
unsafe {
*libc::__errno_location() = libc::EINTR;
}
return -1;
}
unsafe {
*status = libc::SIGTRAP << 8 | 0x7f;
}
1234
})
.expect("interrupted waitpid is retried");
assert_eq!(calls, 2);
assert!(libc::WIFSTOPPED(status));
assert_eq!(libc::WSTOPSIG(status), libc::SIGTRAP);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn thread_group_memory_normalize_assigns_one_memory_owner_per_model_process() {
let regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
let threads = vec![
KboxlikeThreadSnapshot {
host_pid: 2222,
model_pid: 7,
model_thread_id: 2,
regs,
},
KboxlikeThreadSnapshot {
host_pid: 1111,
model_pid: 7,
model_thread_id: 1,
regs,
},
KboxlikeThreadSnapshot {
host_pid: 3333,
model_pid: 8,
model_thread_id: 3,
regs,
},
];
let duplicate_thread_group_action =
|host_pid| KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid,
model_pid: 7,
start: 0x7f00_3000,
prot: libc::PROT_READ,
bytes: b"code".to_vec(),
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate { label: None },
};
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![
duplicate_thread_group_action(2222),
duplicate_thread_group_action(1111),
KboxlikeMaterializedMemoryRestoreAction::RemapCleanFile {
host_pid: 3333,
model_pid: 8,
start: 0x7f00_4000,
len: 0x1000,
prot: libc::PROT_READ,
path: PathBuf::from("/usr/bin/node"),
file_offset: 0,
dev: "08:01".to_string(),
inode: 55,
},
],
};
let duplicate_thread_group_dump = |host_pid| KboxlikeSharedMemoryDump {
host_pid,
model_pid: 7,
start: 0x7f00_5000,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:thread-group".to_string()),
file_offset: 0,
dev: "00:01".to_string(),
inode: 77,
bytes: b"shared".to_vec(),
};
let dumps = vec![
duplicate_thread_group_dump(2222),
duplicate_thread_group_dump(1111),
KboxlikeSharedMemoryDump {
host_pid: 3333,
model_pid: 8,
start: 0x7f00_6000,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:other-process".to_string()),
file_offset: 0,
dev: "00:01".to_string(),
inode: 88,
bytes: b"other".to_vec(),
},
];
let normalized = normalize_thread_group_memory_restore(&restore, &threads, &dumps).unwrap();
assert_eq!(
normalized.memory_owner_host_pids,
BTreeMap::from([(7, 1111), (8, 3333)])
);
assert_eq!(
normalized.summary,
TraceeThreadGroupMemoryNormalizeSummary {
process_groups: 2,
memory_actions_retained: 2,
memory_actions_reassigned_to_owner: 1,
duplicate_memory_actions_removed: 1,
shared_dumps_retained: 2,
shared_dumps_reassigned_to_owner: 1,
duplicate_shared_dumps_removed: 1,
}
);
assert_eq!(normalized.restore.actions.len(), 2);
assert_eq!(
normalized
.restore
.actions
.iter()
.map(materialized_action_host_pid)
.collect::<Vec<_>>(),
vec![1111, 3333]
);
assert_eq!(
normalized
.shared_dumps
.iter()
.map(|dump| dump.host_pid)
.collect::<Vec<_>>(),
vec![1111, 3333]
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn thread_group_memory_normalize_rejects_inconsistent_restore_metadata() {
let regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
let threads = vec![KboxlikeThreadSnapshot {
host_pid: 1111,
model_pid: 7,
model_thread_id: 1,
regs,
}];
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: 1111,
model_pid: 8,
start: 0x7f00_3000,
prot: libc::PROT_READ,
bytes: b"code".to_vec(),
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate { label: None },
}],
};
assert_eq!(
normalize_thread_group_memory_restore(&restore, &threads, &[]).unwrap_err(),
FdError::TraceeInstallFailed("memory restore action model pid mismatch")
);
let duplicate_threads = vec![
KboxlikeThreadSnapshot {
host_pid: 1111,
model_pid: 7,
model_thread_id: 1,
regs,
},
KboxlikeThreadSnapshot {
host_pid: 2222,
model_pid: 7,
model_thread_id: 1,
regs,
},
];
assert_eq!(
normalize_thread_group_memory_restore(
&KboxlikeMaterializedMemoryRestore { actions: vec![] },
&duplicate_threads,
&[],
)
.unwrap_err(),
FdError::TraceeInstallFailed("duplicate tracee model thread id")
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn replacement_pid_remap_rewrites_restore_inputs_by_model_thread_id() {
let mut regs_a: libc::user_regs_struct = unsafe { std::mem::zeroed() };
regs_a.rip = 0x7f00_3000;
let mut regs_b: libc::user_regs_struct = unsafe { std::mem::zeroed() };
regs_b.rip = 0x7f00_4000;
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![
KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: 1111,
model_pid: 7,
start: 0x7f00_3000,
prot: libc::PROT_READ,
bytes: b"code-a".to_vec(),
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate { label: None },
},
KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject {
host_pid: 2222,
model_pid: 7,
start: 0x7f00_4000,
len: 6,
prot: libc::PROT_READ,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:kboxlike-remap".to_string()),
file_offset: 0,
dev: "00:00".to_string(),
inode: 44,
},
],
};
let threads = vec![
KboxlikeThreadSnapshot {
host_pid: 1111,
model_pid: 7,
model_thread_id: 1,
regs: regs_a,
},
KboxlikeThreadSnapshot {
host_pid: 2222,
model_pid: 7,
model_thread_id: 2,
regs: regs_b,
},
];
let dumps = vec![KboxlikeSharedMemoryDump {
host_pid: 2222,
model_pid: 7,
start: 0x7f00_4000,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:kboxlike-remap".to_string()),
file_offset: 0,
dev: "00:00".to_string(),
inode: 44,
bytes: b"code-b".to_vec(),
}];
let replacement_host_pids = BTreeMap::from([(1, 7001), (2, 8001)]);
let remapped = remap_stopped_tracee_restore_set_to_replacement_pids(
&restore,
&threads,
&dumps,
&replacement_host_pids,
)
.unwrap();
assert_eq!(
remapped.summary,
TraceeReplacementPidRemapSummary {
threads_remapped: 2,
memory_actions_remapped: 2,
shared_dumps_remapped: 1,
}
);
assert_eq!(
remapped
.threads
.iter()
.map(|thread| {
(
thread.model_pid,
thread.model_thread_id,
thread.host_pid,
thread.regs.rip,
)
})
.collect::<Vec<_>>(),
vec![(7, 1, 7001, 0x7f00_3000), (7, 2, 8001, 0x7f00_4000)]
);
assert_eq!(
materialized_action_host_pid(&remapped.restore.actions[0]),
7001
);
assert_eq!(
materialized_action_model_pid(&remapped.restore.actions[0]),
7
);
assert_eq!(
materialized_action_host_pid(&remapped.restore.actions[1]),
8001
);
assert_eq!(
materialized_action_model_pid(&remapped.restore.actions[1]),
7
);
assert_eq!(remapped.shared_dumps[0].host_pid, 8001);
assert_eq!(remapped.shared_dumps[0].model_pid, 7);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn replacement_pid_remap_rewrites_kernel_state_by_model_thread_id() {
let states = vec![
KboxlikeThreadKernelStateSnapshot {
host_pid: 1111,
model_pid: 7,
model_thread_id: 1,
clear_child_tid: Some(0x7000),
robust_list_head: Some(0x7100),
robust_list_len: 24,
rseq: None,
},
KboxlikeThreadKernelStateSnapshot {
host_pid: 2222,
model_pid: 7,
model_thread_id: 2,
clear_child_tid: None,
robust_list_head: Some(0x8100),
robust_list_len: 24,
rseq: None,
},
];
let replacement_host_pids = BTreeMap::from([(1, 7001), (2, 8001)]);
let remapped =
remap_thread_kernel_states_to_replacement_pids(&states, &replacement_host_pids)
.unwrap();
assert_eq!(
remapped
.iter()
.map(|state| {
(
state.host_pid,
state.model_pid,
state.model_thread_id,
state.clear_child_tid,
state.robust_list_head,
state.robust_list_len,
)
})
.collect::<Vec<_>>(),
vec![
(7001, 7, 1, Some(0x7000), Some(0x7100), 24),
(8001, 7, 2, None, Some(0x8100), 24),
]
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn replacement_pid_remap_rejects_ambiguous_or_missing_replacements() {
let regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: 1111,
model_pid: 7,
start: 0x7f00_3000,
prot: libc::PROT_READ,
bytes: b"code".to_vec(),
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate { label: None },
}],
};
let threads = vec![KboxlikeThreadSnapshot {
host_pid: 1111,
model_pid: 7,
model_thread_id: 1,
regs,
}];
assert_eq!(
remap_stopped_tracee_restore_set_to_replacement_pids(
&restore,
&threads,
&[],
&BTreeMap::new(),
)
.unwrap_err(),
FdError::TraceeInstallFailed("missing replacement tracee pid")
);
assert_eq!(
remap_stopped_tracee_restore_set_to_replacement_pids(
&restore,
&threads,
&[],
&BTreeMap::from([(1, 0)]),
)
.unwrap_err(),
FdError::TraceeInstallFailed("invalid replacement tracee pid")
);
assert_eq!(
remap_stopped_tracee_restore_set_to_replacement_pids(
&restore,
&threads,
&[],
&BTreeMap::from([(1, 7001), (2, 7001)]),
)
.unwrap_err(),
FdError::TraceeInstallFailed("duplicate replacement tracee pid")
);
let duplicate_threads = vec![
KboxlikeThreadSnapshot {
host_pid: 1111,
model_pid: 7,
model_thread_id: 1,
regs,
},
KboxlikeThreadSnapshot {
host_pid: 1112,
model_pid: 7,
model_thread_id: 1,
regs,
},
];
assert_eq!(
remap_stopped_tracee_restore_set_to_replacement_pids(
&restore,
&duplicate_threads,
&[],
&BTreeMap::from([(1, 7001)]),
)
.unwrap_err(),
FdError::TraceeInstallFailed("duplicate tracee model thread id")
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn stopped_tracee_restore_and_resume_set_restores_all_before_resuming_in_pid_order() {
let mut regs_a: libc::user_regs_struct = unsafe { std::mem::zeroed() };
regs_a.rip = 0x7f00_3000;
let mut regs_b: libc::user_regs_struct = unsafe { std::mem::zeroed() };
regs_b.rip = 0x7f00_3000;
let shared_action_for =
|host_pid| KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject {
host_pid,
model_pid: host_pid as u64,
start: 0x7f00_3000,
len: 4,
prot: libc::PROT_READ,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:kboxlike-set-shared".to_string()),
file_offset: 0,
dev: "00:00".to_string(),
inode: 99,
};
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![shared_action_for(2002), shared_action_for(2001)],
};
let threads = vec![
KboxlikeThreadSnapshot {
host_pid: 2002,
model_pid: 2002,
model_thread_id: 2002,
regs: regs_b,
},
KboxlikeThreadSnapshot {
host_pid: 2001,
model_pid: 2001,
model_thread_id: 2001,
regs: regs_a,
},
];
let dumps = vec![KboxlikeSharedMemoryDump {
host_pid: 2001,
model_pid: 2001,
start: 0x7f00_3000,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:kboxlike-set-shared".to_string()),
file_offset: 0,
dev: "00:00".to_string(),
inode: 99,
bytes: b"code".to_vec(),
}];
let mut factory = FakeRestoreAccessFactory::default();
factory
.returns_by_pid
.insert(2001, vec![0x9000, 901, 0x7f00_3000, 0, 0]);
factory
.returns_by_pid
.insert(2002, vec![0x9000, 902, 0x7f00_3000, 0, 0]);
let mut resumer = FakeRestoredTraceeResumer::default();
let summary = apply_and_resume_stopped_tracee_restore_set(
&restore,
&threads,
&dumps,
1234,
&mut factory,
&mut resumer,
)
.unwrap();
assert_eq!(factory.requested, vec![2001, 2002]);
assert_eq!(resumer.resumed, vec![2001, 2002]);
assert_eq!(
summary,
TraceeRestoreAndResumeSetSummary {
restore: TraceeRestoreSetSummary {
tracees_restored: 2,
shared_objects: TraceeSharedObjectRegistryBuildSummary {
created_objects: 1,
initialized_extents: 1,
skipped_unsupported_objects: 0,
},
memory: TraceeMemoryRestoreSummary {
byte_backed_applied: 0,
clean_file_remapped: 0,
shared_object_remapped: 2,
skipped_no_mapping: 0,
deferred_unsupported: 0,
other_tracee: 2,
},
registers: TraceeRegisterRestoreSummary {
registers_restored: 2,
other_tracee: 2,
},
},
resume: TraceeResumeSetSummary { tracees_resumed: 2 },
}
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn stopped_tracee_restore_and_resume_set_does_not_resume_after_restore_failure() {
let regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: 4321,
model_pid: 7,
start: 0x7f00_3000,
prot: libc::PROT_READ,
bytes: b"code".to_vec(),
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate { label: None },
}],
};
let threads = vec![KboxlikeThreadSnapshot {
host_pid: 4321,
model_pid: 7,
model_thread_id: 1,
regs,
}];
let mut factory = FakeRestoreAccessFactory::default();
let mut resumer = FakeRestoredTraceeResumer::default();
assert_eq!(
apply_and_resume_stopped_tracee_restore_set(
&restore,
&threads,
&[],
1234,
&mut factory,
&mut resumer,
)
.unwrap_err(),
FdError::TraceeInstallFailed("missing fake tracee returns")
);
assert!(resumer.resumed.is_empty());
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn full_replacement_restore_applies_memory_kernel_registers_then_resume() {
let mut regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
regs.rip = 0x7f00_3000;
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: 4321,
model_pid: 7,
start: 0x7f00_3000,
prot: libc::PROT_READ | libc::PROT_WRITE,
bytes: b"code".to_vec(),
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate { label: None },
}],
};
let threads = vec![KboxlikeThreadSnapshot {
host_pid: 4321,
model_pid: 7,
model_thread_id: 1,
regs,
}];
let kernel_states = vec![KboxlikeThreadKernelStateSnapshot {
host_pid: 4321,
model_pid: 7,
model_thread_id: 1,
clear_child_tid: Some(0x7f00_4000),
robust_list_head: Some(0x7f00_5000),
robust_list_len: 24,
rseq: None,
}];
let replacement_host_pids = BTreeMap::from([(1, 9001)]);
let mut factory = FakeRestoreAccessFactory::default();
factory
.returns_by_pid
.insert(9001, vec![0x7f00_3000, 9001, 0]);
let mut resumer = FakeRestoredTraceeResumer::default();
let summary = apply_and_resume_stopped_tracee_full_restore_set_with_replacement_pids(
&restore,
&threads,
&kernel_states,
&[],
&replacement_host_pids,
1234,
&mut factory,
&mut resumer,
)
.unwrap();
assert_eq!(factory.requested, vec![9001]);
assert_eq!(resumer.resumed, vec![9001]);
assert_eq!(
summary,
TraceeFullRestoreAndResumeSetSummary {
restore: TraceeFullRestoreSetSummary {
tracees_restored: 1,
shared_objects: TraceeSharedObjectRegistryBuildSummary::default(),
memory: TraceeMemoryRestoreSummary {
byte_backed_applied: 1,
clean_file_remapped: 0,
shared_object_remapped: 0,
skipped_no_mapping: 0,
deferred_unsupported: 0,
other_tracee: 0,
},
kernel_state: TraceeKernelStateRestoreSummary {
clear_child_tid_applied: 1,
robust_list_applied: 1,
rseq_applied: 0,
other_tracee: 0,
},
registers: TraceeRegisterRestoreSummary {
registers_restored: 1,
other_tracee: 0,
},
},
resume: TraceeResumeSetSummary { tracees_resumed: 1 },
}
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn stopped_tracee_restore_set_rejects_unmatched_or_invalid_tracees() {
let regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: 4321,
model_pid: 7,
start: 0x7f00_3000,
prot: libc::PROT_READ,
bytes: b"code".to_vec(),
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate { label: None },
}],
};
let mut factory = FakeRestoreAccessFactory::default();
assert_eq!(
apply_stopped_tracee_restore_set(&restore, &[], &[], 1234, &mut factory).unwrap_err(),
FdError::TraceeInstallFailed("no tracee register snapshots")
);
let invalid_threads = vec![KboxlikeThreadSnapshot {
host_pid: 0,
model_pid: 7,
model_thread_id: 1,
regs,
}];
assert_eq!(
apply_stopped_tracee_restore_set(&restore, &invalid_threads, &[], 1234, &mut factory)
.unwrap_err(),
FdError::TraceeInstallFailed("invalid tracee register snapshot pid")
);
let unmatched_threads = vec![KboxlikeThreadSnapshot {
host_pid: 5000,
model_pid: 7,
model_thread_id: 1,
regs,
}];
assert_eq!(
apply_stopped_tracee_restore_set(&restore, &unmatched_threads, &[], 1234, &mut factory)
.unwrap_err(),
FdError::TraceeInstallFailed("memory restore action without tracee register snapshot")
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission and a Linux x86_64 tracee"]
fn live_materialized_memory_restore_replays_bytes_into_stopped_child() {
const ADDR: u64 = 0x7000_0000_0000;
const BYTES: &[u8] = b"kboxlike-byte-replay";
let child = unsafe { libc::fork() };
assert!(child >= 0, "fork failed");
if child == 0 {
unsafe {
let ret = libc::ptrace(
libc::PTRACE_TRACEME,
0,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_void>(),
);
if ret < 0 {
libc::_exit(111);
}
libc::raise(libc::SIGSTOP);
let restored = std::slice::from_raw_parts(ADDR as *const u8, BYTES.len());
if restored == BYTES {
libc::_exit(0);
}
libc::_exit(113);
}
}
let result: Result<(), FdError> = (|| {
let mut status = 0;
let waited = unsafe { libc::waitpid(child, &mut status, 0) };
if waited != child {
return Err(FdError::TraceeInstallFailed("initial tracee wait failed"));
}
if !libc::WIFSTOPPED(status) || libc::WSTOPSIG(status) != libc::SIGSTOP {
return Err(FdError::TraceeInstallFailed(
"tracee did not stop at initial SIGSTOP",
));
}
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: child,
model_pid: 1,
start: ADDR,
prot: libc::PROT_READ,
bytes: BYTES.to_vec(),
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate { label: None },
}],
};
let mut writer = ProcessVmMemoryWriter::new(child)?;
let mut executor = PtraceSyscallExecutor::new(child)?;
let summary = apply_materialized_memory_restore_for_tracee(
&restore,
child,
&mut writer,
&mut executor,
)?;
assert_eq!(
summary,
TraceeMemoryRestoreSummary {
byte_backed_applied: 1,
clean_file_remapped: 0,
shared_object_remapped: 0,
skipped_no_mapping: 0,
deferred_unsupported: 0,
other_tracee: 0,
}
);
let detach_ret = unsafe {
libc::ptrace(
libc::PTRACE_DETACH,
child,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_void>(),
)
};
if detach_ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace detach failed"));
}
let waited = unsafe { libc::waitpid(child, &mut status, 0) };
if waited != child {
return Err(FdError::TraceeInstallFailed("final tracee wait failed"));
}
if libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0 {
return Ok(());
}
Err(FdError::TraceeInstallFailed(
"tracee did not observe restored bytes",
))
})();
if result.is_err() {
unsafe {
libc::kill(child, libc::SIGKILL);
let mut status = 0;
libc::waitpid(child, &mut status, 0);
}
}
result.unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission and a Linux x86_64 tracee"]
fn live_thread_register_restore_redirects_stopped_child_to_restored_rip() {
const ADDR: u64 = 0x7400_0000_0000;
const EXIT_CODE: i32 = 42;
const EXIT_42_STUB: &[u8] = &[
0xb8, 0x3c, 0x00, 0x00, 0x00, 0xbf, 0x2a, 0x00, 0x00, 0x00, 0x0f, 0x05, ];
let child = unsafe { libc::fork() };
assert!(child >= 0, "fork failed");
if child == 0 {
unsafe {
let ret = libc::ptrace(
libc::PTRACE_TRACEME,
0,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_void>(),
);
if ret < 0 {
libc::_exit(111);
}
libc::raise(libc::SIGSTOP);
libc::_exit(113);
}
}
let result: Result<(), FdError> = (|| {
let mut status = 0;
let waited = unsafe { libc::waitpid(child, &mut status, 0) };
if waited != child {
return Err(FdError::TraceeInstallFailed("initial tracee wait failed"));
}
if !libc::WIFSTOPPED(status) || libc::WSTOPSIG(status) != libc::SIGSTOP {
return Err(FdError::TraceeInstallFailed(
"tracee did not stop at initial SIGSTOP",
));
}
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: child,
model_pid: 1,
start: ADDR,
prot: libc::PROT_READ | libc::PROT_EXEC,
bytes: EXIT_42_STUB.to_vec(),
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate { label: None },
}],
};
let mut writer = ProcessVmMemoryWriter::new(child)?;
let mut executor = PtraceSyscallExecutor::new(child)?;
apply_materialized_memory_restore_for_tracee(
&restore,
child,
&mut writer,
&mut executor,
)?;
let mut restored_regs = executor.getregs()?;
restored_regs.rip = ADDR;
let threads = vec![KboxlikeThreadSnapshot {
host_pid: child,
model_pid: 1,
model_thread_id: 1,
regs: restored_regs,
}];
let mut register_writer = PtraceRegisterWriter::new(child)?;
let summary =
apply_thread_register_restore_for_tracee(&threads, child, &mut register_writer)?;
assert_eq!(
summary,
TraceeRegisterRestoreSummary {
registers_restored: 1,
other_tracee: 0,
}
);
let detach_ret = unsafe {
libc::ptrace(
libc::PTRACE_DETACH,
child,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_void>(),
)
};
if detach_ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace detach failed"));
}
let waited = unsafe { libc::waitpid(child, &mut status, 0) };
if waited != child {
return Err(FdError::TraceeInstallFailed("final tracee wait failed"));
}
if libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == EXIT_CODE {
return Ok(());
}
Err(FdError::TraceeInstallFailed(
"tracee did not execute restored instruction pointer",
))
})();
if result.is_err() {
unsafe {
libc::kill(child, libc::SIGKILL);
let mut status = 0;
libc::waitpid(child, &mut status, 0);
}
}
result.unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission and a Linux x86_64 tracee"]
fn live_stopped_tracee_restore_replays_memory_then_registers() {
const ADDR: u64 = 0x7500_0000_0000;
const EXIT_CODE: i32 = 43;
const EXIT_43_STUB: &[u8] = &[
0xb8, 0x3c, 0x00, 0x00, 0x00, 0xbf, 0x2b, 0x00, 0x00, 0x00, 0x0f, 0x05, ];
let child = unsafe { libc::fork() };
assert!(child >= 0, "fork failed");
if child == 0 {
unsafe {
let ret = libc::ptrace(
libc::PTRACE_TRACEME,
0,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_void>(),
);
if ret < 0 {
libc::_exit(111);
}
libc::raise(libc::SIGSTOP);
libc::_exit(113);
}
}
let result = (|| {
let mut status = 0;
let waited = unsafe { libc::waitpid(child, &mut status, 0) };
if waited != child {
return Err(FdError::TraceeInstallFailed("initial tracee wait failed"));
}
if !libc::WIFSTOPPED(status) || libc::WSTOPSIG(status) != libc::SIGSTOP {
return Err(FdError::TraceeInstallFailed(
"tracee did not stop at initial SIGSTOP",
));
}
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: child,
model_pid: 1,
start: ADDR,
prot: libc::PROT_READ | libc::PROT_EXEC,
bytes: EXIT_43_STUB.to_vec(),
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate { label: None },
}],
};
let mut writer = ProcessVmMemoryWriter::new(child)?;
let mut executor = PtraceSyscallExecutor::new(child)?;
let mut restored_regs = executor.getregs()?;
restored_regs.rip = ADDR;
let threads = vec![KboxlikeThreadSnapshot {
host_pid: child,
model_pid: 1,
model_thread_id: 1,
regs: restored_regs,
}];
let mut register_writer = PtraceRegisterWriter::new(child)?;
let summary = apply_stopped_tracee_restore(
&restore,
&threads,
child,
&mut writer,
&mut executor,
&mut register_writer,
)?;
assert_eq!(
summary,
TraceeRestoreSummary {
memory: TraceeMemoryRestoreSummary {
byte_backed_applied: 1,
clean_file_remapped: 0,
shared_object_remapped: 0,
skipped_no_mapping: 0,
deferred_unsupported: 0,
other_tracee: 2,
},
registers: TraceeRegisterRestoreSummary {
registers_restored: 1,
other_tracee: 0,
},
}
);
let detach_ret = unsafe {
libc::ptrace(
libc::PTRACE_DETACH,
child,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_void>(),
)
};
if detach_ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace detach failed"));
}
let waited = unsafe { libc::waitpid(child, &mut status, 0) };
if waited != child {
return Err(FdError::TraceeInstallFailed("final tracee wait failed"));
}
if libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == EXIT_CODE {
return Ok(());
}
Err(FdError::TraceeInstallFailed(
"tracee did not execute orchestrated restore",
))
})();
if result.is_err() {
unsafe {
libc::kill(child, libc::SIGKILL);
let mut status = 0;
libc::waitpid(child, &mut status, 0);
}
}
result.unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission and two Linux x86_64 tracees"]
fn live_stopped_tracee_restore_set_materializes_shared_code_once() {
const ADDR: u64 = 0x7600_0000_0000;
const EXIT_CODE: i32 = 44;
const EXIT_44_STUB: &[u8] = &[
0xb8, 0x3c, 0x00, 0x00, 0x00, 0xbf, 0x2c, 0x00, 0x00, 0x00, 0x0f, 0x05, ];
fn fork_stopped_child() -> libc::pid_t {
let child = unsafe { libc::fork() };
assert!(child >= 0, "fork failed");
if child == 0 {
unsafe {
let ret = libc::ptrace(
libc::PTRACE_TRACEME,
0,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_void>(),
);
if ret < 0 {
libc::_exit(111);
}
libc::raise(libc::SIGSTOP);
libc::_exit(113);
}
}
child
}
fn wait_initial_stop(child: libc::pid_t) -> Result<(), FdError> {
let mut status = 0;
let waited = unsafe { libc::waitpid(child, &mut status, 0) };
if waited != child {
return Err(FdError::TraceeInstallFailed("initial tracee wait failed"));
}
if !libc::WIFSTOPPED(status) || libc::WSTOPSIG(status) != libc::SIGSTOP {
return Err(FdError::TraceeInstallFailed(
"tracee did not stop at initial SIGSTOP",
));
}
Ok(())
}
fn wait_exit(child: libc::pid_t, code: i32) -> Result<(), FdError> {
let mut status = 0;
let waited = unsafe { libc::waitpid(child, &mut status, 0) };
if waited != child {
return Err(FdError::TraceeInstallFailed("final tracee wait failed"));
}
if libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == code {
return Ok(());
}
Err(FdError::TraceeInstallFailed(
"tracee did not execute restored shared code",
))
}
let child_a = fork_stopped_child();
let child_b = fork_stopped_child();
let result: Result<(), FdError> = (|| {
wait_initial_stop(child_a)?;
wait_initial_stop(child_b)?;
let shared_action_for =
|host_pid| KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject {
host_pid,
model_pid: host_pid as u64,
start: ADDR,
len: EXIT_44_STUB.len() as u64,
prot: libc::PROT_READ | libc::PROT_EXEC,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:kboxlike-restore-set-code".to_string()),
file_offset: 0,
dev: "00:00".to_string(),
inode: 144,
};
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![shared_action_for(child_a), shared_action_for(child_b)],
};
let dumps = vec![KboxlikeSharedMemoryDump {
host_pid: child_a,
model_pid: child_a as u64,
start: ADDR,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:kboxlike-restore-set-code".to_string()),
file_offset: 0,
dev: "00:00".to_string(),
inode: 144,
bytes: EXIT_44_STUB.to_vec(),
}];
let mut regs_a = PtraceSyscallExecutor::new(child_a)?.getregs()?;
regs_a.rip = ADDR;
let mut regs_b = PtraceSyscallExecutor::new(child_b)?.getregs()?;
regs_b.rip = ADDR;
let threads = vec![
KboxlikeThreadSnapshot {
host_pid: child_a,
model_pid: child_a as u64,
model_thread_id: child_a as u64,
regs: regs_a,
},
KboxlikeThreadSnapshot {
host_pid: child_b,
model_pid: child_b as u64,
model_thread_id: child_b as u64,
regs: regs_b,
},
];
let mut access_factory = LinuxTraceeRestoreAccessFactory;
let mut resumer = PtraceDetachRestoredTraceeResumer;
let summary = apply_and_resume_stopped_tracee_restore_set(
&restore,
&threads,
&dumps,
std::process::id() as i32,
&mut access_factory,
&mut resumer,
)?;
assert_eq!(
summary,
TraceeRestoreAndResumeSetSummary {
restore: TraceeRestoreSetSummary {
tracees_restored: 2,
shared_objects: TraceeSharedObjectRegistryBuildSummary {
created_objects: 1,
initialized_extents: 1,
skipped_unsupported_objects: 0,
},
memory: TraceeMemoryRestoreSummary {
byte_backed_applied: 0,
clean_file_remapped: 0,
shared_object_remapped: 2,
skipped_no_mapping: 0,
deferred_unsupported: 0,
other_tracee: 2,
},
registers: TraceeRegisterRestoreSummary {
registers_restored: 2,
other_tracee: 2,
},
},
resume: TraceeResumeSetSummary { tracees_resumed: 2 },
},
);
wait_exit(child_a, EXIT_CODE)?;
wait_exit(child_b, EXIT_CODE)?;
Ok(())
})();
if result.is_err() {
unsafe {
libc::kill(child_a, libc::SIGKILL);
libc::kill(child_b, libc::SIGKILL);
let mut status = 0;
libc::waitpid(child_a, &mut status, 0);
libc::waitpid(child_b, &mut status, 0);
}
}
result.unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission and a Linux x86_64 replacement tracee"]
fn live_replacement_pid_restore_maps_model_pid_to_new_stopped_child() {
const ADDR: u64 = 0x7700_0000_0000;
const CAPTURED_HOST_PID: i32 = 1111;
const MODEL_PID: ProcessId = 7;
const MODEL_THREAD_ID: ModelThreadId = 1;
const EXIT_CODE: i32 = 45;
const EXIT_45_STUB: &[u8] = &[
0xb8, 0x3c, 0x00, 0x00, 0x00, 0xbf, 0x2d, 0x00, 0x00, 0x00, 0x0f, 0x05, ];
let source_threads = vec![KboxlikeThreadSnapshot {
host_pid: CAPTURED_HOST_PID,
model_pid: MODEL_PID,
model_thread_id: MODEL_THREAD_ID,
regs: unsafe { std::mem::zeroed() },
}];
let mut replacement_factory = LinuxStoppedTraceeReplacementFactory::default();
let result: Result<(), FdError> = (|| {
let created =
create_stopped_replacement_tracee_set(&source_threads, &mut replacement_factory)?;
assert_eq!(
created.summary,
TraceeReplacementThreadGroupCreateSummary {
process_groups: 1,
tracees_requested: 1,
tracees_created: 1,
}
);
assert_eq!(replacement_factory.created_host_pids().len(), 1);
let child = *created.replacement_host_pids.get(&MODEL_THREAD_ID).ok_or(
FdError::TraceeInstallFailed("replacement model thread id was not created"),
)?;
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: CAPTURED_HOST_PID,
model_pid: MODEL_PID,
start: ADDR,
prot: libc::PROT_READ | libc::PROT_EXEC,
bytes: EXIT_45_STUB.to_vec(),
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate { label: None },
}],
};
let mut regs = PtraceSyscallExecutor::new(child)?.getregs()?;
regs.rip = ADDR;
let threads = vec![KboxlikeThreadSnapshot {
host_pid: CAPTURED_HOST_PID,
model_pid: MODEL_PID,
model_thread_id: MODEL_THREAD_ID,
regs,
}];
let mut access_factory = LinuxTraceeRestoreAccessFactory;
let mut resumer = PtraceDetachRestoredTraceeResumer;
let summary = apply_and_resume_stopped_tracee_restore_set_with_replacement_pids(
&restore,
&threads,
&[],
&created.replacement_host_pids,
std::process::id() as i32,
&mut access_factory,
&mut resumer,
)?;
assert_eq!(
summary,
TraceeRestoreAndResumeSetSummary {
restore: TraceeRestoreSetSummary {
tracees_restored: 1,
shared_objects: TraceeSharedObjectRegistryBuildSummary {
created_objects: 0,
initialized_extents: 0,
skipped_unsupported_objects: 0,
},
memory: TraceeMemoryRestoreSummary {
byte_backed_applied: 1,
clean_file_remapped: 0,
shared_object_remapped: 0,
skipped_no_mapping: 0,
deferred_unsupported: 0,
other_tracee: 0,
},
registers: TraceeRegisterRestoreSummary {
registers_restored: 1,
other_tracee: 0,
},
},
resume: TraceeResumeSetSummary { tracees_resumed: 1 },
},
);
let mut status = 0;
let waited = unsafe { libc::waitpid(child, &mut status, 0) };
if waited != child {
return Err(FdError::TraceeInstallFailed("final tracee wait failed"));
}
if libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == EXIT_CODE {
return Ok(());
}
Err(FdError::TraceeInstallFailed(
"replacement tracee did not execute restored code",
))
})();
if result.is_err() {
replacement_factory.kill_created_tracees();
}
result.unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission and Linux x86_64 clone tracing"]
fn live_replacement_factory_remote_syscall_smoke() {
const MODEL_PID: ProcessId = 7;
const MODEL_THREAD: ModelThreadId = 1;
const LINUX_X86_64_SYS_GETPID: i64 = 39;
let regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
let source_threads = vec![KboxlikeThreadSnapshot {
host_pid: 1111,
model_pid: MODEL_PID,
model_thread_id: MODEL_THREAD,
regs,
}];
let mut factory = LinuxStoppedTraceeReplacementFactory::default();
let result: Result<(), FdError> = (|| {
let created = create_stopped_replacement_tracee_set(&source_threads, &mut factory)?;
assert_eq!(
created.summary,
TraceeReplacementThreadGroupCreateSummary {
process_groups: 1,
tracees_requested: 1,
tracees_created: 1,
}
);
let host_pid = *created
.replacement_host_pids
.get(&MODEL_THREAD)
.ok_or(FdError::TraceeInstallFailed("missing replacement leader"))?;
let mut executor = PtraceSyscallExecutor::new(host_pid)?;
let ret = executor.syscall(TraceeSyscall::new3(LINUX_X86_64_SYS_GETPID, 0, 0, 0))?;
assert_eq!(ret, i64::from(host_pid));
Ok(())
})();
factory.kill_created_tracees();
result.unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission and Linux x86_64 clone tracing"]
fn live_replacement_factory_creates_two_stopped_threads_in_one_thread_group() {
const MODEL_PID: ProcessId = 7;
const MODEL_THREAD_A: ModelThreadId = 1;
const MODEL_THREAD_B: ModelThreadId = 2;
const LINUX_X86_64_SYS_GETPID: i64 = 39;
fn proc_status_tgid(tid: i32) -> Result<i32, FdError> {
let status = std::fs::read_to_string(format!("/proc/{tid}/status"))
.map_err(|_| FdError::TraceeInstallFailed("read proc status failed"))?;
for line in status.lines() {
if let Some(rest) = line.strip_prefix("Tgid:") {
return rest
.trim()
.parse::<i32>()
.map_err(|_| FdError::TraceeInstallFailed("parse proc tgid failed"));
}
}
Err(FdError::TraceeInstallFailed("proc status missing tgid"))
}
let regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
let source_threads = vec![
KboxlikeThreadSnapshot {
host_pid: 1111,
model_pid: MODEL_PID,
model_thread_id: MODEL_THREAD_A,
regs,
},
KboxlikeThreadSnapshot {
host_pid: 2222,
model_pid: MODEL_PID,
model_thread_id: MODEL_THREAD_B,
regs,
},
];
let mut factory = LinuxStoppedTraceeReplacementFactory::default();
let result: Result<(), FdError> = (|| {
let created = create_stopped_replacement_tracee_set(&source_threads, &mut factory)?;
assert_eq!(
created.summary,
TraceeReplacementThreadGroupCreateSummary {
process_groups: 1,
tracees_requested: 2,
tracees_created: 2,
}
);
let leader = *created
.replacement_host_pids
.get(&MODEL_THREAD_A)
.ok_or(FdError::TraceeInstallFailed("missing replacement leader"))?;
let follower = *created
.replacement_host_pids
.get(&MODEL_THREAD_B)
.ok_or(FdError::TraceeInstallFailed("missing replacement follower"))?;
assert_ne!(leader, follower);
assert_eq!(proc_status_tgid(leader)?, proc_status_tgid(follower)?);
PtraceSyscallExecutor::new(leader)?.getregs()?;
PtraceSyscallExecutor::new(follower)?.getregs()?;
let mut leader_executor = PtraceSyscallExecutor::new(leader)?;
let leader_ret =
leader_executor.syscall(TraceeSyscall::new3(LINUX_X86_64_SYS_GETPID, 0, 0, 0))?;
assert_eq!(leader_ret, i64::from(leader));
let mut follower_executor = PtraceSyscallExecutor::new(follower)?;
let follower_ret =
follower_executor.syscall(TraceeSyscall::new3(LINUX_X86_64_SYS_GETPID, 0, 0, 0))?;
assert_eq!(follower_ret, i64::from(leader));
Ok(())
})();
factory.kill_created_tracees();
result.unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission and Linux x86_64 clone tracing"]
fn live_replacement_restore_resumes_two_threads_in_shared_vm() {
const ADDR: u64 = 0x7800_0000_0000;
const MSG_A_OFFSET: usize = 0x40;
const MSG_B_OFFSET: usize = 0x41;
const CAPTURED_HOST_A: i32 = 1111;
const CAPTURED_HOST_B: i32 = 2222;
const MODEL_PID: ProcessId = 7;
const MODEL_THREAD_A: ModelThreadId = 1;
const MODEL_THREAD_B: ModelThreadId = 2;
const WRITE_AND_PAUSE_STUB: &[u8] = &[
0xb8, 0x01, 0x00, 0x00, 0x00, 0xba, 0x01, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xb8, 0x22, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xeb, 0xf7, ];
fn set_nonblocking(fd: i32) -> Result<(), FdError> {
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
if flags < 0 {
return Err(FdError::TraceeInstallFailed("fcntl getfl failed"));
}
let ret = unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) };
if ret < 0 {
return Err(FdError::TraceeInstallFailed("fcntl setfl failed"));
}
Ok(())
}
fn proc_status_state(tid: i32) -> String {
std::fs::read_to_string(format!("/proc/{tid}/status"))
.ok()
.and_then(|status| {
status
.lines()
.find(|line| line.starts_with("State:"))
.map(str::to_string)
})
.unwrap_or_else(|| "State: <missing>".to_string())
}
fn read_two_pipe_bytes(fd: i32, leader: i32, follower: i32) -> Result<Vec<u8>, FdError> {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
let mut bytes = Vec::new();
while bytes.len() < 2 && std::time::Instant::now() <= deadline {
let mut byte = 0u8;
let read_ret = unsafe { libc::read(fd, &mut byte as *mut _ as *mut c_void, 1) };
if read_ret == 1 {
bytes.push(byte);
continue;
}
if read_ret < 0 {
let errno = unsafe { *libc::__errno_location() };
if errno != libc::EAGAIN && errno != libc::EWOULDBLOCK {
return Err(FdError::TraceeInstallFailed("pipe read failed"));
}
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
if bytes.len() != 2 {
eprintln!(
"two-thread restore pipe timeout bytes={bytes:?} leader={} follower={} leader_state={} follower_state={}",
leader,
follower,
proc_status_state(leader),
proc_status_state(follower)
);
return Err(FdError::TraceeInstallFailed(
"restored threads did not write pipe bytes",
));
}
bytes.sort_unstable();
Ok(bytes)
}
let mut pipe_fds = [0; 2];
let pipe_ret = unsafe { libc::pipe(pipe_fds.as_mut_ptr()) };
assert_eq!(pipe_ret, 0, "pipe failed");
set_nonblocking(pipe_fds[0]).unwrap();
let regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
let source_threads = vec![
KboxlikeThreadSnapshot {
host_pid: CAPTURED_HOST_A,
model_pid: MODEL_PID,
model_thread_id: MODEL_THREAD_A,
regs,
},
KboxlikeThreadSnapshot {
host_pid: CAPTURED_HOST_B,
model_pid: MODEL_PID,
model_thread_id: MODEL_THREAD_B,
regs,
},
];
let mut replacement_factory = LinuxStoppedTraceeReplacementFactory::default();
let result: Result<(), FdError> = (|| {
let created =
create_stopped_replacement_tracee_set(&source_threads, &mut replacement_factory)?;
let leader = *created
.replacement_host_pids
.get(&MODEL_THREAD_A)
.ok_or(FdError::TraceeInstallFailed("missing replacement leader"))?;
let follower = *created
.replacement_host_pids
.get(&MODEL_THREAD_B)
.ok_or(FdError::TraceeInstallFailed("missing replacement follower"))?;
let mut page = vec![0xcc; 0x100];
page[..WRITE_AND_PAUSE_STUB.len()].copy_from_slice(WRITE_AND_PAUSE_STUB);
page[MSG_A_OFFSET] = b'A';
page[MSG_B_OFFSET] = b'B';
let action_for = |host_pid| KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid,
model_pid: MODEL_PID,
start: ADDR,
prot: libc::PROT_READ | libc::PROT_EXEC,
bytes: page.clone(),
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate { label: None },
};
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![action_for(CAPTURED_HOST_A), action_for(CAPTURED_HOST_B)],
};
let mut regs_a = PtraceSyscallExecutor::new(leader)?.getregs()?;
regs_a.rip = ADDR;
regs_a.rdi = pipe_fds[1] as u64;
regs_a.rsi = ADDR + MSG_A_OFFSET as u64;
let mut regs_b = PtraceSyscallExecutor::new(follower)?.getregs()?;
regs_b.rip = ADDR;
regs_b.rdi = pipe_fds[1] as u64;
regs_b.rsi = ADDR + MSG_B_OFFSET as u64;
let threads = vec![
KboxlikeThreadSnapshot {
host_pid: CAPTURED_HOST_A,
model_pid: MODEL_PID,
model_thread_id: MODEL_THREAD_A,
regs: regs_a,
},
KboxlikeThreadSnapshot {
host_pid: CAPTURED_HOST_B,
model_pid: MODEL_PID,
model_thread_id: MODEL_THREAD_B,
regs: regs_b,
},
];
let mut access_factory = LinuxTraceeRestoreAccessFactory;
let mut resumer = PtraceDetachRestoredTraceeResumer;
let summary = apply_and_resume_stopped_tracee_restore_set_with_replacement_pids(
&restore,
&threads,
&[],
&created.replacement_host_pids,
std::process::id() as i32,
&mut access_factory,
&mut resumer,
)?;
assert_eq!(
summary,
TraceeRestoreAndResumeSetSummary {
restore: TraceeRestoreSetSummary {
tracees_restored: 2,
shared_objects: TraceeSharedObjectRegistryBuildSummary {
created_objects: 0,
initialized_extents: 0,
skipped_unsupported_objects: 0,
},
memory: TraceeMemoryRestoreSummary {
byte_backed_applied: 1,
clean_file_remapped: 0,
shared_object_remapped: 0,
skipped_no_mapping: 0,
deferred_unsupported: 0,
other_tracee: 1,
},
registers: TraceeRegisterRestoreSummary {
registers_restored: 2,
other_tracee: 2,
},
},
resume: TraceeResumeSetSummary { tracees_resumed: 2 },
},
);
let bytes = read_two_pipe_bytes(pipe_fds[0], leader, follower)?;
if bytes != b"AB" {
return Err(FdError::TraceeInstallFailed(
"restored threads wrote unexpected pipe bytes",
));
}
Ok(())
})();
replacement_factory.kill_created_tracees();
unsafe {
libc::close(pipe_fds[0]);
libc::close(pipe_fds[1]);
}
result.unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission and Linux x86_64 clone tracing"]
fn live_captured_two_thread_registers_restore_into_replacement_group() {
const ADDR: u64 = 0x7900_0000_0000;
const MSG_A_OFFSET: usize = 0x40;
const MSG_B_OFFSET: usize = 0x41;
const SOURCE_MODEL_PID: ProcessId = 21;
const MODEL_PID: ProcessId = 7;
const MODEL_THREAD_A: ModelThreadId = 1;
const MODEL_THREAD_B: ModelThreadId = 2;
const WRITE_AND_PAUSE_STUB: &[u8] = &[
0xb8, 0x01, 0x00, 0x00, 0x00, 0xba, 0x01, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xb8, 0x22, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xeb, 0xf7, ];
fn set_nonblocking(fd: i32) -> Result<(), FdError> {
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
if flags < 0 {
return Err(FdError::TraceeInstallFailed("fcntl getfl failed"));
}
let ret = unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) };
if ret < 0 {
return Err(FdError::TraceeInstallFailed("fcntl setfl failed"));
}
Ok(())
}
fn proc_status_state(tid: i32) -> String {
std::fs::read_to_string(format!("/proc/{tid}/status"))
.ok()
.and_then(|status| {
status
.lines()
.find(|line| line.starts_with("State:"))
.map(str::to_string)
})
.unwrap_or_else(|| "State: <missing>".to_string())
}
fn read_two_pipe_bytes(fd: i32, leader: i32, follower: i32) -> Result<Vec<u8>, FdError> {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
let mut bytes = Vec::new();
while bytes.len() < 2 && std::time::Instant::now() <= deadline {
let mut byte = 0u8;
let read_ret = unsafe { libc::read(fd, &mut byte as *mut _ as *mut c_void, 1) };
if read_ret == 1 {
bytes.push(byte);
continue;
}
if read_ret < 0 {
let errno = unsafe { *libc::__errno_location() };
if errno != libc::EAGAIN && errno != libc::EWOULDBLOCK {
return Err(FdError::TraceeInstallFailed("pipe read failed"));
}
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
if bytes.len() != 2 {
eprintln!(
"captured two-thread restore pipe timeout bytes={bytes:?} leader={} follower={} leader_state={} follower_state={}",
leader,
follower,
proc_status_state(leader),
proc_status_state(follower)
);
return Err(FdError::TraceeInstallFailed(
"restored captured threads did not write pipe bytes",
));
}
bytes.sort_unstable();
Ok(bytes)
}
let mut pipe_fds = [0; 2];
let pipe_ret = unsafe { libc::pipe(pipe_fds.as_mut_ptr()) };
assert_eq!(pipe_ret, 0, "pipe failed");
set_nonblocking(pipe_fds[0]).unwrap();
let regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
let source_seed_threads = vec![
KboxlikeThreadSnapshot {
host_pid: 3111,
model_pid: SOURCE_MODEL_PID,
model_thread_id: MODEL_THREAD_A,
regs,
},
KboxlikeThreadSnapshot {
host_pid: 3222,
model_pid: SOURCE_MODEL_PID,
model_thread_id: MODEL_THREAD_B,
regs,
},
];
let mut source_factory = LinuxStoppedTraceeReplacementFactory::default();
let mut replacement_factory = LinuxStoppedTraceeReplacementFactory::default();
let result: Result<(), FdError> = (|| {
let source_created =
create_stopped_replacement_tracee_set(&source_seed_threads, &mut source_factory)?;
let source_leader = *source_created
.replacement_host_pids
.get(&MODEL_THREAD_A)
.ok_or(FdError::TraceeInstallFailed("missing source leader"))?;
let source_follower = *source_created
.replacement_host_pids
.get(&MODEL_THREAD_B)
.ok_or(FdError::TraceeInstallFailed("missing source follower"))?;
let mut regs_a = PtraceSyscallExecutor::new(source_leader)?.getregs()?;
regs_a.rip = ADDR;
regs_a.rdi = pipe_fds[1] as u64;
regs_a.rsi = ADDR + MSG_A_OFFSET as u64;
PtraceRegisterWriter::new(source_leader)?.write_regs(®s_a)?;
let captured_regs_a = PtraceSyscallExecutor::new(source_leader)?.getregs()?;
let mut regs_b = PtraceSyscallExecutor::new(source_follower)?.getregs()?;
regs_b.rip = ADDR;
regs_b.rdi = pipe_fds[1] as u64;
regs_b.rsi = ADDR + MSG_B_OFFSET as u64;
PtraceRegisterWriter::new(source_follower)?.write_regs(®s_b)?;
let captured_regs_b = PtraceSyscallExecutor::new(source_follower)?.getregs()?;
let captured_threads = vec![
KboxlikeThreadSnapshot {
host_pid: source_leader,
model_pid: MODEL_PID,
model_thread_id: MODEL_THREAD_A,
regs: captured_regs_a,
},
KboxlikeThreadSnapshot {
host_pid: source_follower,
model_pid: MODEL_PID,
model_thread_id: MODEL_THREAD_B,
regs: captured_regs_b,
},
];
let replacement_created =
create_stopped_replacement_tracee_set(&captured_threads, &mut replacement_factory)?;
let replacement_leader = *replacement_created
.replacement_host_pids
.get(&MODEL_THREAD_A)
.ok_or(FdError::TraceeInstallFailed(
"missing captured replacement leader",
))?;
let replacement_follower = *replacement_created
.replacement_host_pids
.get(&MODEL_THREAD_B)
.ok_or(FdError::TraceeInstallFailed(
"missing captured replacement follower",
))?;
let mut page = vec![0xcc; 0x100];
page[..WRITE_AND_PAUSE_STUB.len()].copy_from_slice(WRITE_AND_PAUSE_STUB);
page[MSG_A_OFFSET] = b'A';
page[MSG_B_OFFSET] = b'B';
let action_for = |host_pid| KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid,
model_pid: MODEL_PID,
start: ADDR,
prot: libc::PROT_READ | libc::PROT_EXEC,
bytes: page.clone(),
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate { label: None },
};
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![action_for(source_leader), action_for(source_follower)],
};
let mut access_factory = LinuxTraceeRestoreAccessFactory;
let mut resumer = PtraceDetachRestoredTraceeResumer;
let summary = apply_and_resume_stopped_tracee_restore_set_with_replacement_pids(
&restore,
&captured_threads,
&[],
&replacement_created.replacement_host_pids,
std::process::id() as i32,
&mut access_factory,
&mut resumer,
)?;
assert_eq!(
summary,
TraceeRestoreAndResumeSetSummary {
restore: TraceeRestoreSetSummary {
tracees_restored: 2,
shared_objects: TraceeSharedObjectRegistryBuildSummary {
created_objects: 0,
initialized_extents: 0,
skipped_unsupported_objects: 0,
},
memory: TraceeMemoryRestoreSummary {
byte_backed_applied: 1,
clean_file_remapped: 0,
shared_object_remapped: 0,
skipped_no_mapping: 0,
deferred_unsupported: 0,
other_tracee: 1,
},
registers: TraceeRegisterRestoreSummary {
registers_restored: 2,
other_tracee: 2,
},
},
resume: TraceeResumeSetSummary { tracees_resumed: 2 },
},
);
let bytes = read_two_pipe_bytes(pipe_fds[0], replacement_leader, replacement_follower)?;
if bytes != b"AB" {
return Err(FdError::TraceeInstallFailed(
"restored captured threads wrote unexpected pipe bytes",
));
}
Ok(())
})();
replacement_factory.kill_created_tracees();
source_factory.kill_created_tracees();
unsafe {
libc::close(pipe_fds[0]);
libc::close(pipe_fds[1]);
}
result.unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission and Linux x86_64 clone tracing"]
fn live_captured_two_thread_fs_base_restores_tls_addressing() {
const CODE_ADDR: u64 = 0x7a00_0000_0000;
const SCRATCH_ADDR: u64 = 0x7a00_0000_1000;
const TLS_A_ADDR: u64 = 0x7a00_0000_2000;
const TLS_B_ADDR: u64 = 0x7a00_0000_3000;
const SCRATCH_A_OFFSET: usize = 0x40;
const SCRATCH_B_OFFSET: usize = 0x41;
const SOURCE_MODEL_PID: ProcessId = 31;
const MODEL_PID: ProcessId = 7;
const MODEL_THREAD_A: ModelThreadId = 1;
const MODEL_THREAD_B: ModelThreadId = 2;
const READ_FS_WRITE_AND_PAUSE_STUB: &[u8] = &[
0x64, 0x8a, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00, 0x88, 0x06, 0xb8, 0x01, 0x00, 0x00, 0x00, 0xba, 0x01, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xb8, 0x22, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xeb, 0xf7, ];
fn set_nonblocking(fd: i32) -> Result<(), FdError> {
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
if flags < 0 {
return Err(FdError::TraceeInstallFailed("fcntl getfl failed"));
}
let ret = unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) };
if ret < 0 {
return Err(FdError::TraceeInstallFailed("fcntl setfl failed"));
}
Ok(())
}
fn proc_status_state(tid: i32) -> String {
std::fs::read_to_string(format!("/proc/{tid}/status"))
.ok()
.and_then(|status| {
status
.lines()
.find(|line| line.starts_with("State:"))
.map(str::to_string)
})
.unwrap_or_else(|| "State: <missing>".to_string())
}
fn read_two_pipe_bytes(fd: i32, leader: i32, follower: i32) -> Result<Vec<u8>, FdError> {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
let mut bytes = Vec::new();
while bytes.len() < 2 && std::time::Instant::now() <= deadline {
let mut byte = 0u8;
let read_ret = unsafe { libc::read(fd, &mut byte as *mut _ as *mut c_void, 1) };
if read_ret == 1 {
bytes.push(byte);
continue;
}
if read_ret < 0 {
let errno = unsafe { *libc::__errno_location() };
if errno != libc::EAGAIN && errno != libc::EWOULDBLOCK {
return Err(FdError::TraceeInstallFailed("pipe read failed"));
}
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
if bytes.len() != 2 {
eprintln!(
"captured FS-base restore pipe timeout bytes={bytes:?} leader={} follower={} leader_state={} follower_state={}",
leader,
follower,
proc_status_state(leader),
proc_status_state(follower)
);
return Err(FdError::TraceeInstallFailed(
"restored captured FS-base threads did not write pipe bytes",
));
}
bytes.sort_unstable();
Ok(bytes)
}
let mut pipe_fds = [0; 2];
let pipe_ret = unsafe { libc::pipe(pipe_fds.as_mut_ptr()) };
assert_eq!(pipe_ret, 0, "pipe failed");
set_nonblocking(pipe_fds[0]).unwrap();
let regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
let source_seed_threads = vec![
KboxlikeThreadSnapshot {
host_pid: 4111,
model_pid: SOURCE_MODEL_PID,
model_thread_id: MODEL_THREAD_A,
regs,
},
KboxlikeThreadSnapshot {
host_pid: 4222,
model_pid: SOURCE_MODEL_PID,
model_thread_id: MODEL_THREAD_B,
regs,
},
];
let mut source_factory = LinuxStoppedTraceeReplacementFactory::default();
let mut replacement_factory = LinuxStoppedTraceeReplacementFactory::default();
let result: Result<(), FdError> = (|| {
let source_created =
create_stopped_replacement_tracee_set(&source_seed_threads, &mut source_factory)?;
let source_leader = *source_created
.replacement_host_pids
.get(&MODEL_THREAD_A)
.ok_or(FdError::TraceeInstallFailed("missing source leader"))?;
let source_follower = *source_created
.replacement_host_pids
.get(&MODEL_THREAD_B)
.ok_or(FdError::TraceeInstallFailed("missing source follower"))?;
let mut regs_a = PtraceSyscallExecutor::new(source_leader)?.getregs()?;
regs_a.rip = CODE_ADDR;
regs_a.rdi = pipe_fds[1] as u64;
regs_a.rsi = SCRATCH_ADDR + SCRATCH_A_OFFSET as u64;
regs_a.fs_base = TLS_A_ADDR;
PtraceRegisterWriter::new(source_leader)?.write_regs(®s_a)?;
let captured_regs_a = PtraceSyscallExecutor::new(source_leader)?.getregs()?;
if captured_regs_a.fs_base != TLS_A_ADDR {
return Err(FdError::TraceeInstallFailed(
"source leader FS base did not capture",
));
}
let mut regs_b = PtraceSyscallExecutor::new(source_follower)?.getregs()?;
regs_b.rip = CODE_ADDR;
regs_b.rdi = pipe_fds[1] as u64;
regs_b.rsi = SCRATCH_ADDR + SCRATCH_B_OFFSET as u64;
regs_b.fs_base = TLS_B_ADDR;
PtraceRegisterWriter::new(source_follower)?.write_regs(®s_b)?;
let captured_regs_b = PtraceSyscallExecutor::new(source_follower)?.getregs()?;
if captured_regs_b.fs_base != TLS_B_ADDR {
return Err(FdError::TraceeInstallFailed(
"source follower FS base did not capture",
));
}
let captured_threads = vec![
KboxlikeThreadSnapshot {
host_pid: source_leader,
model_pid: MODEL_PID,
model_thread_id: MODEL_THREAD_A,
regs: captured_regs_a,
},
KboxlikeThreadSnapshot {
host_pid: source_follower,
model_pid: MODEL_PID,
model_thread_id: MODEL_THREAD_B,
regs: captured_regs_b,
},
];
let replacement_created =
create_stopped_replacement_tracee_set(&captured_threads, &mut replacement_factory)?;
let replacement_leader = *replacement_created
.replacement_host_pids
.get(&MODEL_THREAD_A)
.ok_or(FdError::TraceeInstallFailed(
"missing FS replacement leader",
))?;
let replacement_follower = *replacement_created
.replacement_host_pids
.get(&MODEL_THREAD_B)
.ok_or(FdError::TraceeInstallFailed(
"missing FS replacement follower",
))?;
let mut code_page = vec![0xcc; 0x100];
code_page[..READ_FS_WRITE_AND_PAUSE_STUB.len()]
.copy_from_slice(READ_FS_WRITE_AND_PAUSE_STUB);
let scratch_page = vec![0u8; 0x100];
let mut tls_a_page = vec![0u8; 0x100];
let mut tls_b_page = vec![0u8; 0x100];
tls_a_page[0] = b'A';
tls_b_page[0] = b'B';
let action_for =
|start, prot, bytes| KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: source_leader,
model_pid: MODEL_PID,
start,
prot,
bytes,
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate { label: None },
};
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![
action_for(CODE_ADDR, libc::PROT_READ | libc::PROT_EXEC, code_page),
action_for(
SCRATCH_ADDR,
libc::PROT_READ | libc::PROT_WRITE,
scratch_page,
),
action_for(TLS_A_ADDR, libc::PROT_READ, tls_a_page),
action_for(TLS_B_ADDR, libc::PROT_READ, tls_b_page),
],
};
let mut access_factory = LinuxTraceeRestoreAccessFactory;
let mut resumer = PtraceDetachRestoredTraceeResumer;
let summary = apply_and_resume_stopped_tracee_restore_set_with_replacement_pids(
&restore,
&captured_threads,
&[],
&replacement_created.replacement_host_pids,
std::process::id() as i32,
&mut access_factory,
&mut resumer,
)?;
assert_eq!(
summary,
TraceeRestoreAndResumeSetSummary {
restore: TraceeRestoreSetSummary {
tracees_restored: 2,
shared_objects: TraceeSharedObjectRegistryBuildSummary {
created_objects: 0,
initialized_extents: 0,
skipped_unsupported_objects: 0,
},
memory: TraceeMemoryRestoreSummary {
byte_backed_applied: 4,
clean_file_remapped: 0,
shared_object_remapped: 0,
skipped_no_mapping: 0,
deferred_unsupported: 0,
other_tracee: 4,
},
registers: TraceeRegisterRestoreSummary {
registers_restored: 2,
other_tracee: 2,
},
},
resume: TraceeResumeSetSummary { tracees_resumed: 2 },
},
);
let bytes = read_two_pipe_bytes(pipe_fds[0], replacement_leader, replacement_follower)?;
if bytes != b"AB" {
return Err(FdError::TraceeInstallFailed(
"restored FS-base threads wrote unexpected pipe bytes",
));
}
Ok(())
})();
replacement_factory.kill_created_tracees();
source_factory.kill_created_tracees();
unsafe {
libc::close(pipe_fds[0]);
libc::close(pipe_fds[1]);
}
result.unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission and Linux x86_64 clone tracing"]
fn live_restored_thread_kernel_state_clears_child_tid_on_exit() {
const CODE_ADDR: u64 = 0x7b00_0000_0000;
const STATE_ADDR: u64 = 0x7b00_0000_1000;
const EXIT_STUB_OFFSET: u64 = 0x40;
const CLEAR_TID_ADDR: u64 = STATE_ADDR + 0x20;
const ROBUST_LEADER_ADDR: u64 = STATE_ADDR + 0x40;
const ROBUST_FOLLOWER_ADDR: u64 = STATE_ADDR + 0x80;
const CAPTURED_HOST_A: i32 = 5111;
const CAPTURED_HOST_B: i32 = 5222;
const MODEL_PID: ProcessId = 7;
const MODEL_THREAD_A: ModelThreadId = 1;
const MODEL_THREAD_B: ModelThreadId = 2;
const ROBUST_LIST_HEAD_LEN: u64 = 24;
const PAUSE_STUB: &[u8] = &[
0xb8, 0x22, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xeb, 0xf7, ];
const EXIT_THREAD_STUB: &[u8] = &[
0xb8, 0x3c, 0x00, 0x00, 0x00, 0x31, 0xff, 0x0f, 0x05, ];
fn robust_list_for_tid(tid: i32) -> Result<(u64, u64), FdError> {
let mut head: *mut c_void = std::ptr::null_mut();
let mut len: libc::size_t = 0;
let ret = unsafe {
libc::syscall(
libc::SYS_get_robust_list,
tid,
&mut head as *mut _,
&mut len as *mut _,
)
};
if ret < 0 {
return Err(FdError::TraceeInstallFailed("get_robust_list failed"));
}
Ok((head as u64, len as u64))
}
fn read_i32_from_tracee(pid: i32, addr: u64) -> Result<i32, FdError> {
let mut value = 0i32;
let local = libc::iovec {
iov_base: &mut value as *mut _ as *mut c_void,
iov_len: std::mem::size_of::<i32>(),
};
let remote = libc::iovec {
iov_base: addr as *mut c_void,
iov_len: std::mem::size_of::<i32>(),
};
let read = unsafe { libc::process_vm_readv(pid, &local, 1, &remote, 1, 0) };
if read != std::mem::size_of::<i32>() as isize {
return Err(FdError::TraceeInstallFailed("process_vm_readv i32 failed"));
}
Ok(value)
}
fn wait_for_clear_child_tid_zero(pid: i32, addr: u64) -> Result<(), FdError> {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while std::time::Instant::now() <= deadline {
if read_i32_from_tracee(pid, addr)? == 0 {
return Ok(());
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
Err(FdError::TraceeInstallFailed(
"clear_child_tid was not cleared after thread exit",
))
}
let regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
let seed_threads = vec![
KboxlikeThreadSnapshot {
host_pid: CAPTURED_HOST_A,
model_pid: MODEL_PID,
model_thread_id: MODEL_THREAD_A,
regs,
},
KboxlikeThreadSnapshot {
host_pid: CAPTURED_HOST_B,
model_pid: MODEL_PID,
model_thread_id: MODEL_THREAD_B,
regs,
},
];
let mut replacement_factory = LinuxStoppedTraceeReplacementFactory::default();
let result: Result<(), FdError> = (|| {
let created =
create_stopped_replacement_tracee_set(&seed_threads, &mut replacement_factory)?;
let leader = *created.replacement_host_pids.get(&MODEL_THREAD_A).ok_or(
FdError::TraceeInstallFailed("missing kernel-state replacement leader"),
)?;
let follower = *created.replacement_host_pids.get(&MODEL_THREAD_B).ok_or(
FdError::TraceeInstallFailed("missing kernel-state replacement follower"),
)?;
let mut leader_regs = PtraceSyscallExecutor::new(leader)?.getregs()?;
leader_regs.rip = CODE_ADDR;
let mut follower_regs = PtraceSyscallExecutor::new(follower)?.getregs()?;
follower_regs.rip = CODE_ADDR + EXIT_STUB_OFFSET;
let captured_threads = vec![
KboxlikeThreadSnapshot {
host_pid: CAPTURED_HOST_A,
model_pid: MODEL_PID,
model_thread_id: MODEL_THREAD_A,
regs: leader_regs,
},
KboxlikeThreadSnapshot {
host_pid: CAPTURED_HOST_B,
model_pid: MODEL_PID,
model_thread_id: MODEL_THREAD_B,
regs: follower_regs,
},
];
let mut code_page = vec![0xcc; 0x100];
code_page[..PAUSE_STUB.len()].copy_from_slice(PAUSE_STUB);
let exit_offset = EXIT_STUB_OFFSET as usize;
code_page[exit_offset..exit_offset + EXIT_THREAD_STUB.len()]
.copy_from_slice(EXIT_THREAD_STUB);
let mut state_page = vec![0u8; 0x100];
state_page[0x20..0x24].copy_from_slice(&0x5151_5151i32.to_ne_bytes());
let action_for =
|start, prot, bytes| KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: CAPTURED_HOST_A,
model_pid: MODEL_PID,
start,
prot,
bytes,
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate { label: None },
};
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![
action_for(CODE_ADDR, libc::PROT_READ | libc::PROT_EXEC, code_page),
action_for(STATE_ADDR, libc::PROT_READ | libc::PROT_WRITE, state_page),
],
};
let kernel_states = vec![
KboxlikeThreadKernelStateSnapshot {
host_pid: CAPTURED_HOST_A,
model_pid: MODEL_PID,
model_thread_id: MODEL_THREAD_A,
clear_child_tid: None,
robust_list_head: Some(ROBUST_LEADER_ADDR),
robust_list_len: ROBUST_LIST_HEAD_LEN,
rseq: None,
},
KboxlikeThreadKernelStateSnapshot {
host_pid: CAPTURED_HOST_B,
model_pid: MODEL_PID,
model_thread_id: MODEL_THREAD_B,
clear_child_tid: Some(CLEAR_TID_ADDR),
robust_list_head: Some(ROBUST_FOLLOWER_ADDR),
robust_list_len: ROBUST_LIST_HEAD_LEN,
rseq: None,
},
];
let mut access_factory = LinuxTraceeRestoreAccessFactory;
let full_restore_summary = apply_stopped_tracee_full_restore_set_with_replacement_pids(
&restore,
&captured_threads,
&kernel_states,
&[],
&created.replacement_host_pids,
std::process::id() as i32,
&mut access_factory,
)?;
assert_eq!(
full_restore_summary,
TraceeFullRestoreSetSummary {
tracees_restored: 2,
shared_objects: TraceeSharedObjectRegistryBuildSummary::default(),
memory: TraceeMemoryRestoreSummary {
byte_backed_applied: 2,
clean_file_remapped: 0,
shared_object_remapped: 0,
skipped_no_mapping: 0,
deferred_unsupported: 0,
other_tracee: 2,
},
kernel_state: TraceeKernelStateRestoreSummary {
clear_child_tid_applied: 1,
robust_list_applied: 2,
rseq_applied: 0,
other_tracee: 2,
},
registers: TraceeRegisterRestoreSummary {
registers_restored: 2,
other_tracee: 2,
},
}
);
assert_eq!(
full_restore_summary.kernel_state,
TraceeKernelStateRestoreSummary {
clear_child_tid_applied: 1,
robust_list_applied: 2,
rseq_applied: 0,
other_tracee: 2,
}
);
assert_eq!(
robust_list_for_tid(leader)?,
(ROBUST_LEADER_ADDR, ROBUST_LIST_HEAD_LEN)
);
assert_eq!(
robust_list_for_tid(follower)?,
(ROBUST_FOLLOWER_ADDR, ROBUST_LIST_HEAD_LEN)
);
if read_i32_from_tracee(leader, CLEAR_TID_ADDR)? == 0 {
return Err(FdError::TraceeInstallFailed(
"clear_child_tid was zero before follower exit",
));
}
let mut resumer = PtraceDetachRestoredTraceeResumer;
resumer.resume_restored_tracee(leader)?;
resumer.resume_restored_tracee(follower)?;
wait_for_clear_child_tid_zero(leader, CLEAR_TID_ADDR)?;
Ok(())
})();
replacement_factory.kill_created_tracees();
result.unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission and a Linux x86_64 tracee"]
fn live_materialized_memory_restore_remaps_clean_file_into_stopped_child() {
const ADDR: u64 = 0x7100_0000_0000;
const BYTES: &[u8] = b"kboxlike-clean-file-remap";
let path =
std::env::temp_dir().join(format!("kboxlike-clean-remap-{}", std::process::id()));
std::fs::write(&path, BYTES).unwrap();
let child = unsafe { libc::fork() };
assert!(child >= 0, "fork failed");
if child == 0 {
unsafe {
let ret = libc::ptrace(
libc::PTRACE_TRACEME,
0,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_void>(),
);
if ret < 0 {
libc::_exit(111);
}
libc::raise(libc::SIGSTOP);
let restored = std::slice::from_raw_parts(ADDR as *const u8, BYTES.len());
if restored == BYTES {
libc::_exit(0);
}
libc::_exit(113);
}
}
let result = (|| {
let mut status = 0;
let waited = unsafe { libc::waitpid(child, &mut status, 0) };
if waited != child {
return Err(FdError::TraceeInstallFailed("initial tracee wait failed"));
}
if !libc::WIFSTOPPED(status) || libc::WSTOPSIG(status) != libc::SIGSTOP {
return Err(FdError::TraceeInstallFailed(
"tracee did not stop at initial SIGSTOP",
));
}
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![KboxlikeMaterializedMemoryRestoreAction::RemapCleanFile {
host_pid: child,
model_pid: 1,
start: ADDR,
len: BYTES.len() as u64,
prot: libc::PROT_READ,
path: path.clone(),
file_offset: 0,
dev: "00:00".to_string(),
inode: 1,
}],
};
let mut writer = ProcessVmMemoryWriter::new(child)?;
let mut executor = PtraceSyscallExecutor::new(child)?;
let summary = apply_materialized_memory_restore_for_tracee(
&restore,
child,
&mut writer,
&mut executor,
)?;
assert_eq!(
summary,
TraceeMemoryRestoreSummary {
byte_backed_applied: 0,
clean_file_remapped: 1,
shared_object_remapped: 0,
skipped_no_mapping: 0,
deferred_unsupported: 0,
other_tracee: 0,
}
);
let detach_ret = unsafe {
libc::ptrace(
libc::PTRACE_DETACH,
child,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_void>(),
)
};
if detach_ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace detach failed"));
}
let waited = unsafe { libc::waitpid(child, &mut status, 0) };
if waited != child {
return Err(FdError::TraceeInstallFailed("final tracee wait failed"));
}
if libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0 {
return Ok(());
}
Err(FdError::TraceeInstallFailed(
"tracee did not observe remapped file bytes",
))
})();
if result.is_err() {
unsafe {
libc::kill(child, libc::SIGKILL);
let mut status = 0;
libc::waitpid(child, &mut status, 0);
}
}
let _ = std::fs::remove_file(path);
result.unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission and two Linux x86_64 tracees"]
fn live_materialized_memory_restore_preserves_shared_mapping_visibility() {
const ADDR: u64 = 0x7200_0000_0000;
const BYTES: &[u8] = b"kboxlike-shared-remap";
fn fork_shared_reader(addr: u64, bytes: &'static [u8]) -> libc::pid_t {
let child = unsafe { libc::fork() };
assert!(child >= 0, "fork failed");
if child == 0 {
unsafe {
let ret = libc::ptrace(
libc::PTRACE_TRACEME,
0,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_void>(),
);
if ret < 0 {
libc::_exit(111);
}
libc::raise(libc::SIGSTOP);
let restored = std::slice::from_raw_parts(addr as *const u8, bytes.len());
if restored == bytes {
libc::_exit(0);
}
libc::_exit(113);
}
}
child
}
fn wait_initial_stop(child: libc::pid_t) -> Result<(), FdError> {
let mut status = 0;
let waited = unsafe { libc::waitpid(child, &mut status, 0) };
if waited != child {
return Err(FdError::TraceeInstallFailed("initial tracee wait failed"));
}
if !libc::WIFSTOPPED(status) || libc::WSTOPSIG(status) != libc::SIGSTOP {
return Err(FdError::TraceeInstallFailed(
"tracee did not stop at initial SIGSTOP",
));
}
Ok(())
}
fn detach_and_wait_clean(child: libc::pid_t) -> Result<(), FdError> {
let detach_ret = unsafe {
libc::ptrace(
libc::PTRACE_DETACH,
child,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_void>(),
)
};
if detach_ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace detach failed"));
}
let mut status = 0;
let waited = unsafe { libc::waitpid(child, &mut status, 0) };
if waited != child {
return Err(FdError::TraceeInstallFailed("final tracee wait failed"));
}
if libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0 {
return Ok(());
}
Err(FdError::TraceeInstallFailed(
"tracee did not observe shared bytes",
))
}
let name = std::ffi::CString::new("kboxlike-shared").unwrap();
let memfd = unsafe { libc::memfd_create(name.as_ptr(), libc::MFD_CLOEXEC) };
assert!(memfd >= 0, "memfd_create failed");
let truncate_ret = unsafe { libc::ftruncate(memfd, 4096) };
assert_eq!(truncate_ret, 0, "ftruncate failed");
let child_a = fork_shared_reader(ADDR, BYTES);
let child_b = fork_shared_reader(ADDR, BYTES);
let result: Result<(), FdError> = (|| {
wait_initial_stop(child_a)?;
wait_initial_stop(child_b)?;
let shared_action_for =
|host_pid| KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject {
host_pid,
model_pid: host_pid as u64,
start: ADDR,
len: 4096,
prot: libc::PROT_READ | libc::PROT_WRITE,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:kboxlike-shared".to_string()),
file_offset: 0,
dev: "00:00".to_string(),
inode: 77,
};
let key = TraceeSharedObjectKey::from_action(&shared_action_for(child_a)).unwrap();
let mut shared = TraceeSharedObjectRegistry::new();
shared.register(key, memfd)?;
for child in [child_a, child_b] {
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![shared_action_for(child)],
};
let mut writer = ProcessVmMemoryWriter::new(child)?;
let mut executor = PtraceSyscallExecutor::new(child)?;
let summary = apply_materialized_memory_restore_for_tracee_with_shared_objects(
&restore,
child,
std::process::id() as i32,
&shared,
&mut writer,
&mut executor,
)?;
assert_eq!(
summary,
TraceeMemoryRestoreSummary {
byte_backed_applied: 0,
clean_file_remapped: 0,
shared_object_remapped: 1,
skipped_no_mapping: 0,
deferred_unsupported: 0,
other_tracee: 0,
}
);
}
let mut writer = ProcessVmMemoryWriter::new(child_a)?;
writer.write_bytes(ADDR, BYTES)?;
detach_and_wait_clean(child_a)?;
detach_and_wait_clean(child_b)?;
Ok(())
})();
if result.is_err() {
unsafe {
libc::kill(child_a, libc::SIGKILL);
libc::kill(child_b, libc::SIGKILL);
let mut status = 0;
libc::waitpid(child_a, &mut status, 0);
libc::waitpid(child_b, &mut status, 0);
}
}
unsafe {
libc::close(memfd);
}
result.unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission and two Linux x86_64 tracees"]
fn live_owned_shared_object_registry_initializes_and_remaps_shared_memory() {
const ADDR: u64 = 0x7300_0000_0000;
const BYTES: &[u8] = b"kboxlike-owned-shared";
fn fork_shared_reader(addr: u64, bytes: &'static [u8]) -> libc::pid_t {
let child = unsafe { libc::fork() };
assert!(child >= 0, "fork failed");
if child == 0 {
unsafe {
let ret = libc::ptrace(
libc::PTRACE_TRACEME,
0,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_void>(),
);
if ret < 0 {
libc::_exit(111);
}
libc::raise(libc::SIGSTOP);
let restored = std::slice::from_raw_parts(addr as *const u8, bytes.len());
if restored == bytes {
libc::_exit(0);
}
libc::_exit(113);
}
}
child
}
fn wait_initial_stop(child: libc::pid_t) -> Result<(), FdError> {
let mut status = 0;
let waited = unsafe { libc::waitpid(child, &mut status, 0) };
if waited != child {
return Err(FdError::TraceeInstallFailed("initial tracee wait failed"));
}
if !libc::WIFSTOPPED(status) || libc::WSTOPSIG(status) != libc::SIGSTOP {
return Err(FdError::TraceeInstallFailed(
"tracee did not stop at initial SIGSTOP",
));
}
Ok(())
}
fn detach_and_wait_clean(child: libc::pid_t) -> Result<(), FdError> {
let detach_ret = unsafe {
libc::ptrace(
libc::PTRACE_DETACH,
child,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_void>(),
)
};
if detach_ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace detach failed"));
}
let mut status = 0;
let waited = unsafe { libc::waitpid(child, &mut status, 0) };
if waited != child {
return Err(FdError::TraceeInstallFailed("final tracee wait failed"));
}
if libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0 {
return Ok(());
}
Err(FdError::TraceeInstallFailed(
"tracee did not observe initialized shared bytes",
))
}
let child_a = fork_shared_reader(ADDR, BYTES);
let child_b = fork_shared_reader(ADDR, BYTES);
let result: Result<(), FdError> = (|| {
wait_initial_stop(child_a)?;
wait_initial_stop(child_b)?;
let shared_action_for =
|host_pid| KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject {
host_pid,
model_pid: host_pid as u64,
start: ADDR,
len: BYTES.len() as u64,
prot: libc::PROT_READ,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:kboxlike-owned-shared".to_string()),
file_offset: 0,
dev: "00:00".to_string(),
inode: 88,
};
let restore = KboxlikeMaterializedMemoryRestore {
actions: vec![shared_action_for(child_a), shared_action_for(child_b)],
};
let dumps = vec![KboxlikeSharedMemoryDump {
host_pid: child_a,
model_pid: child_a as u64,
start: ADDR,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:kboxlike-owned-shared".to_string()),
file_offset: 0,
dev: "00:00".to_string(),
inode: 88,
bytes: BYTES.to_vec(),
}];
let owned = build_owned_shared_object_registry(&restore, &dumps)?;
assert_eq!(
owned.summary(),
TraceeSharedObjectRegistryBuildSummary {
created_objects: 1,
initialized_extents: 1,
skipped_unsupported_objects: 0,
}
);
for child in [child_a, child_b] {
let mut writer = ProcessVmMemoryWriter::new(child)?;
let mut executor = PtraceSyscallExecutor::new(child)?;
let summary = apply_materialized_memory_restore_for_tracee_with_shared_objects(
&restore,
child,
std::process::id() as i32,
owned.registry(),
&mut writer,
&mut executor,
)?;
assert_eq!(
summary,
TraceeMemoryRestoreSummary {
byte_backed_applied: 0,
clean_file_remapped: 0,
shared_object_remapped: 1,
skipped_no_mapping: 0,
deferred_unsupported: 0,
other_tracee: 1,
}
);
}
detach_and_wait_clean(child_a)?;
detach_and_wait_clean(child_b)?;
Ok(())
})();
if result.is_err() {
unsafe {
libc::kill(child_a, libc::SIGKILL);
libc::kill(child_b, libc::SIGKILL);
let mut status = 0;
libc::waitpid(child_a, &mut status, 0);
libc::waitpid(child_b, &mut status, 0);
}
}
result.unwrap();
}
#[test]
fn tracee_plan_rejects_bad_fds_and_source_target_alias() {
assert_eq!(
TraceeInstallPlan::new(-1, 6, false, false).unwrap_err(),
FdError::InvalidFd(-1)
);
assert_eq!(
TraceeInstallPlan::new(7, -2, false, false).unwrap_err(),
FdError::InvalidFd(-2)
);
assert_eq!(
TraceeInstallPlan::new(7, 7, false, false).unwrap_err(),
FdError::TraceeInstallFailed("tracee source fd aliases target fd")
);
}
#[test]
fn proc_fd_source_plan_encodes_supervisor_fd_path_and_openat() {
let source = TraceeProcFdSourcePlan::new(1234, 77, 0x7000).unwrap();
assert_eq!(source.proc_fd_path(), "/proc/1234/fd/77");
assert_eq!(source.path_bytes(), b"/proc/1234/fd/77\0");
assert_eq!(
source.open_syscall(),
TraceeSyscall::new3(
LINUX_X86_64_SYS_OPENAT,
LINUX_AT_FDCWD,
0x7000,
LINUX_O_CLOEXEC,
)
);
}
#[test]
fn proc_fd_source_plan_rejects_invalid_inputs() {
assert_eq!(
TraceeProcFdSourcePlan::new(0, 7, 0x7000).unwrap_err(),
FdError::TraceeInstallFailed("invalid supervisor pid")
);
assert_eq!(
TraceeProcFdSourcePlan::new(12, -1, 0x7000).unwrap_err(),
FdError::InvalidFd(-1)
);
assert_eq!(
TraceeProcFdSourcePlan::new(12, 7, 0).unwrap_err(),
FdError::TraceeInstallFailed("tracee source path address is null")
);
}
#[test]
fn process_vm_memory_writer_rejects_invalid_pid() {
assert_eq!(
ProcessVmMemoryWriter::new(0).unwrap_err(),
FdError::TraceeInstallFailed("invalid tracee pid")
);
assert_eq!(
ProcessVmMemoryWriter::new(-1).unwrap_err(),
FdError::TraceeInstallFailed("invalid tracee pid")
);
assert_eq!(ProcessVmMemoryWriter::new(1234).unwrap().pid(), 1234);
}
#[test]
fn tracee_operation_install_request_rejects_invalid_tracee_pid() {
assert_eq!(
TraceeOperationInstallRequest::new(1, 0, 104, FdOperation::Mmap).unwrap_err(),
FdError::TraceeInstallFailed("invalid tracee pid")
);
assert_eq!(
TraceeOperationInstallRequest::new(1, -1, 104, FdOperation::Mmap).unwrap_err(),
FdError::TraceeInstallFailed("invalid tracee pid")
);
assert_eq!(
TraceeOperationInstallRequest::new(1, 4321, 104, FdOperation::Mmap).unwrap(),
TraceeOperationInstallRequest {
model_pid: 1,
tracee_pid: 4321,
fd: 104,
operation: FdOperation::Mmap,
}
);
}
#[cfg(target_os = "linux")]
#[test]
fn process_vm_memory_writer_writes_current_process_memory() {
let mut target = [0u8; 5];
let addr = target.as_mut_ptr() as u64;
let mut writer = ProcessVmMemoryWriter::new(std::process::id() as i32).unwrap();
writer.write_bytes(addr, b"trace").unwrap();
assert_eq!(&target, b"trace");
}
#[cfg(target_os = "linux")]
#[test]
fn process_vm_memory_writer_rejects_null_address() {
let mut writer = ProcessVmMemoryWriter::new(std::process::id() as i32).unwrap();
assert_eq!(
writer.write_bytes(0, b"trace").unwrap_err(),
FdError::TraceeInstallFailed("tracee memory address is null")
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn ptrace_syscall_executor_rejects_invalid_pid() {
assert_eq!(
PtraceSyscallExecutor::new(0).unwrap_err(),
FdError::TraceeInstallFailed("invalid tracee pid")
);
assert_eq!(
PtraceSyscallExecutor::new(-1).unwrap_err(),
FdError::TraceeInstallFailed("invalid tracee pid")
);
assert_eq!(PtraceSyscallExecutor::new(1234).unwrap().pid(), 1234);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission and an unconfined parent/child test process"]
fn ptrace_syscall_executor_runs_getpid_in_stopped_child() {
unsafe {
let child = libc::fork();
assert!(child >= 0);
if child == 0 {
libc::ptrace(
libc::PTRACE_TRACEME,
0,
std::ptr::null_mut::<c_void>(),
std::ptr::null_mut::<c_void>(),
);
libc::raise(libc::SIGSTOP);
libc::_exit(0);
}
let mut status = 0;
assert_eq!(libc::waitpid(child, &mut status, 0), child);
assert!(libc::WIFSTOPPED(status));
let mut executor = PtraceSyscallExecutor::new(child).unwrap();
let ret = executor.syscall(TraceeSyscall::new3(39, 0, 0, 0)).unwrap();
assert_eq!(ret, child as i64);
libc::kill(child, libc::SIGKILL);
libc::waitpid(child, &mut status, 0);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission and an unconfined parent/child test process"]
fn ptrace_scratch_executor_survives_inherited_mapping_sweep() {
const MODEL_PID: ProcessId = 7;
const MODEL_THREAD: ModelThreadId = 1;
const SYSCALL_PAGE: u64 = 0x7000_0000_0000;
let regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
let source_threads = [KboxlikeThreadSnapshot {
host_pid: 1111,
model_pid: MODEL_PID,
model_thread_id: MODEL_THREAD,
regs,
}];
let mut factory = LinuxStoppedTraceeReplacementFactory::default();
let result: Result<(), FdError> = (|| {
factory.prepare_syscall_page(SYSCALL_PAGE)?;
let created = create_stopped_replacement_tracee_set(&source_threads, &mut factory)?;
let replacement_pid = *created.replacement_host_pids.get(&MODEL_THREAD).ok_or(
FdError::TraceeInstallFailed("missing replacement tracee pid"),
)?;
let mut executor = PtraceScratchSyscallExecutor::new(replacement_pid, SYSCALL_PAGE)?;
let unmapped = executor.unmap_existing_mappings_except_scratch()?;
assert!(unmapped > 0, "expected inherited mappings to be swept");
let ret = executor.syscall(TraceeSyscall::new3(libc::SYS_getpid, 0, 0, 0))?;
assert_eq!(ret, replacement_pid as i64);
Ok(())
})();
factory.kill_created_tracees();
result.unwrap();
}
#[test]
fn apply_runs_tracee_syscalls_in_order() {
let plan = TraceeInstallPlan::new(901, 104, true, true).unwrap();
let mut tracee = FakeTracee::with_returns([104, 0, 0]);
plan.apply(&mut tracee).unwrap();
assert_eq!(tracee.calls, plan.syscalls());
}
#[test]
fn apply_rejects_wrong_dup2_return() {
let plan = TraceeInstallPlan::new(901, 104, true, false).unwrap();
let mut tracee = FakeTracee::with_returns([105]);
assert_eq!(
plan.apply(&mut tracee).unwrap_err(),
FdError::TraceeInstallFailed("tracee dup2 returned wrong fd")
);
}
#[test]
fn apply_rejects_negative_syscall_returns() {
let plan = TraceeInstallPlan::new(901, 104, true, true).unwrap();
let mut dup_fail = FakeTracee::with_returns([-1]);
assert_eq!(
plan.apply(&mut dup_fail).unwrap_err(),
FdError::TraceeInstallFailed("tracee dup2 failed")
);
let mut fcntl_fail = FakeTracee::with_returns([104, -1]);
assert_eq!(
plan.apply(&mut fcntl_fail).unwrap_err(),
FdError::TraceeInstallFailed("tracee fcntl failed")
);
let mut close_fail = FakeTracee::with_returns([104, 0, -1]);
assert_eq!(
plan.apply(&mut close_fail).unwrap_err(),
FdError::TraceeInstallFailed("tracee close failed")
);
}
#[test]
fn proc_fd_shadow_install_opens_source_then_installs_and_closes_it() {
let source = TraceeProcFdSourcePlan::new(1234, 77, 0x7000).unwrap();
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::with_returns([901, 104, 0, 0]);
apply_proc_fd_shadow_install(&source, shadow_install(104, true), &mut memory, &mut tracee)
.unwrap();
assert_eq!(
memory.writes,
vec![(0x7000, b"/proc/1234/fd/77\0".to_vec())]
);
assert_eq!(
tracee.calls,
vec![
TraceeSyscall::new3(
LINUX_X86_64_SYS_OPENAT,
LINUX_AT_FDCWD,
0x7000,
LINUX_O_CLOEXEC,
),
TraceeSyscall::new3(LINUX_X86_64_SYS_DUP2, 901, 104, 0),
TraceeSyscall::new3(LINUX_X86_64_SYS_FCNTL, 104, LINUX_F_SETFD, LINUX_FD_CLOEXEC),
TraceeSyscall::new3(LINUX_X86_64_SYS_CLOSE, 901, 0, 0),
]
);
}
#[test]
fn proc_fd_shadow_install_fails_if_source_open_fails() {
let source = TraceeProcFdSourcePlan::new(1234, 77, 0x7000).unwrap();
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::with_returns([-1]);
assert_eq!(
apply_proc_fd_shadow_install(
&source,
shadow_install(104, true),
&mut memory,
&mut tracee,
)
.unwrap_err(),
FdError::TraceeInstallFailed("tracee open supervisor fd failed")
);
assert_eq!(
memory.writes,
vec![(0x7000, b"/proc/1234/fd/77\0".to_vec())]
);
assert_eq!(tracee.calls, vec![source.open_syscall()]);
}
#[test]
fn proc_fd_shadow_install_fails_before_syscalls_if_path_write_fails() {
let source = TraceeProcFdSourcePlan::new(1234, 77, 0x7000).unwrap();
let mut memory = FakeMemory {
writes: Vec::new(),
fail: true,
};
let mut tracee = FakeTracee::with_returns([901, 104, 0, 0]);
assert_eq!(
apply_proc_fd_shadow_install(
&source,
shadow_install(104, true),
&mut memory,
&mut tracee,
)
.unwrap_err(),
FdError::TraceeInstallFailed("tracee source path write failed")
);
assert!(memory.writes.is_empty());
assert!(tracee.calls.is_empty());
}
#[test]
fn proc_fd_shadow_install_with_scratch_maps_writes_installs_and_unmaps() {
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::with_returns([0x7000, 901, 104, 0, 0, 0]);
apply_proc_fd_shadow_install_with_scratch(
1234,
77,
shadow_install(104, true),
&mut memory,
&mut tracee,
)
.unwrap();
assert_eq!(
memory.writes,
vec![(0x7000, b"/proc/1234/fd/77\0".to_vec())]
);
assert_eq!(
tracee.calls,
vec![
TraceeScratchMapping::mmap_syscall(17),
TraceeSyscall::new3(
LINUX_X86_64_SYS_OPENAT,
LINUX_AT_FDCWD,
0x7000,
LINUX_O_CLOEXEC,
),
TraceeSyscall::new3(LINUX_X86_64_SYS_DUP2, 901, 104, 0),
TraceeSyscall::new3(LINUX_X86_64_SYS_FCNTL, 104, LINUX_F_SETFD, LINUX_FD_CLOEXEC),
TraceeSyscall::new3(LINUX_X86_64_SYS_CLOSE, 901, 0, 0),
TraceeSyscall::new6(LINUX_X86_64_SYS_MUNMAP, [0x7000, 17, 0, 0, 0, 0]),
]
);
}
#[test]
fn proc_fd_shadow_install_with_scratch_unmaps_after_install_failure() {
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::with_returns([0x7000, -1, 0]);
assert_eq!(
apply_proc_fd_shadow_install_with_scratch(
1234,
77,
shadow_install(104, true),
&mut memory,
&mut tracee,
)
.unwrap_err(),
FdError::TraceeInstallFailed("tracee open supervisor fd failed")
);
assert_eq!(
memory.writes,
vec![(0x7000, b"/proc/1234/fd/77\0".to_vec())]
);
assert_eq!(
tracee.calls,
vec![
TraceeScratchMapping::mmap_syscall(17),
TraceeSyscall::new3(
LINUX_X86_64_SYS_OPENAT,
LINUX_AT_FDCWD,
0x7000,
LINUX_O_CLOEXEC,
),
TraceeSyscall::new6(LINUX_X86_64_SYS_MUNMAP, [0x7000, 17, 0, 0, 0, 0]),
]
);
}
#[test]
fn proc_fd_shadow_install_with_scratch_returns_unmap_failure_after_successful_install() {
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::with_returns([0x7000, 901, 104, 0, 0, -1]);
assert_eq!(
apply_proc_fd_shadow_install_with_scratch(
1234,
77,
shadow_install(104, true),
&mut memory,
&mut tracee,
)
.unwrap_err(),
FdError::TraceeInstallFailed("tracee munmap failed")
);
}
#[test]
fn proc_fd_shadow_install_with_scratch_aborts_before_write_if_mmap_fails() {
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::with_returns([-12]);
assert_eq!(
apply_proc_fd_shadow_install_with_scratch(
1234,
77,
shadow_install(104, true),
&mut memory,
&mut tracee,
)
.unwrap_err(),
FdError::TraceeInstallFailed("tracee mmap failed")
);
assert!(memory.writes.is_empty());
assert_eq!(tracee.calls, vec![TraceeScratchMapping::mmap_syscall(17)]);
}
#[test]
fn shadow_install_plan_with_scratch_uses_plan_shadow_supervisor_fd() {
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::with_returns([0x8000, 901, 104, 0, 0, 0]);
apply_shadow_install_plan_with_scratch(
1234,
shadow_install(104, true),
&mut memory,
&mut tracee,
)
.unwrap();
assert_eq!(
memory.writes,
vec![(0x8000, b"/proc/1234/fd/700\0".to_vec())]
);
assert_eq!(
tracee.calls,
vec![
TraceeScratchMapping::mmap_syscall(18),
TraceeSyscall::new3(
LINUX_X86_64_SYS_OPENAT,
LINUX_AT_FDCWD,
0x8000,
LINUX_O_CLOEXEC,
),
TraceeSyscall::new3(LINUX_X86_64_SYS_DUP2, 901, 104, 0),
TraceeSyscall::new3(LINUX_X86_64_SYS_FCNTL, 104, LINUX_F_SETFD, LINUX_FD_CLOEXEC),
TraceeSyscall::new3(LINUX_X86_64_SYS_CLOSE, 901, 0, 0),
TraceeSyscall::new6(LINUX_X86_64_SYS_MUNMAP, [0x8000, 18, 0, 0, 0, 0]),
]
);
}
#[test]
fn operation_shadow_installer_skips_plain_lkl_fd() {
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init();
fds.insert_plain_fd(pid, 4, Some(44), false).unwrap();
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::with_returns([0x8000]);
let installed = install_operation_shadow_if_needed(
&fds,
pid,
4,
FdOperation::Mmap,
1234,
&mut memory,
&mut tracee,
)
.unwrap();
assert!(!installed);
assert!(memory.writes.is_empty());
assert!(tracee.calls.is_empty());
}
#[test]
fn operation_shadow_installer_installs_shadow_for_mmap() {
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init();
fds.insert_shadow_fd(
pid,
104,
Some(204),
ShadowObject {
kind: ShadowKind::LocalOnly,
supervisor_fd: 700,
},
true,
)
.unwrap();
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::with_returns([0x8000, 901, 104, 0, 0, 0]);
let installed = install_operation_shadow_if_needed(
&fds,
pid,
104,
FdOperation::Mmap,
1234,
&mut memory,
&mut tracee,
)
.unwrap();
assert!(installed);
assert_eq!(
memory.writes,
vec![(0x8000, b"/proc/1234/fd/700\0".to_vec())]
);
assert_eq!(
tracee.calls,
vec![
TraceeScratchMapping::mmap_syscall(18),
TraceeSyscall::new3(
LINUX_X86_64_SYS_OPENAT,
LINUX_AT_FDCWD,
0x8000,
LINUX_O_CLOEXEC,
),
TraceeSyscall::new3(LINUX_X86_64_SYS_DUP2, 901, 104, 0),
TraceeSyscall::new3(LINUX_X86_64_SYS_FCNTL, 104, LINUX_F_SETFD, 0),
TraceeSyscall::new3(LINUX_X86_64_SYS_CLOSE, 901, 0, 0),
TraceeSyscall::new6(LINUX_X86_64_SYS_MUNMAP, [0x8000, 18, 0, 0, 0, 0]),
]
);
}
#[test]
fn requested_operation_shadow_installer_uses_request_identity_and_operation() {
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init();
fds.insert_shadow_fd(
pid,
104,
Some(204),
ShadowObject {
kind: ShadowKind::LocalOnly,
supervisor_fd: 700,
},
false,
)
.unwrap();
let request =
TraceeOperationInstallRequest::new(pid, 4321, 104, FdOperation::Mmap).unwrap();
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::with_returns([0x8000, 901, 104, 0, 0, 0]);
let installed = install_requested_operation_shadow_if_needed(
&fds,
request,
1234,
&mut memory,
&mut tracee,
)
.unwrap();
assert!(installed);
assert_eq!(
memory.writes,
vec![(0x8000, b"/proc/1234/fd/700\0".to_vec())]
);
assert_eq!(
tracee.calls,
vec![
TraceeScratchMapping::mmap_syscall(18),
TraceeSyscall::new3(
LINUX_X86_64_SYS_OPENAT,
LINUX_AT_FDCWD,
0x8000,
LINUX_O_CLOEXEC,
),
TraceeSyscall::new3(LINUX_X86_64_SYS_DUP2, 901, 104, 0),
TraceeSyscall::new3(LINUX_X86_64_SYS_FCNTL, 104, LINUX_F_SETFD, 0),
TraceeSyscall::new3(LINUX_X86_64_SYS_CLOSE, 901, 0, 0),
TraceeSyscall::new6(LINUX_X86_64_SYS_MUNMAP, [0x8000, 18, 0, 0, 0, 0]),
]
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn stopped_tracee_installer_rejects_invalid_tracee_before_planning() {
let fds = KboxlikeFdSystem::new();
let request = TraceeOperationInstallRequest {
model_pid: 1,
tracee_pid: -1,
fd: 104,
operation: FdOperation::Mmap,
};
assert_eq!(
install_requested_operation_shadow_in_stopped_tracee(&fds, request, 1234).unwrap_err(),
FdError::TraceeInstallFailed("invalid tracee pid")
);
}
}