use super::dispatch::{
handle_intercepted_syscall_stop, InterceptedSyscall, KboxlikeTraceeIdentity,
KboxlikeTraceeRegistry, SyscallStopOutcome,
};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use super::fd::ModelThreadId;
use super::fd::{FdError, KboxlikeFdSystem, ProcessId};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use super::snapshot::{
capture_stopped_tracees, read_thread_rseq_configuration, KboxlikeStoppedTraceeSnapshot,
KboxlikeThreadKernelStateSnapshot, KboxlikeThreadRseqSnapshot,
};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use super::tracee::{ProcessVmMemoryWriter, PtraceSyscallExecutor};
use super::tracee::{TraceeMemoryWriter, TraceeSyscallExecutor};
use std::collections::{BTreeMap, BTreeSet};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use std::ffi::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::PathBuf;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SyscallStopPhase {
Entry,
Exit,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SupervisedSyscallStopOutcome {
Entry(SyscallStopOutcome),
Exit,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum TraceeProcessEvent {
Fork {
parent_host_pid: i32,
child_host_pid: i32,
shares_fd_table: bool,
},
Exec {
host_pid: i32,
guest_exe: String,
},
Exit {
host_pid: i32,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SupervisedProcessEventOutcome {
Forked {
parent_model_pid: ProcessId,
child_model_pid: ProcessId,
},
Execed {
model_pid: ProcessId,
},
Exited {
model_pid: ProcessId,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum LinuxPtraceStop {
Syscall {
host_pid: i32,
},
Fork {
parent_host_pid: i32,
child_host_pid: i32,
shares_fd_table: bool,
},
Exec {
host_pid: i32,
},
Exit {
host_pid: i32,
},
Signal {
host_pid: i32,
signal: i32,
},
Exited {
host_pid: i32,
code: i32,
},
Signaled {
host_pid: i32,
signal: i32,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum LinuxPtraceDispatchOutcome {
Syscall(SupervisedSyscallStopOutcome),
Process(SupervisedProcessEventOutcome),
Signal { host_pid: i32, signal: i32 },
ProcessExited { host_pid: i32, code: i32 },
ProcessSignaled { host_pid: i32, signal: i32 },
}
impl LinuxPtraceStop {
fn host_pid(&self) -> i32 {
match *self {
LinuxPtraceStop::Syscall { host_pid }
| LinuxPtraceStop::Exec { host_pid }
| LinuxPtraceStop::Exit { host_pid }
| LinuxPtraceStop::Signal { host_pid, .. }
| LinuxPtraceStop::Exited { host_pid, .. }
| LinuxPtraceStop::Signaled { host_pid, .. } => host_pid,
LinuxPtraceStop::Fork {
parent_host_pid, ..
} => parent_host_pid,
}
}
}
pub(crate) trait LinuxPtraceStopInspector {
fn syscall_regs(&mut self, host_pid: i32) -> Result<InterceptedSyscall, FdError>;
fn syscall_return_value(&mut self, host_pid: i32) -> Result<i64, FdError>;
fn exec_guest_path(&mut self, host_pid: i32) -> Result<String, FdError>;
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) trait TraceeRegisterReader {
fn thread_regs(&mut self, host_pid: i32) -> Result<libc::user_regs_struct, FdError>;
fn thread_rseq(
&mut self,
host_pid: i32,
) -> Result<Option<KboxlikeThreadRseqSnapshot>, FdError> {
let _ = host_pid;
Ok(None)
}
}
pub(crate) trait TraceeCStringReader {
fn read_c_string(
&mut self,
host_pid: i32,
addr: u64,
max_len: usize,
) -> Result<String, FdError>;
}
pub(crate) trait TraceeFdArrayReader {
fn read_fd_array(&mut self, host_pid: i32, addr: u64, len: usize) -> Result<Vec<i32>, FdError>;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn read_u64_array(&mut self, host_pid: i32, addr: u64, len: usize)
-> Result<Vec<u64>, FdError>;
}
pub(crate) trait ExecGuestPathRecorder {
fn record_exec_guest_path(&mut self, host_pid: i32, guest_exe: String) -> Result<(), FdError>;
}
pub(crate) trait LinuxPtraceWaitReader {
fn wait_next(&mut self) -> Result<(i32, i32), FdError>;
fn event_msg(&mut self, host_pid: i32) -> Result<u64, FdError>;
}
pub(crate) trait LinuxPtraceConfigurator {
fn set_trace_options(&mut self, host_pid: i32, options: u64) -> Result<(), FdError>;
}
pub(crate) trait SyscallStopResumer {
fn resume_syscall_stop(&mut self, host_pid: i32) -> Result<(), FdError>;
fn resume_signal_stop(&mut self, host_pid: i32, signal: i32) -> Result<(), FdError>;
}
pub(crate) struct TraceeStopAccess<W, E, R> {
pub(crate) writer: W,
pub(crate) executor: E,
pub(crate) resumer: R,
}
pub(crate) trait TraceeAccessFactory {
type Writer: TraceeMemoryWriter;
type Executor: TraceeSyscallExecutor;
type Resumer: SyscallStopResumer;
fn access_for(
&mut self,
host_pid: i32,
) -> Result<TraceeStopAccess<Self::Writer, Self::Executor, Self::Resumer>, FdError>;
}
const LINUX_PTRACE_EVENT_FORK: i32 = 1;
const LINUX_PTRACE_EVENT_VFORK: i32 = 2;
const LINUX_PTRACE_EVENT_CLONE: i32 = 3;
const LINUX_PTRACE_EVENT_EXEC: i32 = 4;
const LINUX_PTRACE_EVENT_EXIT: i32 = 6;
const LINUX_PTRACE_EVENT_SHIFT: i32 = 16;
const LINUX_X86_64_SYS_OPEN: i64 = 2;
const LINUX_X86_64_SYS_CLOSE: i64 = 3;
const LINUX_X86_64_SYS_PIPE: i64 = 22;
const LINUX_X86_64_SYS_DUP: i64 = 32;
const LINUX_X86_64_SYS_DUP2: i64 = 33;
const LINUX_X86_64_SYS_SOCKET: i64 = 41;
const LINUX_X86_64_SYS_ACCEPT: i64 = 43;
const LINUX_X86_64_SYS_SHUTDOWN: i64 = 48;
const LINUX_X86_64_SYS_SOCKETPAIR: i64 = 53;
const LINUX_X86_64_SYS_CLONE: i64 = 56;
const LINUX_X86_64_SYS_EXECVE: i64 = 59;
const LINUX_X86_64_SYS_EXECVEAT: i64 = 322;
const LINUX_X86_64_SYS_FCNTL: i64 = 72;
const LINUX_X86_64_SYS_SET_TID_ADDRESS: i64 = 218;
const LINUX_X86_64_SYS_SET_ROBUST_LIST: i64 = 273;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const LINUX_X86_64_SYS_CLONE3: i64 = 435;
const LINUX_X86_64_SYS_INOTIFY_INIT: i64 = 253;
const LINUX_X86_64_SYS_TIMERFD_CREATE: i64 = 283;
const LINUX_X86_64_SYS_EVENTFD: i64 = 284;
const LINUX_X86_64_SYS_ACCEPT4: i64 = 288;
const LINUX_X86_64_SYS_SIGNALFD4: i64 = 289;
const LINUX_X86_64_SYS_EVENTFD2: i64 = 290;
const LINUX_X86_64_SYS_EPOLL_CREATE1: i64 = 291;
const LINUX_X86_64_SYS_OPENAT: i64 = 257;
const LINUX_X86_64_SYS_PIPE2: i64 = 293;
const LINUX_X86_64_SYS_INOTIFY_INIT1: i64 = 294;
const LINUX_X86_64_SYS_MEMFD_CREATE: i64 = 319;
const LINUX_X86_64_SYS_DUP3: i64 = 292;
const LINUX_X86_64_SYS_OPENAT2: i64 = 437;
const LINUX_WAIT_STOPPED_LOW: i32 = 0x7f;
const LINUX_WAIT_SYSCALL_STOP_SIGNAL: i32 = libc::SIGTRAP | 0x80;
const LINUX_EXEC_PATH_MAX: usize = 4096;
const LINUX_O_CLOEXEC: u64 = 0o2000000;
const LINUX_SOCK_CLOEXEC: u64 = LINUX_O_CLOEXEC;
const LINUX_F_DUPFD: u64 = 0;
const LINUX_F_SETFD: u64 = 2;
const LINUX_F_DUPFD_CLOEXEC: u64 = 1030;
const LINUX_FD_CLOEXEC: u64 = 1;
const LINUX_CLONE_CHILD_CLEARTID: u64 = 0x0020_0000;
const LINUX_CLONE_CHILD_SETTID: u64 = 0x0100_0000;
const LINUX_PTRACE_TRACE_OPTIONS: u64 = 0x0000_0001 | 0x0000_0002 | 0x0000_0004 | 0x0000_0008 | 0x0000_0010 | 0x0000_0040 | 0x0010_0000; #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const LINUX_PTRACE_WAIT_FLAGS: i32 = libc::__WALL | libc::__WNOTHREAD;
#[derive(Debug, Default)]
pub(crate) struct KboxlikeSyscallSupervisor {
registry: KboxlikeTraceeRegistry,
in_syscall: BTreeSet<i32>,
pending_syscalls: BTreeMap<i32, InterceptedSyscall>,
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
thread_kernel_states: BTreeMap<ModelThreadId, KboxlikeThreadKernelStateSnapshot>,
}
impl KboxlikeSyscallSupervisor {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn register_tracee(
&mut self,
host_pid: i32,
model_pid: ProcessId,
) -> Result<(), FdError> {
self.registry.register_tracee(host_pid, model_pid)?;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
let identity = self.registry.tracee_identity(host_pid)?;
self.thread_kernel_states.insert(
identity.model_thread_id,
KboxlikeThreadKernelStateSnapshot {
host_pid: identity.host_pid,
model_pid: identity.model_pid,
model_thread_id: identity.model_thread_id,
..KboxlikeThreadKernelStateSnapshot::default()
},
);
}
Ok(())
}
pub(crate) fn unregister_tracee(&mut self, host_pid: i32) -> Option<ProcessId> {
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Ok(identity) = self.registry.tracee_identity(host_pid) {
self.thread_kernel_states.remove(&identity.model_thread_id);
}
self.in_syscall.remove(&host_pid);
self.pending_syscalls.remove(&host_pid);
self.registry.unregister_tracee(host_pid)
}
pub(crate) fn model_pid_for_tracee(&self, host_pid: i32) -> Result<ProcessId, FdError> {
self.registry.model_pid(host_pid)
}
pub(crate) fn registered_tracees(&self) -> Vec<(i32, ProcessId)> {
self.registry.tracees()
}
pub(crate) fn registered_tracee_identities(&self) -> Vec<KboxlikeTraceeIdentity> {
self.registry.tracee_identities()
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn registered_thread_kernel_states(&self) -> Vec<KboxlikeThreadKernelStateSnapshot> {
self.thread_kernel_states.values().copied().collect()
}
pub(crate) fn handle_process_event(
&mut self,
fd_system: &mut KboxlikeFdSystem,
event: TraceeProcessEvent,
) -> Result<SupervisedProcessEventOutcome, FdError> {
match event {
TraceeProcessEvent::Fork {
parent_host_pid,
child_host_pid,
shares_fd_table,
} => {
if child_host_pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
if self.registry.contains_tracee(child_host_pid) {
return Err(FdError::TraceeInstallFailed(
"tracee pid already registered",
));
}
let parent_model_pid = self.registry.model_pid(parent_host_pid)?;
let child_model_pid = if shares_fd_table {
parent_model_pid
} else {
fd_system.fork(parent_model_pid)?
};
self.register_tracee(child_host_pid, child_model_pid)?;
Ok(SupervisedProcessEventOutcome::Forked {
parent_model_pid,
child_model_pid,
})
}
TraceeProcessEvent::Exec {
host_pid,
guest_exe,
} => {
let model_pid = self.registry.model_pid(host_pid)?;
fd_system.exec_with_path(model_pid, guest_exe)?;
Ok(SupervisedProcessEventOutcome::Execed { model_pid })
}
TraceeProcessEvent::Exit { host_pid } => {
let model_pid = self.registry.model_pid(host_pid)?;
self.unregister_tracee(host_pid);
if !self.registry.contains_model_pid(model_pid) {
fd_system.exit_process(model_pid)?;
}
Ok(SupervisedProcessEventOutcome::Exited { model_pid })
}
}
}
pub(crate) fn next_syscall_stop_phase(
&self,
host_pid: i32,
) -> Result<SyscallStopPhase, FdError> {
self.ensure_registered(host_pid)?;
if self.in_syscall.contains(&host_pid) {
Ok(SyscallStopPhase::Exit)
} else {
Ok(SyscallStopPhase::Entry)
}
}
pub(crate) fn handle_syscall_stop(
&mut self,
fd_system: &mut KboxlikeFdSystem,
host_pid: i32,
syscall: InterceptedSyscall,
supervisor_pid: i32,
writer: &mut impl TraceeMemoryWriter,
executor: &mut impl TraceeSyscallExecutor,
resumer: &mut impl SyscallStopResumer,
) -> Result<SupervisedSyscallStopOutcome, FdError> {
let phase = self.next_syscall_stop_phase(host_pid)?;
self.handle_syscall_stop_at_phase(
fd_system,
host_pid,
syscall,
phase,
supervisor_pid,
writer,
executor,
resumer,
)
}
pub(crate) fn handle_syscall_stop_at_phase(
&mut self,
fd_system: &mut KboxlikeFdSystem,
host_pid: i32,
syscall: InterceptedSyscall,
phase: SyscallStopPhase,
supervisor_pid: i32,
writer: &mut impl TraceeMemoryWriter,
executor: &mut impl TraceeSyscallExecutor,
resumer: &mut impl SyscallStopResumer,
) -> Result<SupervisedSyscallStopOutcome, FdError> {
self.ensure_registered(host_pid)?;
let outcome = match phase {
SyscallStopPhase::Entry => {
SupervisedSyscallStopOutcome::Entry(handle_intercepted_syscall_stop(
fd_system,
&self.registry,
host_pid,
syscall,
supervisor_pid,
writer,
executor,
)?)
}
SyscallStopPhase::Exit => SupervisedSyscallStopOutcome::Exit,
};
resumer.resume_syscall_stop(host_pid)?;
match phase {
SyscallStopPhase::Entry => {
self.pending_syscalls.insert(host_pid, syscall);
}
SyscallStopPhase::Exit => {
self.pending_syscalls.remove(&host_pid);
}
}
self.advance_after_resume(host_pid, phase);
Ok(outcome)
}
fn apply_successful_syscall_exit(
&mut self,
fd_system: &mut KboxlikeFdSystem,
host_pid: i32,
ret: i64,
reader: &mut impl TraceeFdArrayReader,
) -> Result<(), FdError> {
if ret < 0 {
return Ok(());
}
let Some(entry) = self.pending_syscalls.get(&host_pid).copied() else {
return Err(FdError::TraceeInstallFailed(
"missing syscall entry for exit stop",
));
};
apply_fd_lifecycle_syscall_exit(fd_system, &self.registry, host_pid, entry, ret, reader)?;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
self.apply_thread_kernel_state_syscall_exit(host_pid, entry, ret, reader)?;
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn apply_thread_kernel_state_syscall_exit(
&mut self,
host_pid: i32,
syscall: InterceptedSyscall,
ret: i64,
reader: &mut impl TraceeFdArrayReader,
) -> Result<(), FdError> {
match syscall.nr {
LINUX_X86_64_SYS_SET_TID_ADDRESS => {
let identity = self.registry.tracee_identity(host_pid)?;
let state = self.thread_kernel_state_mut(identity.model_thread_id)?;
state.clear_child_tid = Some(syscall.args[0]);
}
LINUX_X86_64_SYS_SET_ROBUST_LIST => {
let identity = self.registry.tracee_identity(host_pid)?;
let state = self.thread_kernel_state_mut(identity.model_thread_id)?;
state.robust_list_head = Some(syscall.args[0]);
state.robust_list_len = syscall.args[1];
}
LINUX_X86_64_SYS_CLONE => {
let flags = syscall.args[0];
let child_tid_ptr = syscall.args[3];
self.record_child_clear_tid_from_clone_exit(ret as i32, flags, child_tid_ptr)?;
}
LINUX_X86_64_SYS_CLONE3 => {
let args_addr = syscall.args[0];
let args_size = syscall.args[1];
if args_addr != 0 && args_size >= 24 {
let clone_args = reader.read_u64_array(host_pid, args_addr, 3)?;
let flags = clone_args[0];
let child_tid_ptr = clone_args[2];
self.record_child_clear_tid_from_clone_exit(ret as i32, flags, child_tid_ptr)?;
}
}
_ => {}
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn record_child_clear_tid_from_clone_exit(
&mut self,
child_host_pid: i32,
flags: u64,
child_tid_ptr: u64,
) -> Result<(), FdError> {
if child_tid_ptr != 0
&& flags & (LINUX_CLONE_CHILD_CLEARTID | LINUX_CLONE_CHILD_SETTID) != 0
&& self.registry.contains_tracee(child_host_pid)
{
let child_identity = self.registry.tracee_identity(child_host_pid)?;
let state = self.thread_kernel_state_mut(child_identity.model_thread_id)?;
state.clear_child_tid = Some(child_tid_ptr);
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn thread_kernel_state_mut(
&mut self,
model_thread_id: ModelThreadId,
) -> Result<&mut KboxlikeThreadKernelStateSnapshot, FdError> {
self.thread_kernel_states
.get_mut(&model_thread_id)
.ok_or(FdError::TraceeInstallFailed(
"missing thread kernel state snapshot",
))
}
fn advance_after_resume(&mut self, host_pid: i32, phase: SyscallStopPhase) {
match phase {
SyscallStopPhase::Entry => {
self.in_syscall.insert(host_pid);
}
SyscallStopPhase::Exit => {
self.in_syscall.remove(&host_pid);
}
}
}
fn ensure_registered(&self, host_pid: i32) -> Result<(), FdError> {
if host_pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
if !self.registry.contains_tracee(host_pid) {
return Err(FdError::TraceeInstallFailed("unknown tracee pid"));
}
Ok(())
}
}
pub(crate) fn capture_exec_guest_path_from_syscall(
supervisor: &KboxlikeSyscallSupervisor,
fd_system: &KboxlikeFdSystem,
host_pid: i32,
syscall: InterceptedSyscall,
reader: &mut impl TraceeCStringReader,
) -> Result<Option<String>, FdError> {
let path_addr = match syscall.nr {
LINUX_X86_64_SYS_EXECVE => syscall.args[0],
LINUX_X86_64_SYS_EXECVEAT => syscall.args[1],
_ => return Ok(None),
};
let raw_path = reader.read_c_string(host_pid, path_addr, LINUX_EXEC_PATH_MAX)?;
let model_pid = supervisor.model_pid_for_tracee(host_pid)?;
let guest_path = fd_system
.resolve_proc_exe(model_pid, &raw_path)?
.unwrap_or(raw_path);
Ok(Some(guest_path))
}
fn apply_fd_lifecycle_syscall_exit(
fd_system: &mut KboxlikeFdSystem,
registry: &KboxlikeTraceeRegistry,
host_pid: i32,
syscall: InterceptedSyscall,
ret: i64,
reader: &mut impl TraceeFdArrayReader,
) -> Result<(), FdError> {
let model_pid = registry.model_pid(host_pid)?;
match syscall.nr {
LINUX_X86_64_SYS_OPEN => {
let fd = fd_from_nonnegative_ret(ret)?;
let cloexec = syscall.args[1] & LINUX_O_CLOEXEC != 0;
fd_system.insert_plain_fd(model_pid, fd, Some(fd as i64), cloexec)
}
LINUX_X86_64_SYS_OPENAT => {
let fd = fd_from_nonnegative_ret(ret)?;
let cloexec = syscall.args[2] & LINUX_O_CLOEXEC != 0;
fd_system.insert_plain_fd(model_pid, fd, Some(fd as i64), cloexec)
}
LINUX_X86_64_SYS_OPENAT2 => {
let fd = fd_from_nonnegative_ret(ret)?;
fd_system.insert_plain_fd(model_pid, fd, Some(fd as i64), false)
}
LINUX_X86_64_SYS_CLOSE => {
let fd = fd_from_arg(syscall.args[0])?;
match fd_system.close(model_pid, fd) {
Ok(()) | Err(FdError::BadFd(_)) => Ok(()),
Err(err) => Err(err),
}
}
LINUX_X86_64_SYS_PIPE => {
let fds = reader.read_fd_array(host_pid, syscall.args[0], 2)?;
insert_plain_fd_pair(fd_system, model_pid, &fds, false)
}
LINUX_X86_64_SYS_PIPE2 => {
let fds = reader.read_fd_array(host_pid, syscall.args[0], 2)?;
let cloexec = syscall.args[1] & LINUX_O_CLOEXEC != 0;
insert_plain_fd_pair(fd_system, model_pid, &fds, cloexec)
}
LINUX_X86_64_SYS_SOCKET => {
let fd = fd_from_nonnegative_ret(ret)?;
let cloexec = syscall.args[1] & LINUX_SOCK_CLOEXEC != 0;
fd_system.insert_plain_fd(model_pid, fd, Some(fd as i64), cloexec)
}
LINUX_X86_64_SYS_ACCEPT => {
let fd = fd_from_nonnegative_ret(ret)?;
fd_system.insert_plain_fd(model_pid, fd, Some(fd as i64), false)
}
LINUX_X86_64_SYS_ACCEPT4 => {
let fd = fd_from_nonnegative_ret(ret)?;
let cloexec = syscall.args[3] & LINUX_SOCK_CLOEXEC != 0;
fd_system.insert_plain_fd(model_pid, fd, Some(fd as i64), cloexec)
}
LINUX_X86_64_SYS_SHUTDOWN => {
let fd = fd_from_arg(syscall.args[0])?;
let how = i32_from_arg(syscall.args[1])?;
match fd_system.set_socket_shutdown(model_pid, fd, how) {
Ok(()) | Err(FdError::BadFd(_)) => Ok(()),
Err(err) => Err(err),
}
}
LINUX_X86_64_SYS_SOCKETPAIR => {
let fds = reader.read_fd_array(host_pid, syscall.args[3], 2)?;
let cloexec = syscall.args[1] & LINUX_SOCK_CLOEXEC != 0;
insert_plain_fd_pair(fd_system, model_pid, &fds, cloexec)
}
LINUX_X86_64_SYS_EVENTFD
| LINUX_X86_64_SYS_EVENTFD2
| LINUX_X86_64_SYS_EPOLL_CREATE1
| LINUX_X86_64_SYS_INOTIFY_INIT
| LINUX_X86_64_SYS_INOTIFY_INIT1
| LINUX_X86_64_SYS_MEMFD_CREATE
| LINUX_X86_64_SYS_TIMERFD_CREATE
| LINUX_X86_64_SYS_SIGNALFD4 => {
let fd = fd_from_nonnegative_ret(ret)?;
let cloexec = syscall_args_have_cloexec(syscall);
fd_system.insert_plain_fd(model_pid, fd, Some(fd as i64), cloexec)
}
LINUX_X86_64_SYS_DUP => {
let oldfd = fd_from_arg(syscall.args[0])?;
let newfd = fd_from_nonnegative_ret(ret)?;
dup_or_track_plain_fd(fd_system, model_pid, oldfd, newfd, false)
}
LINUX_X86_64_SYS_DUP2 => {
let oldfd = fd_from_arg(syscall.args[0])?;
let newfd = fd_from_arg(syscall.args[1])?;
if ret != newfd as i64 {
return Err(FdError::TraceeInstallFailed("dup2 returned unexpected fd"));
}
dup_or_track_plain_fd(fd_system, model_pid, oldfd, newfd, false)
}
LINUX_X86_64_SYS_DUP3 => {
let oldfd = fd_from_arg(syscall.args[0])?;
let newfd = fd_from_arg(syscall.args[1])?;
if ret != newfd as i64 {
return Err(FdError::TraceeInstallFailed("dup3 returned unexpected fd"));
}
let cloexec = syscall.args[2] & LINUX_O_CLOEXEC != 0;
dup_or_track_plain_fd(fd_system, model_pid, oldfd, newfd, cloexec)
}
LINUX_X86_64_SYS_FCNTL => match syscall.args[1] {
LINUX_F_DUPFD => {
let oldfd = fd_from_arg(syscall.args[0])?;
let newfd = fd_from_nonnegative_ret(ret)?;
dup_or_track_plain_fd(fd_system, model_pid, oldfd, newfd, false)
}
LINUX_F_DUPFD_CLOEXEC => {
let oldfd = fd_from_arg(syscall.args[0])?;
let newfd = fd_from_nonnegative_ret(ret)?;
dup_or_track_plain_fd(fd_system, model_pid, oldfd, newfd, true)
}
LINUX_F_SETFD => {
let fd = fd_from_arg(syscall.args[0])?;
let cloexec = syscall.args[2] & LINUX_FD_CLOEXEC != 0;
match fd_system.set_cloexec(model_pid, fd, cloexec) {
Ok(()) | Err(FdError::BadFd(_)) => Ok(()),
Err(err) => Err(err),
}
}
_ => Ok(()),
},
_ => Ok(()),
}
}
fn insert_plain_fd_pair(
fd_system: &mut KboxlikeFdSystem,
model_pid: ProcessId,
fds: &[i32],
cloexec: bool,
) -> Result<(), FdError> {
if fds.len() != 2 {
return Err(FdError::TraceeInstallFailed(
"tracee fd array length mismatch",
));
}
for fd in fds {
fd_system.insert_plain_fd(model_pid, *fd, Some(*fd as i64), cloexec)?;
}
Ok(())
}
fn syscall_args_have_cloexec(syscall: InterceptedSyscall) -> bool {
match syscall.nr {
LINUX_X86_64_SYS_EVENTFD => false,
LINUX_X86_64_SYS_EVENTFD2 => syscall.args[1] & LINUX_O_CLOEXEC != 0,
LINUX_X86_64_SYS_EPOLL_CREATE1 => syscall.args[0] & LINUX_O_CLOEXEC != 0,
LINUX_X86_64_SYS_INOTIFY_INIT => false,
LINUX_X86_64_SYS_INOTIFY_INIT1 => syscall.args[0] & LINUX_O_CLOEXEC != 0,
LINUX_X86_64_SYS_MEMFD_CREATE => syscall.args[1] & LINUX_O_CLOEXEC != 0,
LINUX_X86_64_SYS_TIMERFD_CREATE => syscall.args[1] & LINUX_O_CLOEXEC != 0,
LINUX_X86_64_SYS_SIGNALFD4 => syscall.args[3] & LINUX_O_CLOEXEC != 0,
_ => false,
}
}
fn dup_or_track_plain_fd(
fd_system: &mut KboxlikeFdSystem,
model_pid: ProcessId,
oldfd: i32,
newfd: i32,
cloexec: bool,
) -> Result<(), FdError> {
match fd_system.dup_to(model_pid, oldfd, newfd, cloexec) {
Ok(()) => Ok(()),
Err(FdError::BadFd(_)) => {
fd_system.insert_plain_fd(model_pid, newfd, Some(newfd as i64), cloexec)
}
Err(err) => Err(err),
}
}
fn fd_from_arg(raw: u64) -> Result<i32, FdError> {
if raw > i32::MAX as u64 {
return Err(FdError::InvalidFd(raw as i32));
}
Ok(raw as i32)
}
fn i32_from_arg(raw: u64) -> Result<i32, FdError> {
if raw > i32::MAX as u64 {
return Err(FdError::TraceeInstallFailed(
"syscall argument outside i32 range",
));
}
Ok(raw as i32)
}
fn fd_from_nonnegative_ret(ret: i64) -> Result<i32, FdError> {
if ret < 0 || ret > i32::MAX as i64 {
return Err(FdError::InvalidFd(ret as i32));
}
Ok(ret as i32)
}
pub(crate) fn dispatch_linux_ptrace_stop(
supervisor: &mut KboxlikeSyscallSupervisor,
fd_system: &mut KboxlikeFdSystem,
stop: LinuxPtraceStop,
supervisor_pid: i32,
inspector: &mut (impl LinuxPtraceStopInspector
+ TraceeCStringReader
+ TraceeFdArrayReader
+ ExecGuestPathRecorder),
writer: &mut impl TraceeMemoryWriter,
executor: &mut impl TraceeSyscallExecutor,
resumer: &mut impl SyscallStopResumer,
) -> Result<LinuxPtraceDispatchOutcome, FdError> {
match stop {
LinuxPtraceStop::Syscall { host_pid } => {
let syscall = inspector.syscall_regs(host_pid)?;
let phase = supervisor.next_syscall_stop_phase(host_pid)?;
if std::env::var_os("SUPERMACHINE_KBOXLIVE_TRACE").is_some() {
eprintln!(
"KBOXLIVE_SYSCALL host_pid={host_pid} phase={phase:?} nr={} args={:?}",
syscall.nr, syscall.args
);
}
let syscall_return = if matches!(phase, SyscallStopPhase::Exit) {
Some(inspector.syscall_return_value(host_pid)?)
} else {
None
};
if let Some(ret) = syscall_return {
supervisor.apply_successful_syscall_exit(fd_system, host_pid, ret, inspector)?;
}
let exec_guest_path = if matches!(phase, SyscallStopPhase::Entry) {
capture_exec_guest_path_from_syscall(
supervisor, fd_system, host_pid, syscall, inspector,
)
.map_err(|err| {
if std::env::var_os("SUPERMACHINE_KBOXLIVE_TRACE").is_some() {
eprintln!(
"KBOXLIVE_CAPTURE_EXEC_PATH_ERR host_pid={} nr={} args={:?} err={:?}",
host_pid, syscall.nr, syscall.args, err
);
}
err
})?
} else {
None
};
supervisor
.handle_syscall_stop_at_phase(
fd_system,
host_pid,
syscall,
phase,
supervisor_pid,
writer,
executor,
resumer,
)
.map_err(|err| {
if std::env::var_os("SUPERMACHINE_KBOXLIVE_TRACE").is_some() {
eprintln!(
"KBOXLIVE_HANDLE_SYSCALL_ERR host_pid={} phase={:?} nr={} args={:?} err={:?}",
host_pid, phase, syscall.nr, syscall.args, err
);
}
err
})
.and_then(|outcome| {
if let Some(guest_exe) = exec_guest_path {
inspector.record_exec_guest_path(host_pid, guest_exe)?;
}
Ok(LinuxPtraceDispatchOutcome::Syscall(outcome))
})
}
LinuxPtraceStop::Fork {
parent_host_pid,
child_host_pid,
shares_fd_table,
} => {
let outcome = supervisor.handle_process_event(
fd_system,
TraceeProcessEvent::Fork {
parent_host_pid,
child_host_pid,
shares_fd_table,
},
)?;
resumer.resume_syscall_stop(parent_host_pid)?;
Ok(LinuxPtraceDispatchOutcome::Process(outcome))
}
LinuxPtraceStop::Exec { host_pid } => {
let guest_exe = inspector.exec_guest_path(host_pid)?;
let outcome = supervisor.handle_process_event(
fd_system,
TraceeProcessEvent::Exec {
host_pid,
guest_exe,
},
)?;
resumer.resume_syscall_stop(host_pid)?;
Ok(LinuxPtraceDispatchOutcome::Process(outcome))
}
LinuxPtraceStop::Exit { host_pid } => {
let outcome = supervisor
.handle_process_event(fd_system, TraceeProcessEvent::Exit { host_pid })?;
resumer.resume_syscall_stop(host_pid)?;
Ok(LinuxPtraceDispatchOutcome::Process(outcome))
}
LinuxPtraceStop::Signal { host_pid, signal } => {
resumer.resume_signal_stop(host_pid, signal)?;
Ok(LinuxPtraceDispatchOutcome::Signal { host_pid, signal })
}
LinuxPtraceStop::Exited { host_pid, code } => {
let _ =
supervisor.handle_process_event(fd_system, TraceeProcessEvent::Exit { host_pid });
Ok(LinuxPtraceDispatchOutcome::ProcessExited { host_pid, code })
}
LinuxPtraceStop::Signaled { host_pid, signal } => {
let _ =
supervisor.handle_process_event(fd_system, TraceeProcessEvent::Exit { host_pid });
Ok(LinuxPtraceDispatchOutcome::ProcessSignaled { host_pid, signal })
}
}
}
pub(crate) fn configure_initial_linux_ptrace_tracee(
supervisor: &mut KboxlikeSyscallSupervisor,
fd_system: &mut KboxlikeFdSystem,
host_pid: i32,
guest_exe: impl Into<String>,
configurator: &mut impl LinuxPtraceConfigurator,
resumer: &mut impl SyscallStopResumer,
) -> Result<ProcessId, FdError> {
if host_pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
configurator.set_trace_options(host_pid, LINUX_PTRACE_TRACE_OPTIONS)?;
let model_pid = fd_system.spawn_init_with_exe(guest_exe);
if let Err(err) = seed_initial_fds(fd_system, model_pid, host_pid) {
let _ = fd_system.exit_process(model_pid);
return Err(err);
}
if let Err(err) = supervisor.register_tracee(host_pid, model_pid) {
let _ = fd_system.exit_process(model_pid);
return Err(err);
}
if let Err(err) = resumer.resume_syscall_stop(host_pid) {
supervisor.unregister_tracee(host_pid);
let _ = fd_system.exit_process(model_pid);
return Err(err);
}
Ok(model_pid)
}
fn seed_initial_fds(
fd_system: &mut KboxlikeFdSystem,
model_pid: ProcessId,
host_pid: i32,
) -> Result<(), FdError> {
let mut seeded = false;
if let Ok(entries) = std::fs::read_dir(format!("/proc/{host_pid}/fd")) {
for entry in entries.flatten() {
let name = entry.file_name();
let Some(name) = name.to_str() else {
continue;
};
let Ok(fd) = name.parse::<i32>() else {
continue;
};
if fd >= 0 {
fd_system.insert_plain_fd(model_pid, fd, Some(fd as i64), false)?;
seeded = true;
}
}
}
if seeded {
return Ok(());
}
for fd in 0..=2 {
fd_system.insert_plain_fd(model_pid, fd, Some(fd as i64), false)?;
}
Ok(())
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct LinuxPtraceLoopSummary {
pub(crate) dispatched: usize,
pub(crate) terminal: Option<LinuxPtraceDispatchOutcome>,
}
pub(crate) fn run_linux_ptrace_dispatch_loop<T, A>(
supervisor: &mut KboxlikeSyscallSupervisor,
fd_system: &mut KboxlikeFdSystem,
supervisor_pid: i32,
transport: &mut T,
access_factory: &mut A,
max_stops: usize,
) -> Result<LinuxPtraceLoopSummary, FdError>
where
T: LinuxPtraceWaitReader
+ LinuxPtraceStopInspector
+ TraceeCStringReader
+ TraceeFdArrayReader
+ ExecGuestPathRecorder,
A: TraceeAccessFactory,
{
if max_stops == 0 {
return Err(FdError::TraceeInstallFailed(
"ptrace dispatch loop max stops is zero",
));
}
let mut summary = LinuxPtraceLoopSummary::default();
for _ in 0..max_stops {
let stop = read_linux_ptrace_stop(transport)?;
let host_pid = stop.host_pid();
let mut access = access_factory.access_for(host_pid)?;
let outcome = dispatch_linux_ptrace_stop(
supervisor,
fd_system,
stop,
supervisor_pid,
transport,
&mut access.writer,
&mut access.executor,
&mut access.resumer,
)?;
summary.dispatched += 1;
if matches!(
outcome,
LinuxPtraceDispatchOutcome::ProcessExited { .. }
| LinuxPtraceDispatchOutcome::ProcessSignaled { .. }
) {
summary.terminal = Some(outcome);
return Ok(summary);
}
}
Ok(summary)
}
pub(crate) fn decode_linux_ptrace_wait_status(
host_pid: i32,
status: i32,
event_msg: Option<u64>,
) -> Result<LinuxPtraceStop, FdError> {
if host_pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
if libc::WIFEXITED(status) {
return Ok(LinuxPtraceStop::Exited {
host_pid,
code: libc::WEXITSTATUS(status),
});
}
if libc::WIFSIGNALED(status) {
return Ok(LinuxPtraceStop::Signaled {
host_pid,
signal: libc::WTERMSIG(status),
});
}
if !libc::WIFSTOPPED(status) {
return Err(FdError::TraceeInstallFailed("unsupported wait status"));
}
let signal = libc::WSTOPSIG(status);
if signal == LINUX_WAIT_SYSCALL_STOP_SIGNAL {
return Ok(LinuxPtraceStop::Syscall { host_pid });
}
let event = ptrace_event_from_status(status);
match event {
LINUX_PTRACE_EVENT_FORK | LINUX_PTRACE_EVENT_VFORK | LINUX_PTRACE_EVENT_CLONE => {
let child_host_pid =
event_msg.ok_or(FdError::TraceeInstallFailed("missing ptrace event msg"))? as i32;
if child_host_pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
Ok(LinuxPtraceStop::Fork {
parent_host_pid: host_pid,
child_host_pid,
shares_fd_table: event == LINUX_PTRACE_EVENT_CLONE,
})
}
LINUX_PTRACE_EVENT_EXEC => Ok(LinuxPtraceStop::Exec { host_pid }),
LINUX_PTRACE_EVENT_EXIT => Ok(LinuxPtraceStop::Exit { host_pid }),
_ => Ok(LinuxPtraceStop::Signal { host_pid, signal }),
}
}
fn ptrace_event_from_status(status: i32) -> i32 {
status >> LINUX_PTRACE_EVENT_SHIFT
}
fn ptrace_event_needs_message(status: i32) -> bool {
matches!(
ptrace_event_from_status(status),
LINUX_PTRACE_EVENT_FORK | LINUX_PTRACE_EVENT_VFORK | LINUX_PTRACE_EVENT_CLONE
)
}
pub(crate) fn read_linux_ptrace_stop(
reader: &mut impl LinuxPtraceWaitReader,
) -> Result<LinuxPtraceStop, FdError> {
let (host_pid, status) = reader.wait_next()?;
let event_msg = if ptrace_event_needs_message(status) {
Some(reader.event_msg(host_pid)?)
} else {
None
};
decode_linux_ptrace_wait_status(host_pid, status, event_msg)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Debug, Default)]
pub(crate) struct LinuxPtraceReader {
exec_paths: BTreeMap<i32, String>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl LinuxPtraceReader {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn record_exec_guest_path(
&mut self,
host_pid: i32,
guest_exe: impl Into<String>,
) -> Result<(), FdError> {
if host_pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
self.exec_paths.insert(host_pid, guest_exe.into());
Ok(())
}
fn ptrace_geteventmsg(&self, host_pid: i32) -> Result<u64, FdError> {
if host_pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
let mut msg = 0u64;
let ret = unsafe {
libc::ptrace(
libc::PTRACE_GETEVENTMSG,
host_pid,
std::ptr::null_mut::<libc::c_void>(),
&mut msg as *mut _ as *mut libc::c_void,
)
};
if ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace geteventmsg failed"));
}
Ok(msg)
}
fn ptrace_getregs(&self, host_pid: i32) -> Result<libc::user_regs_struct, FdError> {
if host_pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
let mut regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
let ret = unsafe {
libc::ptrace(
libc::PTRACE_GETREGS,
host_pid,
std::ptr::null_mut::<libc::c_void>(),
&mut regs as *mut _ as *mut libc::c_void,
)
};
if ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace getregs failed"));
}
Ok(regs)
}
fn process_vm_read_c_string(
&self,
host_pid: i32,
addr: u64,
max_len: usize,
) -> Result<String, FdError> {
if host_pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
if addr == 0 {
return Err(FdError::TraceeInstallFailed(
"tracee cstring address is null",
));
}
if max_len == 0 {
return Err(FdError::TraceeInstallFailed(
"tracee cstring max length is zero",
));
}
let mut out = Vec::new();
while out.len() < max_len {
let remaining = max_len - out.len();
let chunk_len = remaining.min(256);
let mut chunk = vec![0u8; chunk_len];
let local = libc::iovec {
iov_base: chunk.as_mut_ptr() as *mut libc::c_void,
iov_len: chunk.len(),
};
let remote = libc::iovec {
iov_base: (addr + out.len() as u64) as *mut libc::c_void,
iov_len: chunk.len(),
};
let read = unsafe { libc::process_vm_readv(host_pid, &local, 1, &remote, 1, 0) };
if read < 0 {
return Err(FdError::TraceeInstallFailed("process_vm_readv failed"));
}
if read == 0 {
return Err(FdError::TraceeInstallFailed(
"tracee cstring read returned zero",
));
}
let read = read as usize;
if let Some(nul) = chunk[..read].iter().position(|b| *b == 0) {
out.extend_from_slice(&chunk[..nul]);
return String::from_utf8(out)
.map_err(|_| FdError::TraceeInstallFailed("tracee cstring invalid utf8"));
}
out.extend_from_slice(&chunk[..read]);
}
Err(FdError::TraceeInstallFailed(
"tracee cstring exceeds max length",
))
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl LinuxPtraceConfigurator for LinuxPtraceReader {
fn set_trace_options(&mut self, host_pid: i32, options: u64) -> Result<(), FdError> {
if host_pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
let ret = unsafe {
libc::ptrace(
libc::PTRACE_SETOPTIONS,
host_pid,
std::ptr::null_mut::<libc::c_void>(),
options as usize as *mut libc::c_void,
)
};
if ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace setoptions failed"));
}
Ok(())
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl LinuxPtraceWaitReader for LinuxPtraceReader {
fn wait_next(&mut self) -> Result<(i32, i32), FdError> {
waitpid_any_tracee_blocking_with(|status| unsafe {
libc::waitpid(-1, status, LINUX_PTRACE_WAIT_FLAGS)
})
}
fn event_msg(&mut self, host_pid: i32) -> Result<u64, FdError> {
self.ptrace_geteventmsg(host_pid)
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl LinuxPtraceStopInspector for LinuxPtraceReader {
fn syscall_regs(&mut self, host_pid: i32) -> Result<InterceptedSyscall, FdError> {
Ok(InterceptedSyscall::from_linux_x86_64_regs(
&self.ptrace_getregs(host_pid)?,
))
}
fn syscall_return_value(&mut self, host_pid: i32) -> Result<i64, FdError> {
Ok(self.ptrace_getregs(host_pid)?.rax as i64)
}
fn exec_guest_path(&mut self, host_pid: i32) -> Result<String, FdError> {
self.exec_paths
.remove(&host_pid)
.ok_or(FdError::TraceeInstallFailed("missing exec guest path"))
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeRegisterReader for LinuxPtraceReader {
fn thread_regs(&mut self, host_pid: i32) -> Result<libc::user_regs_struct, FdError> {
self.ptrace_getregs(host_pid)
}
fn thread_rseq(
&mut self,
host_pid: i32,
) -> Result<Option<KboxlikeThreadRseqSnapshot>, FdError> {
read_thread_rseq_configuration(host_pid)
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeCStringReader for LinuxPtraceReader {
fn read_c_string(
&mut self,
host_pid: i32,
addr: u64,
max_len: usize,
) -> Result<String, FdError> {
self.process_vm_read_c_string(host_pid, addr, max_len)
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeFdArrayReader for LinuxPtraceReader {
fn read_fd_array(&mut self, host_pid: i32, addr: u64, len: usize) -> Result<Vec<i32>, FdError> {
if host_pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
if addr == 0 {
return Err(FdError::TraceeInstallFailed(
"tracee fd array address is null",
));
}
if len == 0 {
return Err(FdError::TraceeInstallFailed(
"tracee fd array length is zero",
));
}
let mut bytes = vec![0u8; len * std::mem::size_of::<i32>()];
let local = libc::iovec {
iov_base: bytes.as_mut_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 read = unsafe { libc::process_vm_readv(host_pid, &local, 1, &remote, 1, 0) };
if read < 0 {
return Err(FdError::TraceeInstallFailed("process_vm_readv failed"));
}
if read as usize != bytes.len() {
return Err(FdError::TraceeInstallFailed(
"process_vm_readv partial fd array read",
));
}
bytes
.chunks_exact(std::mem::size_of::<i32>())
.map(|chunk| {
Ok(i32::from_ne_bytes(chunk.try_into().map_err(|_| {
FdError::TraceeInstallFailed("invalid fd array chunk")
})?))
})
.collect()
}
fn read_u64_array(
&mut self,
host_pid: i32,
addr: u64,
len: usize,
) -> Result<Vec<u64>, FdError> {
if host_pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
if addr == 0 {
return Err(FdError::TraceeInstallFailed(
"tracee u64 array address is null",
));
}
if len == 0 {
return Err(FdError::TraceeInstallFailed(
"tracee u64 array length is zero",
));
}
let mut bytes = vec![0u8; len * std::mem::size_of::<u64>()];
let local = libc::iovec {
iov_base: bytes.as_mut_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 read = unsafe { libc::process_vm_readv(host_pid, &local, 1, &remote, 1, 0) };
if read < 0 {
return Err(FdError::TraceeInstallFailed("process_vm_readv failed"));
}
if read as usize != bytes.len() {
return Err(FdError::TraceeInstallFailed(
"process_vm_readv partial u64 array read",
));
}
bytes
.chunks_exact(std::mem::size_of::<u64>())
.map(|chunk| {
Ok(u64::from_ne_bytes(chunk.try_into().map_err(|_| {
FdError::TraceeInstallFailed("invalid u64 array chunk")
})?))
})
.collect()
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl ExecGuestPathRecorder for LinuxPtraceReader {
fn record_exec_guest_path(&mut self, host_pid: i32, guest_exe: String) -> Result<(), FdError> {
LinuxPtraceReader::record_exec_guest_path(self, host_pid, guest_exe)
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct PtraceSyscallStopResumer;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl SyscallStopResumer for PtraceSyscallStopResumer {
fn resume_syscall_stop(&mut self, host_pid: i32) -> Result<(), FdError> {
ptrace_syscall_resume(host_pid, 0)
}
fn resume_signal_stop(&mut self, host_pid: i32, signal: i32) -> Result<(), FdError> {
ptrace_syscall_resume(host_pid, signal)
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn ptrace_syscall_resume(host_pid: i32, signal: i32) -> Result<(), FdError> {
if host_pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
if signal < 0 {
return Err(FdError::TraceeInstallFailed("invalid signal"));
}
let ret = unsafe {
libc::ptrace(
libc::PTRACE_SYSCALL,
host_pid,
std::ptr::null_mut::<libc::c_void>(),
signal as usize as *mut libc::c_void,
)
};
if ret < 0 {
if std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH) {
return Ok(());
}
return Err(FdError::TraceeInstallFailed("ptrace syscall resume failed"));
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct LinuxPtraceTraceeAccessFactory;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeAccessFactory for LinuxPtraceTraceeAccessFactory {
type Writer = ProcessVmMemoryWriter;
type Executor = PtraceSyscallExecutor;
type Resumer = PtraceSyscallStopResumer;
fn access_for(
&mut self,
host_pid: i32,
) -> Result<TraceeStopAccess<Self::Writer, Self::Executor, Self::Resumer>, FdError> {
Ok(TraceeStopAccess {
writer: ProcessVmMemoryWriter::new(host_pid)?,
executor: PtraceSyscallExecutor::new(host_pid)?,
resumer: PtraceSyscallStopResumer,
})
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Debug)]
pub(crate) struct KboxlikeCapturedLiveSnapshot {
pub(crate) root_host_pid: i32,
pub(crate) snapshot: KboxlikeStoppedTraceeSnapshot,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Debug)]
pub(crate) struct KboxlikeRootfsExecConfig {
pub(crate) rootfs: PathBuf,
pub(crate) argv: Vec<String>,
pub(crate) env: Vec<(String, String)>,
pub(crate) cwd: String,
pub(crate) user: Option<(u32, u32)>,
pub(crate) mounts: Vec<(PathBuf, String)>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug)]
pub(crate) struct KboxlikeCaptureSettle {
pub(crate) stop_after: std::time::Duration,
pub(crate) ready_file: Option<std::path::PathBuf>,
pub(crate) ready_pattern: Option<String>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn capture_live_ptrace_exec_snapshot_after_sigstop_request(
argv: &[String],
min_tracees: usize,
max_stops: usize,
settle: KboxlikeCaptureSettle,
) -> Result<KboxlikeCapturedLiveSnapshot, FdError> {
if argv.is_empty() {
return Err(FdError::TraceeInstallFailed("live exec argv is empty"));
}
let argv_cstrings = argv
.iter()
.map(|arg| {
CString::new(arg.as_str())
.map_err(|_| FdError::TraceeInstallFailed("argv contains nul byte"))
})
.collect::<Result<Vec<_>, _>>()?;
let mut argv_ptrs = argv_cstrings
.iter()
.map(|arg| arg.as_ptr())
.collect::<Vec<_>>();
argv_ptrs.push(std::ptr::null());
let path = argv_cstrings[0].as_ptr();
let child = unsafe { libc::fork() };
if child < 0 {
return Err(FdError::TraceeInstallFailed("fork failed"));
}
if child == 0 {
unsafe {
libc::setpgid(0, 0);
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::execv(path, argv_ptrs.as_ptr());
libc::_exit(112);
}
}
let result =
capture_live_ptrace_child_after_sigstop_request(child, min_tracees, max_stops, settle).map(
|snapshot| KboxlikeCapturedLiveSnapshot {
root_host_pid: child,
snapshot,
},
);
if result.is_err() {
kill_and_reap_live_process_group(child);
}
result
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn capture_live_ptrace_rootfs_exec_snapshot_after_sigstop_request(
config: &KboxlikeRootfsExecConfig,
min_tracees: usize,
max_stops: usize,
settle: KboxlikeCaptureSettle,
) -> Result<KboxlikeCapturedLiveSnapshot, FdError> {
if config.argv.is_empty() {
return Err(FdError::TraceeInstallFailed("live exec argv is empty"));
}
let argv_cstrings = config
.argv
.iter()
.map(|arg| {
CString::new(arg.as_str())
.map_err(|_| FdError::TraceeInstallFailed("argv contains nul byte"))
})
.collect::<Result<Vec<_>, _>>()?;
let mut argv_ptrs = argv_cstrings
.iter()
.map(|arg| arg.as_ptr())
.collect::<Vec<_>>();
argv_ptrs.push(std::ptr::null());
let path = argv_cstrings[0].as_ptr();
crate::kboxlike::prepare_rootfs_network_config(&config.rootfs).map_err(|_| {
FdError::TraceeInstallFailed("prepare kboxlike rootfs network config failed")
})?;
let mut env_pairs = config.env.clone();
crate::kboxlike::merge_host_proxy_env_if_absent(&mut env_pairs);
let env_cstrings = env_pairs
.iter()
.map(|(key, value)| {
CString::new(format!("{key}={value}"))
.map_err(|_| FdError::TraceeInstallFailed("env contains nul byte"))
})
.collect::<Result<Vec<_>, _>>()?;
let mut env_ptrs = env_cstrings
.iter()
.map(|value| value.as_ptr())
.collect::<Vec<_>>();
env_ptrs.push(std::ptr::null());
let rootfs = CString::new(config.rootfs.as_os_str().as_bytes())
.map_err(|_| FdError::TraceeInstallFailed("rootfs contains nul byte"))?;
let cwd = CString::new(config.cwd.as_str())
.map_err(|_| FdError::TraceeInstallFailed("cwd contains nul byte"))?;
let mounts = prepare_rootfs_runtime_mounts(&config.rootfs, &config.mounts)?;
let child = unsafe { libc::fork() };
if child < 0 {
return Err(FdError::TraceeInstallFailed("fork failed"));
}
if child == 0 {
unsafe {
libc::setpgid(0, 0);
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);
if setup_rootfs_runtime_mounts(&mounts) < 0 {
libc::_exit(118);
}
if libc::chroot(rootfs.as_ptr()) < 0 {
libc::_exit(113);
}
if libc::chdir(cwd.as_ptr()) < 0 {
libc::_exit(114);
}
if let Some((uid, gid)) = config.user {
if libc::setgroups(0, std::ptr::null()) < 0 {
libc::_exit(115);
}
if libc::setgid(gid as libc::gid_t) < 0 {
libc::_exit(116);
}
if libc::setuid(uid as libc::uid_t) < 0 {
libc::_exit(117);
}
}
libc::execve(path, argv_ptrs.as_ptr(), env_ptrs.as_ptr());
libc::_exit(112);
}
}
let result =
capture_live_ptrace_child_after_sigstop_request(child, min_tracees, max_stops, settle).map(
|snapshot| KboxlikeCapturedLiveSnapshot {
root_host_pid: child,
snapshot,
},
);
if result.is_err() {
kill_and_reap_live_process_group(child);
}
result
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
struct PreparedRootfsRuntimeMounts {
binds: Vec<(CString, CString)>,
proc_target: CString,
shm_target: CString,
pts_target: CString,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn prepare_rootfs_runtime_mounts(
rootfs: &std::path::Path,
mounts: &[(PathBuf, String)],
) -> Result<PreparedRootfsRuntimeMounts, FdError> {
create_nested_bind_mount_targets(mounts)?;
let mut prepared = Vec::with_capacity(mounts.len());
for (host_path, guest_path) in mounts {
let target = confined_rootfs_join(rootfs, guest_path)?;
std::fs::create_dir_all(&target)
.map_err(|_| FdError::TraceeInstallFailed("create mount target failed"))?;
let host = CString::new(host_path.as_os_str().as_bytes())
.map_err(|_| FdError::TraceeInstallFailed("mount host path contains nul byte"))?;
let target = CString::new(target.as_os_str().as_bytes())
.map_err(|_| FdError::TraceeInstallFailed("mount target contains nul byte"))?;
prepared.push((host, target));
}
append_standard_dev_bind_mounts(rootfs, &mut prepared)?;
let proc_target = rootfs.join("proc");
std::fs::create_dir_all(&proc_target)
.map_err(|_| FdError::TraceeInstallFailed("create /proc failed"))?;
let shm_target = rootfs.join("dev").join("shm");
std::fs::create_dir_all(&shm_target)
.map_err(|_| FdError::TraceeInstallFailed("create /dev/shm failed"))?;
let pts_target = rootfs.join("dev").join("pts");
std::fs::create_dir_all(&pts_target)
.map_err(|_| FdError::TraceeInstallFailed("create /dev/pts failed"))?;
install_standard_dev_symlinks(rootfs)?;
let proc_target = CString::new(proc_target.as_os_str().as_bytes())
.map_err(|_| FdError::TraceeInstallFailed("proc target path contains nul byte"))?;
let shm_target = CString::new(shm_target.as_os_str().as_bytes())
.map_err(|_| FdError::TraceeInstallFailed("shm target path contains nul byte"))?;
let pts_target = CString::new(pts_target.as_os_str().as_bytes())
.map_err(|_| FdError::TraceeInstallFailed("pts target path contains nul byte"))?;
Ok(PreparedRootfsRuntimeMounts {
binds: prepared,
proc_target,
shm_target,
pts_target,
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn create_nested_bind_mount_targets(mounts: &[(PathBuf, String)]) -> Result<(), FdError> {
for (parent_host, parent_guest) in mounts {
for (_, child_guest) in mounts {
let Some(relative_child) = nested_guest_mount_suffix(parent_guest, child_guest) else {
continue;
};
std::fs::create_dir_all(parent_host.join(relative_child))
.map_err(|_| FdError::TraceeInstallFailed("create nested mount target failed"))?;
}
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn nested_guest_mount_suffix(parent_guest: &str, child_guest: &str) -> Option<PathBuf> {
let parent = std::path::Path::new(parent_guest.strip_prefix('/').unwrap_or(parent_guest));
let child = std::path::Path::new(child_guest.strip_prefix('/').unwrap_or(child_guest));
if parent == child {
return None;
}
child
.strip_prefix(parent)
.ok()
.map(std::path::Path::to_path_buf)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn append_standard_dev_bind_mounts(
rootfs: &std::path::Path,
prepared: &mut Vec<(CString, CString)>,
) -> Result<(), FdError> {
let dev_dir = rootfs.join("dev");
std::fs::create_dir_all(&dev_dir)
.map_err(|_| FdError::TraceeInstallFailed("create /dev failed"))?;
for name in ["null", "zero", "random", "urandom"] {
let host_path = PathBuf::from("/dev").join(name);
if !host_path.exists() {
continue;
}
let target = dev_dir.join(name);
if !target.exists() {
std::fs::File::create(&target)
.map_err(|_| FdError::TraceeInstallFailed("create device bind target failed"))?;
}
let host = CString::new(host_path.as_os_str().as_bytes())
.map_err(|_| FdError::TraceeInstallFailed("device host path contains nul byte"))?;
let target = CString::new(target.as_os_str().as_bytes())
.map_err(|_| FdError::TraceeInstallFailed("device target path contains nul byte"))?;
prepared.push((host, target));
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn install_standard_dev_symlinks(rootfs: &std::path::Path) -> Result<(), FdError> {
use std::os::unix::fs::symlink;
let dev_dir = rootfs.join("dev");
std::fs::create_dir_all(&dev_dir)
.map_err(|_| FdError::TraceeInstallFailed("create /dev failed"))?;
for (link, target) in [
("fd", "/proc/self/fd"),
("stdin", "/proc/self/fd/0"),
("stdout", "/proc/self/fd/1"),
("stderr", "/proc/self/fd/2"),
("ptmx", "pts/ptmx"),
] {
let path = dev_dir.join(link);
if matches!(std::fs::read_link(&path), Ok(existing) if existing.as_os_str() == std::ffi::OsStr::new(target))
{
continue;
}
match std::fs::symlink_metadata(&path) {
Ok(meta) if meta.file_type().is_symlink() || meta.is_file() => {
let _ = std::fs::remove_file(&path);
}
Ok(meta) if meta.is_dir() => {
let _ = std::fs::remove_dir(&path);
}
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(_) => return Err(FdError::TraceeInstallFailed("inspect /dev symlink failed")),
}
match symlink(target, &path) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(_) => return Err(FdError::TraceeInstallFailed("create /dev symlink failed")),
}
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn confined_rootfs_join(rootfs: &std::path::Path, guest_path: &str) -> Result<PathBuf, FdError> {
let rel = guest_path.strip_prefix('/').unwrap_or(guest_path);
let p = std::path::Path::new(rel);
if rel.is_empty()
|| p.components().any(|component| {
matches!(
component,
std::path::Component::ParentDir | std::path::Component::RootDir
)
})
{
return Err(FdError::TraceeInstallFailed(
"mount guest path escapes rootfs",
));
}
Ok(rootfs.join(p))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn setup_rootfs_runtime_mounts(mounts: &PreparedRootfsRuntimeMounts) -> i32 {
unsafe {
if libc::unshare(libc::CLONE_NEWNS) < 0 {
return -1;
}
if libc::mount(
std::ptr::null(),
c"/".as_ptr(),
std::ptr::null(),
(libc::MS_REC | libc::MS_PRIVATE) as libc::c_ulong,
std::ptr::null(),
) < 0
{
return -1;
}
for (host, target) in &mounts.binds {
if libc::mount(
host.as_ptr(),
target.as_ptr(),
std::ptr::null(),
(libc::MS_BIND | libc::MS_REC) as libc::c_ulong,
std::ptr::null(),
) < 0
{
return -1;
}
}
if libc::mount(
c"proc".as_ptr(),
mounts.proc_target.as_ptr(),
c"proc".as_ptr(),
(libc::MS_NOSUID | libc::MS_NOEXEC | libc::MS_NODEV) as libc::c_ulong,
std::ptr::null(),
) < 0
{
return -1;
}
if libc::mount(
c"tmpfs".as_ptr(),
mounts.shm_target.as_ptr(),
c"tmpfs".as_ptr(),
(libc::MS_NOSUID | libc::MS_NODEV) as libc::c_ulong,
c"mode=1777,size=64m".as_ptr().cast(),
) < 0
{
return -1;
}
if libc::mount(
c"devpts".as_ptr(),
mounts.pts_target.as_ptr(),
c"devpts".as_ptr(),
(libc::MS_NOSUID | libc::MS_NOEXEC) as libc::c_ulong,
c"newinstance,ptmxmode=0666,mode=0620,gid=5".as_ptr().cast(),
) < 0
{
return -1;
}
}
0
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn capture_live_ptrace_child_after_sigstop_request(
child: i32,
min_tracees: usize,
max_stops: usize,
settle: KboxlikeCaptureSettle,
) -> Result<KboxlikeStoppedTraceeSnapshot, FdError> {
wait_for_traceme_sigstop(child)?;
let mut fds = KboxlikeFdSystem::new();
let mut supervisor = KboxlikeSyscallSupervisor::new();
let mut reader = LinuxPtraceReader::default();
let mut resumer = PtraceSyscallStopResumer;
configure_initial_linux_ptrace_tracee(
&mut supervisor,
&mut fds,
child,
"/proc/self/exe",
&mut reader,
&mut resumer,
)?;
let mut access_factory = LinuxPtraceTraceeAccessFactory;
let mut parked_tracees = BTreeSet::new();
let mut stop_requested = false;
let stop_now = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let stopper_child = child;
let stopper_stop_now = std::sync::Arc::clone(&stop_now);
let _stopper = std::thread::spawn(move || {
let started = std::time::Instant::now();
loop {
let remaining = settle.stop_after.saturating_sub(started.elapsed());
if remaining.is_zero() {
break;
}
if let Some(ready_file) = &settle.ready_file {
let ready = match std::fs::read_to_string(ready_file) {
Ok(text) => match &settle.ready_pattern {
Some(pattern) => text.contains(pattern),
None => true,
},
Err(_) => false,
};
if ready {
break;
}
}
std::thread::sleep(remaining.min(std::time::Duration::from_millis(200)));
}
stopper_stop_now.store(true, std::sync::atomic::Ordering::Relaxed);
if std::env::var_os("SUPERMACHINE_KBOXLIKE_TRACE_CAPTURE").is_some() {
eprintln!("KBOXLIVE_SIGSTOP_REQUEST pgid={stopper_child}");
}
unsafe {
libc::kill(-stopper_child, libc::SIGSTOP);
libc::kill(stopper_child, libc::SIGSTOP);
}
});
let mut premature_child_stops: BTreeSet<i32> = BTreeSet::new();
let mut io_uring_deny_exits: BTreeSet<i32> = BTreeSet::new();
for _ in 0..max_stops {
let stop = read_linux_ptrace_stop(&mut reader)?;
if !stop_requested && stop_now.load(std::sync::atomic::Ordering::Relaxed) {
stop_requested = true;
request_sigstop_for_registered_tracees(&supervisor, &parked_tracees)?;
}
let host_pid = stop.host_pid();
if stop_requested {
request_sigstop_for_registered_tracees(&supervisor, &parked_tracees)?;
}
if matches!(
stop,
LinuxPtraceStop::Signal {
signal: libc::SIGSTOP,
..
}
) && supervisor
.registered_tracee_identities()
.iter()
.any(|identity| identity.host_pid == host_pid)
{
if !stop_requested {
resumer.resume_syscall_stop(host_pid)?;
continue;
}
parked_tracees.insert(host_pid);
let registered = supervisor.registered_tracee_identities();
if std::env::var_os("SUPERMACHINE_KBOXLIKE_TRACE_CAPTURE").is_some() {
eprintln!(
"KBOXLIVE_SIGSTOP_PARKED host_pid={} parked={} registered={}",
host_pid,
parked_tracees.len(),
registered.len()
);
}
if parked_tracees.len() >= min_tracees
&& registered
.iter()
.all(|identity| parked_tracees.contains(&identity.host_pid))
{
if std::env::var_os("SUPERMACHINE_KBOXLIKE_TRACE_CAPTURE").is_some() {
eprintln!("KBOXLIVE_SIGSTOP_CAPTURE tracees={}", registered.len());
}
let snapshot = capture_stopped_tracees(&supervisor, &mut reader)?;
if !snapshot
.threads
.iter()
.all(|thread| parked_tracees.contains(&thread.host_pid))
{
return Err(FdError::TraceeInstallFailed(
"snapshot included an unparked tracee",
));
}
return Ok(snapshot);
}
continue;
}
if matches!(
stop,
LinuxPtraceStop::Signal {
signal: libc::SIGSTOP,
..
}
) && !supervisor
.registered_tracee_identities()
.iter()
.any(|identity| identity.host_pid == host_pid)
{
premature_child_stops.insert(host_pid);
continue;
}
if let LinuxPtraceStop::Syscall {
host_pid: sys_host_pid,
} = stop
{
if io_uring_deny_exits.remove(&sys_host_pid) {
force_tracee_syscall_result(sys_host_pid, -(libc::ENOSYS as i64))?;
} else if let Ok(syscall) = reader.syscall_regs(sys_host_pid) {
if syscall.nr == LINUX_X86_64_SYS_IO_URING_SETUP {
deny_tracee_syscall_at_entry(sys_host_pid)?;
io_uring_deny_exits.insert(sys_host_pid);
if std::env::var_os("SUPERMACHINE_KBOXLIKE_TRACE_CAPTURE").is_some() {
eprintln!("KBOXLIVE_IO_URING_DENIED host_pid={sys_host_pid}");
}
}
if let Ok(Some(path)) = capture_exec_guest_path_from_syscall(
&supervisor,
&fds,
sys_host_pid,
syscall,
&mut reader,
) {
let _ = reader.record_exec_guest_path(sys_host_pid, path);
}
}
resumer.resume_syscall_stop(sys_host_pid)?;
continue;
}
if let LinuxPtraceStop::Signal { signal, .. } = stop {
if !supervisor
.registered_tracee_identities()
.iter()
.any(|identity| identity.host_pid == host_pid)
{
resumer.resume_signal_stop(host_pid, signal)?;
continue;
}
}
let fork_child_host_pid = match &stop {
LinuxPtraceStop::Fork { child_host_pid, .. } => Some(*child_host_pid),
_ => None,
};
let mut access = access_factory.access_for(host_pid)?;
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
stop,
std::process::id() as i32,
&mut reader,
&mut access.writer,
&mut access.executor,
&mut access.resumer,
)?;
if let Some(child) = fork_child_host_pid {
if premature_child_stops.remove(&child) {
resumer.resume_syscall_stop(child)?;
}
}
}
Err(FdError::TraceeInstallFailed(
"live exec tracees did not stop after SIGSTOP request",
))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const LINUX_X86_64_SYS_IO_URING_SETUP: i64 = 425;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn deny_tracee_syscall_at_entry(host_pid: i32) -> Result<(), FdError> {
let mut regs = raw_ptrace_getregs(host_pid)?;
regs.orig_rax = u64::MAX;
raw_ptrace_setregs(host_pid, ®s)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn force_tracee_syscall_result(host_pid: i32, result: i64) -> Result<(), FdError> {
let mut regs = raw_ptrace_getregs(host_pid)?;
regs.rax = result as u64;
raw_ptrace_setregs(host_pid, ®s)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn raw_ptrace_getregs(host_pid: i32) -> 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,
host_pid,
std::ptr::null_mut::<libc::c_void>(),
&mut regs as *mut _ as *mut libc::c_void,
)
};
if ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace getregs failed"));
}
Ok(regs)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn raw_ptrace_setregs(host_pid: i32, regs: &libc::user_regs_struct) -> Result<(), FdError> {
let ret = unsafe {
libc::ptrace(
libc::PTRACE_SETREGS,
host_pid,
std::ptr::null_mut::<libc::c_void>(),
regs as *const _ as *mut libc::c_void,
)
};
if ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace setregs failed"));
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn wait_for_traceme_sigstop(child: i32) -> Result<(), FdError> {
let status =
waitpid_specific_blocking_with(child, |status| unsafe { libc::waitpid(child, status, 0) })
.map_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(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn waitpid_any_tracee_blocking_with<F>(mut wait: F) -> Result<(i32, i32), FdError>
where
F: FnMut(*mut i32) -> i32,
{
loop {
let mut status = 0;
let waited = wait(&mut status);
if waited > 0 {
return Ok((waited, status));
}
if waited == 0 {
return Err(FdError::TraceeInstallFailed("waitpid returned no child"));
}
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",
}));
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn waitpid_specific_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"))]
fn request_sigstop_for_registered_tracees(
supervisor: &KboxlikeSyscallSupervisor,
parked_tracees: &BTreeSet<i32>,
) -> Result<(), FdError> {
for identity in supervisor.registered_tracee_identities() {
if parked_tracees.contains(&identity.host_pid) {
continue;
}
let ret = unsafe { libc::kill(identity.host_pid, libc::SIGSTOP) };
if ret < 0 {
return Err(FdError::TraceeInstallFailed(
"failed to request tracee SIGSTOP",
));
}
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn kill_and_reap_live_process_group(child: i32) {
unsafe {
let mut pids = live_process_group_pids(child);
if !pids.contains(&child) {
pids.push(child);
}
libc::kill(-child, libc::SIGKILL);
for pid in &pids {
libc::kill(*pid, libc::SIGKILL);
libc::ptrace(
libc::PTRACE_SYSCALL,
*pid,
std::ptr::null_mut::<libc::c_void>(),
libc::SIGKILL as usize as *mut libc::c_void,
);
}
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while std::time::Instant::now() <= deadline {
let mut status = 0;
let waited = libc::waitpid(-child, &mut status, libc::__WALL | libc::WNOHANG);
if waited == 0 {
std::thread::sleep(std::time::Duration::from_millis(1));
continue;
}
if waited < 0 {
break;
}
}
let root_deadline = std::time::Instant::now() + std::time::Duration::from_secs(1);
while std::time::Instant::now() <= root_deadline {
let mut status = 0;
let waited = libc::waitpid(child, &mut status, libc::__WALL | libc::WNOHANG);
if waited == 0 {
std::thread::sleep(std::time::Duration::from_millis(1));
} else {
break;
}
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn live_process_group_pids(pgid: i32) -> Vec<i32> {
let Ok(entries) = std::fs::read_dir("/proc") else {
return Vec::new();
};
let mut pids = Vec::new();
for entry in entries.flatten() {
let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
continue;
};
let Ok(pid) = name.parse::<i32>() else {
continue;
};
let Ok(stat) = std::fs::read_to_string(format!("/proc/{pid}/stat")) else {
continue;
};
let Some(after_comm) = stat.rsplit_once(") ") else {
continue;
};
let fields = after_comm.1.split_whitespace().collect::<Vec<_>>();
let Some(proc_pgrp) = fields.get(2).and_then(|value| value.parse::<i32>().ok()) else {
continue;
};
if proc_pgrp == pgid {
pids.push(pid);
}
}
pids
}
#[cfg(test)]
mod tests {
use super::super::dispatch::InterceptedSyscall;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use super::super::fd::ShadowBacking;
use super::super::fd::{FdOperation, FdRoute, ShadowKind, ShadowObject};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use super::super::restore::{
restore_stopped_kboxlike_snapshot_with_replacements, KboxlikeFdKernelReplayInputs,
KboxlikeTraceeRestoreInputs,
};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use super::super::shadow::{HostShadowRebuilder, KboxlikeFdKernelReplaySummary};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use super::super::snapshot::{
apply_fd_shadow_backing_plans, capture_dump_candidate_memory, capture_shared_object_memory,
capture_stopped_tracees, classify_memory_restore_actions,
materialize_memory_restore_actions, plan_epoll_shadow_backings,
plan_eventfd_shadow_backings, plan_fd_queued_data_replay, plan_fd_scm_rights_replay,
plan_fd_socket_options_replay, plan_fd_socket_shutdown_replay, plan_pipe_shadow_backings,
plan_socketpair_shadow_backings, plan_timerfd_shadow_backings,
KboxlikeByteBackedRestoreSource, KboxlikeFdTargetKind, KboxlikeMaterializedMemoryRestore,
KboxlikeMaterializedMemoryRestoreAction, KboxlikeMemoryDumpCapture,
KboxlikeMemoryRestoreAction, KboxlikeSocketIntOption, KboxlikeStoppedTraceeSnapshot,
KboxlikeUnixCredentials,
};
use super::super::tracee::TraceeSyscall;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use super::super::tracee::{
apply_stopped_tracee_restore_set, apply_thread_kernel_state_restore_for_tracee,
create_stopped_replacement_tracee_set,
remap_stopped_tracee_restore_set_to_replacement_pids,
remap_thread_kernel_states_to_replacement_pids, LinuxStoppedTraceeReplacementFactory,
LinuxTraceeRestoreAccessFactory, ProcessVmMemoryWriter, PtraceDetachRestoredTraceeResumer,
PtraceRegisterWriter, PtraceScratchSyscallExecutor, PtraceSyscallExecutor,
RestoredTraceeResumer, TraceeMemoryWriter, TraceeRegisterWriter, TraceeRestoreAccess,
TraceeRestoreAccessFactory,
};
use super::*;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use std::collections::BTreeSet;
use std::collections::VecDeque;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use std::os::fd::{AsRawFd, FromRawFd};
const LINUX_X86_64_SYS_MMAP: i64 = 9;
#[derive(Default)]
struct FakeTracee {
calls: Vec<TraceeSyscall>,
returns: VecDeque<i64>,
}
#[derive(Default)]
struct FakeMemory {
writes: Vec<(u64, Vec<u8>)>,
}
#[derive(Default)]
struct FakeResumer {
resumed: Vec<i32>,
signal_resumed: Vec<(i32, i32)>,
fail: bool,
}
#[derive(Default)]
struct FakeConfigurator {
calls: Vec<(i32, u64)>,
fail: bool,
}
#[derive(Default)]
struct FakeAccessFactory {
pids: Vec<i32>,
fail: bool,
}
#[derive(Default)]
struct FakeInspector {
waits: VecDeque<Result<(i32, i32), FdError>>,
event_msgs: VecDeque<Result<u64, FdError>>,
regs: VecDeque<Result<InterceptedSyscall, FdError>>,
return_values: VecDeque<Result<i64, FdError>>,
exec_paths: VecDeque<Result<String, FdError>>,
cstrings: VecDeque<Result<String, FdError>>,
fd_arrays: VecDeque<Result<Vec<i32>, FdError>>,
u64_arrays: VecDeque<Result<Vec<u64>, FdError>>,
event_msg_pids: Vec<i32>,
reg_pids: Vec<i32>,
return_pids: Vec<i32>,
exec_pids: Vec<i32>,
cstring_calls: Vec<(i32, u64, usize)>,
fd_array_calls: Vec<(i32, u64, usize)>,
u64_array_calls: Vec<(i32, u64, usize)>,
recorded_exec_paths: VecDeque<(i32, String)>,
}
#[derive(Default)]
struct FakeWaitReader {
waits: VecDeque<Result<(i32, i32), FdError>>,
event_msgs: VecDeque<Result<u64, FdError>>,
event_msg_pids: Vec<i32>,
}
#[derive(Default)]
struct FakeCStringReader {
reads: VecDeque<Result<String, FdError>>,
calls: Vec<(i32, u64, usize)>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const LIVE_SOURCE_THREAD_STACK_LEN: usize = 1024 * 1024;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const LIVE_SOURCE_ROBUST_LIST_LEN: u64 = 24;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[repr(C)]
struct LiveSourceThreadState {
leader_clear_tid: i32,
follower_clear_tid: i32,
leader_robust_list: [u64; 3],
follower_robust_list: [u64; 3],
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
static mut LIVE_SOURCE_THREAD_STATE: LiveSourceThreadState = LiveSourceThreadState {
leader_clear_tid: 0x1111_1111,
follower_clear_tid: 0x2222_2222,
leader_robust_list: [0; 3],
follower_robust_list: [0; 3],
};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[repr(C)]
struct LivePthreadSourceState {
follower_started: i32,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
static mut LIVE_PTHREAD_SOURCE_STATE: LivePthreadSourceState = LivePthreadSourceState {
follower_started: 0,
};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[repr(C)]
struct LivePthreadContinuationState {
source_marker: u8,
leader_fs_byte: u8,
follower_marker: u32,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
static mut LIVE_PTHREAD_CONTINUATION_STATE: LivePthreadContinuationState =
LivePthreadContinuationState {
source_marker: 0,
leader_fs_byte: 0,
follower_marker: 0,
};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn live_source_state_ptr() -> *mut LiveSourceThreadState {
std::ptr::addr_of_mut!(LIVE_SOURCE_THREAD_STATE)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
unsafe fn install_live_source_thread_state(
clear_tid: *mut i32,
robust_list: *mut [u64; 3],
) -> Result<(), FdError> {
let set_tid_ret = unsafe { libc::syscall(libc::SYS_set_tid_address, clear_tid) };
if set_tid_ret < 0 {
return Err(FdError::TraceeInstallFailed(
"live source set_tid_address failed",
));
}
let robust_ret = unsafe {
libc::syscall(
libc::SYS_set_robust_list,
robust_list as *mut libc::c_void,
LIVE_SOURCE_ROBUST_LIST_LEN as libc::size_t,
)
};
if robust_ret < 0 {
return Err(FdError::TraceeInstallFailed(
"live source set_robust_list failed",
));
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
extern "C" fn live_source_follower_main(arg: *mut libc::c_void) -> i32 {
unsafe {
let state = arg as *mut LiveSourceThreadState;
let _ = install_live_source_thread_state(
std::ptr::addr_of_mut!((*state).follower_clear_tid),
std::ptr::addr_of_mut!((*state).follower_robust_list),
);
loop {
libc::syscall(libc::SYS_pause);
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn live_source_thread_child_main() -> ! {
unsafe {
let trace_ret = libc::ptrace(
libc::PTRACE_TRACEME,
0,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_void>(),
);
if trace_ret < 0 {
libc::_exit(111);
}
libc::raise(libc::SIGSTOP);
let state = live_source_state_ptr();
if install_live_source_thread_state(
std::ptr::addr_of_mut!((*state).leader_clear_tid),
std::ptr::addr_of_mut!((*state).leader_robust_list),
)
.is_err()
{
libc::_exit(112);
}
let stack = libc::mmap(
std::ptr::null_mut(),
LIVE_SOURCE_THREAD_STACK_LEN,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
-1,
0,
);
if stack == libc::MAP_FAILED {
libc::_exit(113);
}
let stack_top =
(stack as *mut u8).add(LIVE_SOURCE_THREAD_STACK_LEN) as *mut libc::c_void;
let flags = libc::CLONE_VM
| libc::CLONE_FS
| libc::CLONE_FILES
| libc::CLONE_SIGHAND
| libc::CLONE_THREAD
| libc::CLONE_SYSVSEM
| libc::CLONE_CHILD_CLEARTID
| libc::CLONE_CHILD_SETTID;
let tid = libc::clone(
live_source_follower_main,
stack_top,
flags,
state as *mut libc::c_void,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_void>(),
std::ptr::addr_of_mut!((*state).follower_clear_tid),
);
if tid < 0 {
libc::_exit(114);
}
loop {
libc::syscall(libc::SYS_pause);
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
extern "C" fn live_pthread_source_follower_main(arg: *mut libc::c_void) -> *mut libc::c_void {
unsafe {
let state = arg as *mut LivePthreadSourceState;
(*state).follower_started = 1;
loop {
libc::syscall(libc::SYS_pause);
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[inline(never)]
extern "C" fn live_pthread_source_leader_continue(
pipe_fd: libc::c_int,
state: *mut LivePthreadContinuationState,
) -> ! {
unsafe {
let fs_byte: u8;
std::arch::asm!(
"mov {0}, byte ptr fs:[0]",
out(reg_byte) fs_byte,
options(nostack, preserves_flags)
);
(*state).leader_fs_byte = fs_byte;
let bytes = [(*state).source_marker, fs_byte];
libc::write(pipe_fd, bytes.as_ptr() as *const libc::c_void, bytes.len());
loop {
libc::syscall(libc::SYS_pause);
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[inline(never)]
extern "C" fn live_pthread_source_follower_exit(state: *mut LivePthreadContinuationState) -> ! {
unsafe {
(*state).follower_marker = 0xfeed_cafe;
libc::syscall(libc::SYS_exit, 0);
libc::_exit(123);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn live_pthread_source_child_main() -> ! {
unsafe {
let trace_ret = libc::ptrace(
libc::PTRACE_TRACEME,
0,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_void>(),
);
if trace_ret < 0 {
libc::_exit(121);
}
libc::raise(libc::SIGSTOP);
LIVE_PTHREAD_CONTINUATION_STATE.source_marker = 0x5a;
LIVE_PTHREAD_CONTINUATION_STATE.leader_fs_byte = 0;
LIVE_PTHREAD_CONTINUATION_STATE.follower_marker = 0;
let mut thread: libc::pthread_t = std::mem::zeroed();
let ret = libc::pthread_create(
&mut thread as *mut libc::pthread_t,
std::ptr::null(),
live_pthread_source_follower_main,
std::ptr::addr_of_mut!(LIVE_PTHREAD_SOURCE_STATE) as *mut libc::c_void,
);
if ret != 0 {
libc::_exit(122);
}
loop {
libc::syscall(libc::SYS_pause);
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn send_fd_with_payload(socket_fd: i32, passed_fd: i32, payload: &[u8]) -> bool {
if payload.is_empty() {
return false;
}
let mut payload = payload.to_vec();
let mut iov = libc::iovec {
iov_base: payload.as_mut_ptr() as *mut libc::c_void,
iov_len: payload.len(),
};
let mut control =
vec![
0u8;
unsafe { libc::CMSG_SPACE(std::mem::size_of::<libc::c_int>() as u32) as usize }
];
let msg = libc::msghdr {
msg_name: std::ptr::null_mut(),
msg_namelen: 0,
msg_iov: &mut iov,
msg_iovlen: 1,
msg_control: control.as_mut_ptr() as *mut libc::c_void,
msg_controllen: control.len(),
msg_flags: 0,
};
let cmsg = unsafe { libc::CMSG_FIRSTHDR(&msg) };
if cmsg.is_null() {
return false;
}
unsafe {
(*cmsg).cmsg_level = libc::SOL_SOCKET;
(*cmsg).cmsg_type = libc::SCM_RIGHTS;
(*cmsg).cmsg_len =
libc::CMSG_LEN(std::mem::size_of::<libc::c_int>() as u32) as libc::size_t;
(libc::CMSG_DATA(cmsg) as *mut libc::c_int).write_unaligned(passed_fd);
libc::sendmsg(socket_fd, &msg, libc::MSG_NOSIGNAL) == payload.len() as isize
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn live_kernel_object_source_child_main() -> ! {
unsafe {
let trace_ret = libc::ptrace(
libc::PTRACE_TRACEME,
0,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_void>(),
);
if trace_ret < 0 {
libc::_exit(131);
}
libc::raise(libc::SIGSTOP);
let mut pipe_fds = [-1; 2];
if libc::pipe2(pipe_fds.as_mut_ptr(), libc::O_CLOEXEC) < 0 {
libc::_exit(132);
}
let duplicate_pipe_read = libc::dup(pipe_fds[0]);
if duplicate_pipe_read < 0 {
libc::_exit(141);
}
let fd_flags = libc::fcntl(duplicate_pipe_read, libc::F_GETFD);
if fd_flags < 0
|| libc::fcntl(
duplicate_pipe_read,
libc::F_SETFD,
fd_flags | libc::FD_CLOEXEC,
) < 0
{
libc::_exit(142);
}
let mut socket_fds = [-1; 2];
if libc::socketpair(
libc::AF_UNIX,
libc::SOCK_STREAM | libc::SOCK_CLOEXEC,
0,
socket_fds.as_mut_ptr(),
) < 0
{
libc::_exit(133);
}
let passcred = 1 as libc::c_int;
for socket_fd in socket_fds {
if libc::setsockopt(
socket_fd,
libc::SOL_SOCKET,
libc::SO_PASSCRED,
&passcred as *const libc::c_int as *const libc::c_void,
std::mem::size_of::<libc::c_int>() as libc::socklen_t,
) < 0
{
libc::_exit(148);
}
}
let event_fd = libc::eventfd(7, libc::EFD_CLOEXEC | libc::EFD_NONBLOCK);
if event_fd < 0 {
libc::_exit(134);
}
let pipe_msg = b"pipeq";
if libc::write(
pipe_fds[1],
pipe_msg.as_ptr() as *const libc::c_void,
pipe_msg.len(),
) != pipe_msg.len() as isize
{
libc::_exit(146);
}
let socket_msg = b"sockq";
if libc::write(
socket_fds[0],
socket_msg.as_ptr() as *const libc::c_void,
socket_msg.len(),
) != socket_msg.len() as isize
{
libc::_exit(147);
}
if libc::shutdown(socket_fds[0], libc::SHUT_WR) < 0 {
libc::_exit(150);
}
if !send_fd_with_payload(socket_fds[1], event_fd, b"r") {
libc::_exit(149);
}
let epoll_fd = libc::epoll_create1(libc::EPOLL_CLOEXEC);
if epoll_fd < 0 {
libc::_exit(135);
}
let mut event = libc::epoll_event {
events: libc::EPOLLIN as u32,
u64: event_fd as u64,
};
if libc::epoll_ctl(epoll_fd, libc::EPOLL_CTL_ADD, event_fd, &mut event) < 0 {
libc::_exit(136);
}
let mut pipe_event = libc::epoll_event {
events: libc::EPOLLIN as u32,
u64: duplicate_pipe_read as u64,
};
if libc::epoll_ctl(
epoll_fd,
libc::EPOLL_CTL_ADD,
duplicate_pipe_read,
&mut pipe_event,
) < 0
{
libc::_exit(143);
}
let timer_fd = libc::timerfd_create(
libc::CLOCK_MONOTONIC,
libc::TFD_CLOEXEC | libc::TFD_NONBLOCK,
);
if timer_fd < 0 {
libc::_exit(137);
}
let timer_spec = libc::itimerspec {
it_interval: libc::timespec {
tv_sec: 2,
tv_nsec: 123_456_789,
},
it_value: libc::timespec {
tv_sec: 60,
tv_nsec: 987_654_321,
},
};
if libc::timerfd_settime(timer_fd, 0, &timer_spec, std::ptr::null_mut()) < 0 {
libc::_exit(144);
}
let mut timer_event = libc::epoll_event {
events: libc::EPOLLIN as u32,
u64: timer_fd as u64,
};
if libc::epoll_ctl(epoll_fd, libc::EPOLL_CTL_ADD, timer_fd, &mut timer_event) < 0 {
libc::_exit(145);
}
let name = std::ffi::CString::new("kboxlike-kernel-objects").unwrap();
let memfd = libc::memfd_create(name.as_ptr(), libc::MFD_CLOEXEC);
if memfd < 0 {
libc::_exit(138);
}
if libc::ftruncate(memfd, 4096) < 0 {
libc::_exit(139);
}
let mapped = libc::mmap(
std::ptr::null_mut(),
4096,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_SHARED,
memfd,
0,
);
if mapped == libc::MAP_FAILED {
libc::_exit(140);
}
*(mapped as *mut u8) = 0x4b;
loop {
libc::syscall(libc::SYS_pause);
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn capture_live_source_snapshot_at_pause(
child_main: fn() -> !,
expected_paused_tracees: usize,
) -> Result<(i32, KboxlikeStoppedTraceeSnapshot), FdError> {
const MAX_STOPS: usize = 4096;
let child = unsafe { libc::fork() };
if child < 0 {
return Err(FdError::TraceeInstallFailed("fork failed"));
}
if child == 0 {
child_main();
}
let result = (|| {
wait_for_traceme_sigstop(child)?;
let mut fds = KboxlikeFdSystem::new();
let mut supervisor = KboxlikeSyscallSupervisor::new();
let mut reader = LinuxPtraceReader::default();
let mut resumer = PtraceSyscallStopResumer;
configure_initial_linux_ptrace_tracee(
&mut supervisor,
&mut fds,
child,
"/proc/self/exe",
&mut reader,
&mut resumer,
)?;
let mut access_factory = LinuxPtraceTraceeAccessFactory;
let mut paused_tracees = BTreeSet::new();
for _ in 0..MAX_STOPS {
let stop = read_linux_ptrace_stop(&mut reader)?;
let host_pid = stop.host_pid();
if let LinuxPtraceStop::Syscall { host_pid } = stop {
let phase = supervisor.next_syscall_stop_phase(host_pid)?;
let syscall = reader.syscall_regs(host_pid)?;
if matches!(phase, SyscallStopPhase::Entry)
&& syscall.nr == libc::SYS_pause as i64
&& supervisor
.registered_tracee_identities()
.iter()
.any(|identity| identity.host_pid == host_pid)
{
paused_tracees.insert(host_pid);
if paused_tracees.len() == expected_paused_tracees {
break;
}
continue;
}
}
let mut access = access_factory.access_for(host_pid)?;
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
stop,
std::process::id() as i32,
&mut reader,
&mut access.writer,
&mut access.executor,
&mut access.resumer,
)?;
}
if paused_tracees.len() != expected_paused_tracees {
return Err(FdError::TraceeInstallFailed(
"source tracees did not stop at pause syscall entry",
));
}
let snapshot = capture_stopped_tracees(&supervisor, &mut reader)?;
if snapshot.threads.len() != expected_paused_tracees
|| snapshot.thread_kernel_states.len() != expected_paused_tracees
{
return Err(FdError::TraceeInstallFailed(
"live source snapshot did not capture expected threads",
));
}
if !snapshot
.threads
.iter()
.all(|thread| paused_tracees.contains(&thread.host_pid))
{
return Err(FdError::TraceeInstallFailed(
"snapshot included a tracee not parked at pause",
));
}
Ok((child, snapshot))
})();
if result.is_err() {
kill_and_reap_tracee(child);
}
result
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn capture_live_source_thread_snapshot(
child_main: fn() -> !,
) -> Result<(i32, KboxlikeStoppedTraceeSnapshot), FdError> {
capture_live_source_snapshot_at_pause(child_main, 2)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn capture_live_source_clone_thread_snapshot(
) -> Result<(i32, KboxlikeStoppedTraceeSnapshot), FdError> {
capture_live_source_thread_snapshot(live_source_thread_child_main)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn capture_live_source_pthread_snapshot(
) -> Result<(i32, KboxlikeStoppedTraceeSnapshot), FdError> {
capture_live_source_thread_snapshot(live_pthread_source_child_main)
}
impl FakeTracee {
fn with_returns(returns: impl IntoIterator<Item = i64>) -> Self {
Self {
calls: Vec::new(),
returns: returns.into_iter().collect(),
}
}
}
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))
}
}
impl LinuxPtraceConfigurator for FakeConfigurator {
fn set_trace_options(&mut self, host_pid: i32, options: u64) -> Result<(), FdError> {
if self.fail {
return Err(FdError::TraceeInstallFailed("ptrace setoptions failed"));
}
self.calls.push((host_pid, options));
Ok(())
}
}
impl TraceeMemoryWriter for FakeMemory {
fn write_bytes(&mut self, addr: u64, bytes: &[u8]) -> Result<(), FdError> {
self.writes.push((addr, bytes.to_vec()));
Ok(())
}
}
impl SyscallStopResumer for FakeResumer {
fn resume_syscall_stop(&mut self, host_pid: i32) -> Result<(), FdError> {
if self.fail {
return Err(FdError::TraceeInstallFailed("resume failed"));
}
self.resumed.push(host_pid);
Ok(())
}
fn resume_signal_stop(&mut self, host_pid: i32, signal: i32) -> Result<(), FdError> {
if self.fail {
return Err(FdError::TraceeInstallFailed("resume failed"));
}
self.signal_resumed.push((host_pid, signal));
Ok(())
}
}
impl TraceeAccessFactory for FakeAccessFactory {
type Writer = FakeMemory;
type Executor = FakeTracee;
type Resumer = FakeResumer;
fn access_for(
&mut self,
host_pid: i32,
) -> Result<TraceeStopAccess<Self::Writer, Self::Executor, Self::Resumer>, FdError>
{
if self.fail {
return Err(FdError::TraceeInstallFailed("tracee access failed"));
}
self.pids.push(host_pid);
Ok(TraceeStopAccess {
writer: FakeMemory::default(),
executor: FakeTracee::default(),
resumer: FakeResumer::default(),
})
}
}
impl LinuxPtraceWaitReader for FakeInspector {
fn wait_next(&mut self) -> Result<(i32, i32), FdError> {
self.waits
.pop_front()
.unwrap_or(Err(FdError::TraceeInstallFailed("missing wait status")))
}
fn event_msg(&mut self, host_pid: i32) -> Result<u64, FdError> {
self.event_msg_pids.push(host_pid);
self.event_msgs
.pop_front()
.unwrap_or(Err(FdError::TraceeInstallFailed(
"missing ptrace event msg",
)))
}
}
impl LinuxPtraceStopInspector for FakeInspector {
fn syscall_regs(&mut self, host_pid: i32) -> Result<InterceptedSyscall, FdError> {
self.reg_pids.push(host_pid);
self.regs
.pop_front()
.unwrap_or_else(|| Ok(InterceptedSyscall::new(9999, [0; 6])))
}
fn syscall_return_value(&mut self, host_pid: i32) -> Result<i64, FdError> {
self.return_pids.push(host_pid);
self.return_values.pop_front().unwrap_or(Ok(0))
}
fn exec_guest_path(&mut self, host_pid: i32) -> Result<String, FdError> {
self.exec_pids.push(host_pid);
if let Some(path) = self.exec_paths.pop_front() {
return path;
}
let Some(index) = self
.recorded_exec_paths
.iter()
.position(|(recorded_pid, _)| *recorded_pid == host_pid)
else {
return Err(FdError::TraceeInstallFailed("missing exec guest path"));
};
let (_, guest_exe) = self
.recorded_exec_paths
.remove(index)
.expect("recorded exec path index exists");
Ok(guest_exe)
}
}
impl TraceeCStringReader for FakeInspector {
fn read_c_string(
&mut self,
host_pid: i32,
addr: u64,
max_len: usize,
) -> Result<String, FdError> {
self.cstring_calls.push((host_pid, addr, max_len));
self.cstrings
.pop_front()
.unwrap_or(Err(FdError::TraceeInstallFailed("missing tracee cstring")))
}
}
impl TraceeFdArrayReader for FakeInspector {
fn read_fd_array(
&mut self,
host_pid: i32,
addr: u64,
len: usize,
) -> Result<Vec<i32>, FdError> {
self.fd_array_calls.push((host_pid, addr, len));
self.fd_arrays
.pop_front()
.unwrap_or(Err(FdError::TraceeInstallFailed("missing tracee fd array")))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn read_u64_array(
&mut self,
host_pid: i32,
addr: u64,
len: usize,
) -> Result<Vec<u64>, FdError> {
self.u64_array_calls.push((host_pid, addr, len));
self.u64_arrays
.pop_front()
.unwrap_or(Err(FdError::TraceeInstallFailed(
"missing tracee u64 array",
)))
}
}
impl ExecGuestPathRecorder for FakeInspector {
fn record_exec_guest_path(
&mut self,
host_pid: i32,
guest_exe: String,
) -> Result<(), FdError> {
if host_pid <= 0 {
return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
}
self.recorded_exec_paths.push_back((host_pid, guest_exe));
Ok(())
}
}
impl LinuxPtraceWaitReader for FakeWaitReader {
fn wait_next(&mut self) -> Result<(i32, i32), FdError> {
self.waits
.pop_front()
.unwrap_or(Err(FdError::TraceeInstallFailed("missing wait status")))
}
fn event_msg(&mut self, host_pid: i32) -> Result<u64, FdError> {
self.event_msg_pids.push(host_pid);
self.event_msgs
.pop_front()
.unwrap_or(Err(FdError::TraceeInstallFailed(
"missing ptrace event msg",
)))
}
}
impl TraceeCStringReader for FakeCStringReader {
fn read_c_string(
&mut self,
host_pid: i32,
addr: u64,
max_len: usize,
) -> Result<String, FdError> {
self.calls.push((host_pid, addr, max_len));
self.reads
.pop_front()
.unwrap_or(Err(FdError::TraceeInstallFailed("missing tracee cstring")))
}
}
fn mmap_syscall(fd: i32) -> InterceptedSyscall {
InterceptedSyscall::new(LINUX_X86_64_SYS_MMAP, [0, 0, 0, 0, fd as u64, 0])
}
fn execve_syscall(path_addr: u64) -> InterceptedSyscall {
InterceptedSyscall::new(LINUX_X86_64_SYS_EXECVE, [path_addr, 0, 0, 0, 0, 0])
}
fn execveat_syscall(path_addr: u64) -> InterceptedSyscall {
InterceptedSyscall::new(LINUX_X86_64_SYS_EXECVEAT, [0, path_addr, 0, 0, 0, 0])
}
fn openat_syscall(flags: u64) -> InterceptedSyscall {
InterceptedSyscall::new(LINUX_X86_64_SYS_OPENAT, [0, 0, flags, 0, 0, 0])
}
fn open_syscall(flags: u64) -> InterceptedSyscall {
InterceptedSyscall::new(LINUX_X86_64_SYS_OPEN, [0, flags, 0, 0, 0, 0])
}
fn openat2_syscall() -> InterceptedSyscall {
InterceptedSyscall::new(LINUX_X86_64_SYS_OPENAT2, [0, 0, 0, 0, 0, 0])
}
fn close_syscall(fd: i32) -> InterceptedSyscall {
InterceptedSyscall::new(LINUX_X86_64_SYS_CLOSE, [fd as u64, 0, 0, 0, 0, 0])
}
fn pipe_syscall(addr: u64) -> InterceptedSyscall {
InterceptedSyscall::new(LINUX_X86_64_SYS_PIPE, [addr, 0, 0, 0, 0, 0])
}
fn pipe2_syscall(addr: u64, flags: u64) -> InterceptedSyscall {
InterceptedSyscall::new(LINUX_X86_64_SYS_PIPE2, [addr, flags, 0, 0, 0, 0])
}
fn socket_syscall(kind: u64) -> InterceptedSyscall {
InterceptedSyscall::new(LINUX_X86_64_SYS_SOCKET, [0, kind, 0, 0, 0, 0])
}
fn socketpair_syscall(kind: u64, addr: u64) -> InterceptedSyscall {
InterceptedSyscall::new(LINUX_X86_64_SYS_SOCKETPAIR, [0, kind, 0, addr, 0, 0])
}
fn shutdown_syscall(fd: i32, how: i32) -> InterceptedSyscall {
InterceptedSyscall::new(
LINUX_X86_64_SYS_SHUTDOWN,
[fd as u64, how as u64, 0, 0, 0, 0],
)
}
fn accept4_syscall(flags: u64) -> InterceptedSyscall {
InterceptedSyscall::new(LINUX_X86_64_SYS_ACCEPT4, [0, 0, 0, flags, 0, 0])
}
fn eventfd2_syscall(flags: u64) -> InterceptedSyscall {
InterceptedSyscall::new(LINUX_X86_64_SYS_EVENTFD2, [0, flags, 0, 0, 0, 0])
}
fn memfd_create_syscall(flags: u64) -> InterceptedSyscall {
InterceptedSyscall::new(LINUX_X86_64_SYS_MEMFD_CREATE, [0, flags, 0, 0, 0, 0])
}
fn epoll_create1_syscall(flags: u64) -> InterceptedSyscall {
InterceptedSyscall::new(LINUX_X86_64_SYS_EPOLL_CREATE1, [flags, 0, 0, 0, 0, 0])
}
fn dup_syscall(fd: i32) -> InterceptedSyscall {
InterceptedSyscall::new(LINUX_X86_64_SYS_DUP, [fd as u64, 0, 0, 0, 0, 0])
}
fn dup3_syscall(oldfd: i32, newfd: i32, flags: u64) -> InterceptedSyscall {
InterceptedSyscall::new(
LINUX_X86_64_SYS_DUP3,
[oldfd as u64, newfd as u64, flags, 0, 0, 0],
)
}
fn fcntl_syscall(fd: i32, command: u64, arg: u64) -> InterceptedSyscall {
InterceptedSyscall::new(LINUX_X86_64_SYS_FCNTL, [fd as u64, command, arg, 0, 0, 0])
}
fn set_tid_address_syscall(addr: u64) -> InterceptedSyscall {
InterceptedSyscall::new(LINUX_X86_64_SYS_SET_TID_ADDRESS, [addr, 0, 0, 0, 0, 0])
}
fn set_robust_list_syscall(head: u64, len: u64) -> InterceptedSyscall {
InterceptedSyscall::new(LINUX_X86_64_SYS_SET_ROBUST_LIST, [head, len, 0, 0, 0, 0])
}
fn clone_syscall(flags: u64, child_tid: u64) -> InterceptedSyscall {
InterceptedSyscall::new(LINUX_X86_64_SYS_CLONE, [flags, 0, 0, child_tid, 0, 0])
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn clone3_syscall(args_addr: u64, args_size: u64) -> InterceptedSyscall {
InterceptedSyscall::new(LINUX_X86_64_SYS_CLONE3, [args_addr, args_size, 0, 0, 0, 0])
}
fn stopped_status(signal: i32) -> i32 {
LINUX_WAIT_STOPPED_LOW | (signal << 8)
}
fn ptrace_event_status(event: i32) -> i32 {
stopped_status(libc::SIGTRAP) | (event << LINUX_PTRACE_EVENT_SHIFT)
}
fn exited_status(code: i32) -> i32 {
code << 8
}
fn signaled_status(signal: i32) -> i32 {
signal
}
#[test]
fn configure_initial_tracee_sets_options_registers_model_and_resumes() {
let mut fds = KboxlikeFdSystem::new();
let mut supervisor = KboxlikeSyscallSupervisor::new();
let mut configurator = FakeConfigurator::default();
let mut resumer = FakeResumer::default();
let model_pid = configure_initial_linux_ptrace_tracee(
&mut supervisor,
&mut fds,
4321,
"/ms-playwright/chromium/chrome",
&mut configurator,
&mut resumer,
)
.unwrap();
assert_eq!(configurator.calls, vec![(4321, LINUX_PTRACE_TRACE_OPTIONS)]);
assert_eq!(supervisor.model_pid_for_tracee(4321).unwrap(), model_pid);
assert_eq!(
fds.current_exe(model_pid).unwrap(),
Some("/ms-playwright/chromium/chrome")
);
for fd in 0..=2 {
assert_eq!(
fds.route_fd_operation(model_pid, fd, FdOperation::Generic)
.unwrap(),
FdRoute::Lkl { fd: fd as i64 }
);
}
assert_eq!(resumer.resumed, vec![4321]);
}
#[test]
fn configure_initial_tracee_rolls_back_model_on_resume_error() {
let mut fds = KboxlikeFdSystem::new();
let mut supervisor = KboxlikeSyscallSupervisor::new();
let mut configurator = FakeConfigurator::default();
let mut resumer = FakeResumer {
fail: true,
..FakeResumer::default()
};
assert_eq!(
configure_initial_linux_ptrace_tracee(
&mut supervisor,
&mut fds,
4321,
"/bin/sh",
&mut configurator,
&mut resumer,
)
.unwrap_err(),
FdError::TraceeInstallFailed("resume failed")
);
assert_eq!(configurator.calls, vec![(4321, LINUX_PTRACE_TRACE_OPTIONS)]);
assert_eq!(
supervisor.model_pid_for_tracee(4321).unwrap_err(),
FdError::TraceeInstallFailed("unknown tracee pid")
);
assert_eq!(fds.current_exe(1), Err(FdError::BadProcess(1)));
}
#[test]
fn ptrace_dispatch_loop_runs_until_terminal_process_status() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init_with_exe("/usr/local/bin/node");
fds.insert_plain_fd(model_pid, 104, Some(204), false)
.unwrap();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
let mut transport = FakeInspector {
waits: VecDeque::from([
Ok((4321, stopped_status(LINUX_WAIT_SYSCALL_STOP_SIGNAL))),
Ok((4321, exited_status(0))),
]),
regs: VecDeque::from([Ok(mmap_syscall(104))]),
..FakeInspector::default()
};
let mut access = FakeAccessFactory::default();
assert_eq!(
run_linux_ptrace_dispatch_loop(
&mut supervisor,
&mut fds,
1234,
&mut transport,
&mut access,
8,
)
.unwrap(),
LinuxPtraceLoopSummary {
dispatched: 2,
terminal: Some(LinuxPtraceDispatchOutcome::ProcessExited {
host_pid: 4321,
code: 0,
}),
}
);
assert_eq!(transport.reg_pids, vec![4321]);
assert_eq!(access.pids, vec![4321, 4321]);
assert_eq!(
supervisor.model_pid_for_tracee(4321).unwrap_err(),
FdError::TraceeInstallFailed("unknown tracee pid")
);
}
#[test]
fn ptrace_dispatch_loop_rejects_zero_stop_bound() {
let mut fds = KboxlikeFdSystem::new();
let mut supervisor = KboxlikeSyscallSupervisor::new();
let mut transport = FakeInspector::default();
let mut access = FakeAccessFactory::default();
assert_eq!(
run_linux_ptrace_dispatch_loop(
&mut supervisor,
&mut fds,
1234,
&mut transport,
&mut access,
0,
)
.unwrap_err(),
FdError::TraceeInstallFailed("ptrace dispatch loop max stops is zero")
);
assert!(access.pids.is_empty());
}
#[test]
fn syscall_exit_tracks_successful_openat_returned_fd() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
let mut inspector = FakeInspector {
regs: VecDeque::from([
Ok(openat_syscall(LINUX_O_CLOEXEC)),
Ok(openat_syscall(LINUX_O_CLOEXEC)),
]),
return_values: VecDeque::from([Ok(5)]),
..FakeInspector::default()
};
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::default();
let mut resumer = FakeResumer::default();
assert_eq!(
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
LinuxPtraceStop::Syscall { host_pid: 4321 },
1234,
&mut inspector,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap(),
LinuxPtraceDispatchOutcome::Syscall(SupervisedSyscallStopOutcome::Entry(
SyscallStopOutcome::Unclassified
))
);
assert_eq!(
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
LinuxPtraceStop::Syscall { host_pid: 4321 },
1234,
&mut inspector,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap(),
LinuxPtraceDispatchOutcome::Syscall(SupervisedSyscallStopOutcome::Exit)
);
let info = fds.descriptor(model_pid, 5).unwrap();
assert_eq!(info.lkl_fd, Some(5));
assert!(info.cloexec);
assert_eq!(inspector.return_pids, vec![4321]);
assert_eq!(resumer.resumed, vec![4321, 4321]);
}
#[test]
fn syscall_exit_tracks_close_and_ignores_failed_openat() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init();
fds.insert_plain_fd(model_pid, 5, Some(5), false).unwrap();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
let mut inspector = FakeInspector {
regs: VecDeque::from([
Ok(openat_syscall(0)),
Ok(openat_syscall(0)),
Ok(close_syscall(5)),
Ok(close_syscall(5)),
]),
return_values: VecDeque::from([Ok(-2), Ok(0)]),
..FakeInspector::default()
};
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::default();
let mut resumer = FakeResumer::default();
for _ in 0..4 {
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
LinuxPtraceStop::Syscall { host_pid: 4321 },
1234,
&mut inspector,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap();
}
assert_eq!(fds.descriptor(model_pid, 5), Err(FdError::BadFd(5)));
assert_eq!(fds.descriptor(model_pid, 3), Err(FdError::BadFd(3)));
}
#[test]
fn syscall_exit_tracks_dup_variants_and_cloexec() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init();
fds.insert_shadow_fd(
model_pid,
10,
Some(10),
ShadowObject {
kind: ShadowKind::LocalOnly,
supervisor_fd: 710,
},
false,
)
.unwrap();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
let mut inspector = FakeInspector {
regs: VecDeque::from([
Ok(dup_syscall(10)),
Ok(dup_syscall(10)),
Ok(dup3_syscall(10, 12, LINUX_O_CLOEXEC)),
Ok(dup3_syscall(10, 12, LINUX_O_CLOEXEC)),
Ok(fcntl_syscall(10, LINUX_F_DUPFD_CLOEXEC, 20)),
Ok(fcntl_syscall(10, LINUX_F_DUPFD_CLOEXEC, 20)),
Ok(fcntl_syscall(11, LINUX_F_SETFD, LINUX_FD_CLOEXEC)),
Ok(fcntl_syscall(11, LINUX_F_SETFD, LINUX_FD_CLOEXEC)),
]),
return_values: VecDeque::from([Ok(11), Ok(12), Ok(21), Ok(0)]),
..FakeInspector::default()
};
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::default();
let mut resumer = FakeResumer::default();
for _ in 0..8 {
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
LinuxPtraceStop::Syscall { host_pid: 4321 },
1234,
&mut inspector,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap();
}
let source = fds.descriptor(model_pid, 10).unwrap();
let dup = fds.descriptor(model_pid, 11).unwrap();
let dup3 = fds.descriptor(model_pid, 12).unwrap();
let fcntl_dup = fds.descriptor(model_pid, 21).unwrap();
assert_eq!(dup.open_file, source.open_file);
assert_eq!(dup3.open_file, source.open_file);
assert_eq!(fcntl_dup.open_file, source.open_file);
assert!(dup.cloexec);
assert!(dup3.cloexec);
assert!(fcntl_dup.cloexec);
}
#[test]
fn syscall_exit_tracks_socket_shutdown_state() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init();
fds.insert_plain_fd(model_pid, 10, Some(10), false).unwrap();
fds.dup_to(model_pid, 10, 11, false).unwrap();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
let mut inspector = FakeInspector {
regs: VecDeque::from([
Ok(shutdown_syscall(10, libc::SHUT_WR)),
Ok(shutdown_syscall(10, libc::SHUT_WR)),
Ok(shutdown_syscall(11, libc::SHUT_RD)),
Ok(shutdown_syscall(11, libc::SHUT_RD)),
]),
return_values: VecDeque::from([Ok(0), Ok(0)]),
..FakeInspector::default()
};
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::default();
let mut resumer = FakeResumer::default();
for _ in 0..4 {
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
LinuxPtraceStop::Syscall { host_pid: 4321 },
1234,
&mut inspector,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap();
}
let shutdown = fds.socket_shutdown(model_pid, 10).unwrap().unwrap();
assert!(shutdown.read_closed);
assert!(shutdown.write_closed);
assert_eq!(fds.socket_shutdown(model_pid, 11).unwrap(), Some(shutdown));
}
#[test]
fn syscall_exit_tracks_common_return_fd_syscalls() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
let syscalls = [
open_syscall(LINUX_O_CLOEXEC),
openat2_syscall(),
socket_syscall(LINUX_SOCK_CLOEXEC),
accept4_syscall(LINUX_SOCK_CLOEXEC),
eventfd2_syscall(LINUX_O_CLOEXEC),
memfd_create_syscall(LINUX_O_CLOEXEC),
epoll_create1_syscall(LINUX_O_CLOEXEC),
];
let mut regs = VecDeque::new();
for syscall in syscalls.iter().copied() {
regs.push_back(Ok(syscall));
regs.push_back(Ok(syscall));
}
let mut inspector = FakeInspector {
regs,
return_values: VecDeque::from([Ok(40), Ok(41), Ok(42), Ok(43), Ok(44), Ok(45), Ok(46)]),
..FakeInspector::default()
};
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::default();
let mut resumer = FakeResumer::default();
for _ in 0..(syscalls.len() * 2) {
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
LinuxPtraceStop::Syscall { host_pid: 4321 },
1234,
&mut inspector,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap();
}
for fd in 40..=46 {
assert_eq!(
fds.descriptor(model_pid, fd).unwrap().lkl_fd,
Some(fd as i64)
);
}
assert!(fds.descriptor(model_pid, 40).unwrap().cloexec);
assert!(!fds.descriptor(model_pid, 41).unwrap().cloexec);
for fd in 42..=46 {
assert!(fds.descriptor(model_pid, fd).unwrap().cloexec);
}
assert!(inspector.fd_array_calls.is_empty());
}
#[test]
fn syscall_exit_tracks_fd_array_syscalls() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
let syscalls = [
pipe_syscall(0x1000),
pipe2_syscall(0x2000, LINUX_O_CLOEXEC),
socketpair_syscall(LINUX_SOCK_CLOEXEC, 0x3000),
];
let mut regs = VecDeque::new();
for syscall in syscalls.iter().copied() {
regs.push_back(Ok(syscall));
regs.push_back(Ok(syscall));
}
let mut inspector = FakeInspector {
regs,
return_values: VecDeque::from([Ok(0), Ok(0), Ok(0)]),
fd_arrays: VecDeque::from([Ok(vec![50, 51]), Ok(vec![52, 53]), Ok(vec![54, 55])]),
..FakeInspector::default()
};
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::default();
let mut resumer = FakeResumer::default();
for _ in 0..(syscalls.len() * 2) {
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
LinuxPtraceStop::Syscall { host_pid: 4321 },
1234,
&mut inspector,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap();
}
assert_eq!(
inspector.fd_array_calls,
vec![(4321, 0x1000, 2), (4321, 0x2000, 2), (4321, 0x3000, 2)]
);
for fd in 50..=55 {
assert_eq!(
fds.descriptor(model_pid, fd).unwrap().lkl_fd,
Some(fd as i64)
);
}
assert!(!fds.descriptor(model_pid, 50).unwrap().cloexec);
assert!(!fds.descriptor(model_pid, 51).unwrap().cloexec);
for fd in 52..=55 {
assert!(fds.descriptor(model_pid, fd).unwrap().cloexec);
}
}
#[test]
fn syscall_exit_does_not_read_fd_array_after_failed_pipe() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
let mut inspector = FakeInspector {
regs: VecDeque::from([Ok(pipe_syscall(0x1000)), Ok(pipe_syscall(0x1000))]),
return_values: VecDeque::from([Ok(-22)]),
fd_arrays: VecDeque::from([Err(FdError::TraceeInstallFailed("should not read"))]),
..FakeInspector::default()
};
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::default();
let mut resumer = FakeResumer::default();
for _ in 0..2 {
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
LinuxPtraceStop::Syscall { host_pid: 4321 },
1234,
&mut inspector,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap();
}
assert!(inspector.fd_array_calls.is_empty());
assert_eq!(fds.descriptor(model_pid, 50), Err(FdError::BadFd(50)));
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn syscall_exit_tracks_thread_kernel_state_syscalls() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
let mut inspector = FakeInspector::default();
supervisor
.pending_syscalls
.insert(4321, set_tid_address_syscall(0x7000));
supervisor
.apply_successful_syscall_exit(&mut fds, 4321, 4321, &mut inspector)
.unwrap();
supervisor
.pending_syscalls
.insert(4321, set_robust_list_syscall(0x8000, 24));
supervisor
.apply_successful_syscall_exit(&mut fds, 4321, 0, &mut inspector)
.unwrap();
let states = supervisor.registered_thread_kernel_states();
assert_eq!(states.len(), 1);
assert_eq!(states[0].host_pid, 4321);
assert_eq!(states[0].model_pid, model_pid);
assert_eq!(states[0].clear_child_tid, Some(0x7000));
assert_eq!(states[0].robust_list_head, Some(0x8000));
assert_eq!(states[0].robust_list_len, 24);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn clone_exit_tracks_child_tid_pointer_for_registered_child_thread() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
supervisor
.handle_process_event(
&mut fds,
TraceeProcessEvent::Fork {
parent_host_pid: 4321,
child_host_pid: 4322,
shares_fd_table: true,
},
)
.unwrap();
let mut inspector = FakeInspector::default();
supervisor.pending_syscalls.insert(
4321,
clone_syscall(
LINUX_CLONE_CHILD_CLEARTID | LINUX_CLONE_CHILD_SETTID,
0x9000,
),
);
supervisor
.apply_successful_syscall_exit(&mut fds, 4321, 4322, &mut inspector)
.unwrap();
let states = supervisor.registered_thread_kernel_states();
let parent = states.iter().find(|state| state.host_pid == 4321).unwrap();
let child = states.iter().find(|state| state.host_pid == 4322).unwrap();
assert_eq!(parent.clear_child_tid, None);
assert_eq!(child.model_pid, model_pid);
assert_eq!(child.clear_child_tid, Some(0x9000));
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn clone3_exit_tracks_child_tid_pointer_for_registered_child_thread() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
supervisor
.handle_process_event(
&mut fds,
TraceeProcessEvent::Fork {
parent_host_pid: 4321,
child_host_pid: 4322,
shares_fd_table: true,
},
)
.unwrap();
let mut inspector = FakeInspector {
u64_arrays: VecDeque::from([Ok(vec![
LINUX_CLONE_CHILD_CLEARTID | LINUX_CLONE_CHILD_SETTID,
0,
0x9000,
])]),
..FakeInspector::default()
};
supervisor
.pending_syscalls
.insert(4321, clone3_syscall(0x7000, 88));
supervisor
.apply_successful_syscall_exit(&mut fds, 4321, 4322, &mut inspector)
.unwrap();
let states = supervisor.registered_thread_kernel_states();
let parent = states.iter().find(|state| state.host_pid == 4321).unwrap();
let child = states.iter().find(|state| state.host_pid == 4322).unwrap();
assert_eq!(parent.clear_child_tid, None);
assert_eq!(child.model_pid, model_pid);
assert_eq!(child.clear_child_tid, Some(0x9000));
assert_eq!(inspector.u64_array_calls, vec![(4321, 0x7000, 3)]);
}
#[test]
fn exec_path_capture_skips_non_exec_syscalls_without_reading_memory() {
let fds = KboxlikeFdSystem::new();
let supervisor = KboxlikeSyscallSupervisor::new();
let mut reader = FakeCStringReader::default();
assert_eq!(
capture_exec_guest_path_from_syscall(
&supervisor,
&fds,
4321,
mmap_syscall(104),
&mut reader,
)
.unwrap(),
None
);
assert!(reader.calls.is_empty());
}
#[test]
fn exec_path_capture_reads_execve_and_execveat_path_pointers() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
let mut reader = FakeCStringReader {
reads: VecDeque::from([
Ok("/usr/local/bin/node".to_owned()),
Ok("/bin/chrome-helper".to_owned()),
]),
..FakeCStringReader::default()
};
assert_eq!(
capture_exec_guest_path_from_syscall(
&supervisor,
&fds,
4321,
execve_syscall(0x1000),
&mut reader,
)
.unwrap(),
Some("/usr/local/bin/node".to_owned())
);
assert_eq!(
capture_exec_guest_path_from_syscall(
&supervisor,
&fds,
4321,
execveat_syscall(0x2000),
&mut reader,
)
.unwrap(),
Some("/bin/chrome-helper".to_owned())
);
assert_eq!(
reader.calls,
vec![
(4321, 0x1000, LINUX_EXEC_PATH_MAX),
(4321, 0x2000, LINUX_EXEC_PATH_MAX),
]
);
}
#[test]
fn exec_path_capture_resolves_proc_self_exe_through_model_identity() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init_with_exe("/ms-playwright/chrome");
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
let mut reader = FakeCStringReader {
reads: VecDeque::from([Ok("/proc/self/exe".to_owned())]),
..FakeCStringReader::default()
};
assert_eq!(
capture_exec_guest_path_from_syscall(
&supervisor,
&fds,
4321,
execve_syscall(0x1000),
&mut reader,
)
.unwrap(),
Some("/ms-playwright/chrome".to_owned())
);
}
#[test]
fn exec_path_capture_propagates_unknown_tracee_and_read_errors() {
let fds = KboxlikeFdSystem::new();
let supervisor = KboxlikeSyscallSupervisor::new();
let mut reader = FakeCStringReader {
reads: VecDeque::from([Ok("/bin/sh".to_owned())]),
..FakeCStringReader::default()
};
assert_eq!(
capture_exec_guest_path_from_syscall(
&supervisor,
&fds,
4321,
execve_syscall(0x1000),
&mut reader,
)
.unwrap_err(),
FdError::TraceeInstallFailed("unknown tracee pid")
);
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
let mut reader = FakeCStringReader {
reads: VecDeque::from([Err(FdError::TraceeInstallFailed("process_vm_readv failed"))]),
..FakeCStringReader::default()
};
assert_eq!(
capture_exec_guest_path_from_syscall(
&supervisor,
&fds,
4321,
execve_syscall(0x1000),
&mut reader,
)
.unwrap_err(),
FdError::TraceeInstallFailed("process_vm_readv failed")
);
}
#[test]
fn ptrace_reader_fetches_event_msg_only_for_fork_like_events() {
let mut reader = FakeWaitReader {
waits: VecDeque::from([
Ok((4321, ptrace_event_status(LINUX_PTRACE_EVENT_CLONE))),
Ok((4321, ptrace_event_status(LINUX_PTRACE_EVENT_EXEC))),
Ok((4321, stopped_status(LINUX_WAIT_SYSCALL_STOP_SIGNAL))),
]),
event_msgs: VecDeque::from([Ok(4322)]),
..FakeWaitReader::default()
};
assert_eq!(
read_linux_ptrace_stop(&mut reader).unwrap(),
LinuxPtraceStop::Fork {
parent_host_pid: 4321,
child_host_pid: 4322,
shares_fd_table: true,
}
);
assert_eq!(
read_linux_ptrace_stop(&mut reader).unwrap(),
LinuxPtraceStop::Exec { host_pid: 4321 }
);
assert_eq!(
read_linux_ptrace_stop(&mut reader).unwrap(),
LinuxPtraceStop::Syscall { host_pid: 4321 }
);
assert_eq!(reader.event_msg_pids, vec![4321]);
}
#[test]
fn ptrace_reader_propagates_wait_and_event_msg_errors() {
let mut wait_fail = FakeWaitReader {
waits: VecDeque::from([Err(FdError::TraceeInstallFailed("waitpid failed"))]),
..FakeWaitReader::default()
};
assert_eq!(
read_linux_ptrace_stop(&mut wait_fail).unwrap_err(),
FdError::TraceeInstallFailed("waitpid failed")
);
let mut event_fail = FakeWaitReader {
waits: VecDeque::from([Ok((4321, ptrace_event_status(LINUX_PTRACE_EVENT_CLONE)))]),
event_msgs: VecDeque::from([Err(FdError::TraceeInstallFailed(
"ptrace geteventmsg failed",
))]),
..FakeWaitReader::default()
};
assert_eq!(
read_linux_ptrace_stop(&mut event_fail).unwrap_err(),
FdError::TraceeInstallFailed("ptrace geteventmsg failed")
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn supervisor_wait_retries_interrupted_waitpid() {
let mut calls = 0;
let (pid, status) = waitpid_any_tracee_blocking_with(|status| {
calls += 1;
if calls == 1 {
unsafe {
*libc::__errno_location() = libc::EINTR;
}
return -1;
}
unsafe {
*status = libc::SIGSTOP << 8 | 0x7f;
}
4321
})
.expect("interrupted supervisor waitpid is retried");
assert_eq!(calls, 2);
assert_eq!(pid, 4321);
assert!(libc::WIFSTOPPED(status));
assert_eq!(libc::WSTOPSIG(status), libc::SIGSTOP);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn initial_tracee_wait_retries_interrupted_waitpid() {
let mut calls = 0;
let status = waitpid_specific_blocking_with(4321, |status| {
calls += 1;
if calls == 1 {
unsafe {
*libc::__errno_location() = libc::EINTR;
}
return -1;
}
unsafe {
*status = libc::SIGSTOP << 8 | 0x7f;
}
4321
})
.expect("interrupted initial waitpid is retried");
assert_eq!(calls, 2);
assert!(libc::WIFSTOPPED(status));
assert_eq!(libc::WSTOPSIG(status), libc::SIGSTOP);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn linux_ptrace_reader_exec_path_cache_validates_and_consumes_paths() {
let mut reader = LinuxPtraceReader::new();
assert_eq!(
reader.record_exec_guest_path(0, "/bin/false").unwrap_err(),
FdError::TraceeInstallFailed("invalid tracee pid")
);
reader
.record_exec_guest_path(4321, "/ms-playwright/chrome")
.unwrap();
assert_eq!(
reader.exec_guest_path(4321).unwrap(),
"/ms-playwright/chrome"
);
assert_eq!(
reader.exec_guest_path(4321).unwrap_err(),
FdError::TraceeInstallFailed("missing exec guest path")
);
}
#[test]
fn ptrace_dispatch_fetches_regs_and_handles_syscall_stop() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init();
fds.insert_plain_fd(model_pid, 104, Some(204), false)
.unwrap();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
let mut inspector = FakeInspector {
regs: VecDeque::from([Ok(mmap_syscall(104))]),
..FakeInspector::default()
};
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::default();
let mut resumer = FakeResumer::default();
assert_eq!(
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
LinuxPtraceStop::Syscall { host_pid: 4321 },
1234,
&mut inspector,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap(),
LinuxPtraceDispatchOutcome::Syscall(SupervisedSyscallStopOutcome::Entry(
SyscallStopOutcome::NoShadowNeeded
))
);
assert_eq!(inspector.reg_pids, vec![4321]);
assert_eq!(resumer.resumed, vec![4321]);
}
#[test]
fn ptrace_dispatch_records_exec_path_on_syscall_entry_before_exec_event() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init_with_exe("/ms-playwright/chromium/chrome");
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
let mut inspector = FakeInspector {
regs: VecDeque::from([Ok(execve_syscall(0x1000))]),
cstrings: VecDeque::from([Ok("/proc/self/exe".to_owned())]),
..FakeInspector::default()
};
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::default();
let mut resumer = FakeResumer::default();
assert_eq!(
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
LinuxPtraceStop::Syscall { host_pid: 4321 },
1234,
&mut inspector,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap(),
LinuxPtraceDispatchOutcome::Syscall(SupervisedSyscallStopOutcome::Entry(
SyscallStopOutcome::Unclassified
))
);
assert_eq!(
inspector.cstring_calls,
vec![(4321, 0x1000, LINUX_EXEC_PATH_MAX)]
);
assert_eq!(
inspector.recorded_exec_paths,
VecDeque::from([(4321, "/ms-playwright/chromium/chrome".to_owned())])
);
assert_eq!(
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
LinuxPtraceStop::Exec { host_pid: 4321 },
1234,
&mut inspector,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap(),
LinuxPtraceDispatchOutcome::Process(SupervisedProcessEventOutcome::Execed {
model_pid,
})
);
assert_eq!(
fds.current_exe(model_pid).unwrap(),
Some("/ms-playwright/chromium/chrome")
);
assert!(inspector.recorded_exec_paths.is_empty());
}
#[test]
fn ptrace_dispatch_skips_exec_path_capture_on_syscall_exit() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init_with_exe("/ms-playwright/chromium/chrome");
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
supervisor.advance_after_resume(4321, SyscallStopPhase::Entry);
supervisor
.pending_syscalls
.insert(4321, execve_syscall(0x1000));
let mut inspector = FakeInspector {
regs: VecDeque::from([Ok(execve_syscall(0x1000))]),
cstrings: VecDeque::from([Err(FdError::TraceeInstallFailed("should not read"))]),
..FakeInspector::default()
};
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::default();
let mut resumer = FakeResumer::default();
assert_eq!(
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
LinuxPtraceStop::Syscall { host_pid: 4321 },
1234,
&mut inspector,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap(),
LinuxPtraceDispatchOutcome::Syscall(SupervisedSyscallStopOutcome::Exit)
);
assert!(inspector.cstring_calls.is_empty());
assert!(inspector.recorded_exec_paths.is_empty());
assert_eq!(resumer.resumed, vec![4321]);
}
#[test]
fn ptrace_dispatch_propagates_exec_path_capture_error_without_resuming() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init_with_exe("/ms-playwright/chromium/chrome");
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
let mut inspector = FakeInspector {
regs: VecDeque::from([Ok(execve_syscall(0x1000))]),
cstrings: VecDeque::from([Err(FdError::TraceeInstallFailed(
"process_vm_readv failed",
))]),
..FakeInspector::default()
};
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::default();
let mut resumer = FakeResumer::default();
assert_eq!(
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
LinuxPtraceStop::Syscall { host_pid: 4321 },
1234,
&mut inspector,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap_err(),
FdError::TraceeInstallFailed("process_vm_readv failed")
);
assert_eq!(
inspector.cstring_calls,
vec![(4321, 0x1000, LINUX_EXEC_PATH_MAX)]
);
assert!(inspector.recorded_exec_paths.is_empty());
assert!(resumer.resumed.is_empty());
assert_eq!(
supervisor.next_syscall_stop_phase(4321).unwrap(),
SyscallStopPhase::Entry
);
}
#[test]
fn ptrace_dispatch_handles_fork_and_exec_process_events() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init_with_exe("/ms-playwright/chrome");
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
let mut inspector = FakeInspector {
exec_paths: VecDeque::from([Ok("/usr/lib/chrome_crashpad_handler".to_owned())]),
..FakeInspector::default()
};
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::default();
let mut resumer = FakeResumer::default();
assert_eq!(
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
LinuxPtraceStop::Fork {
parent_host_pid: 4321,
child_host_pid: 4322,
shares_fd_table: false,
},
1234,
&mut inspector,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap(),
LinuxPtraceDispatchOutcome::Process(SupervisedProcessEventOutcome::Forked {
parent_model_pid: model_pid,
child_model_pid: model_pid + 1,
})
);
let child_model = supervisor.model_pid_for_tracee(4322).unwrap();
assert_eq!(
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
LinuxPtraceStop::Exec { host_pid: 4322 },
1234,
&mut inspector,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap(),
LinuxPtraceDispatchOutcome::Process(SupervisedProcessEventOutcome::Execed {
model_pid: child_model,
})
);
assert_eq!(inspector.exec_pids, vec![4322]);
assert_eq!(
fds.current_exe(child_model).unwrap(),
Some("/usr/lib/chrome_crashpad_handler")
);
assert_eq!(resumer.resumed, vec![4321, 4322]);
}
#[test]
fn ptrace_dispatch_unregisters_process_on_ptrace_exit_and_end_status() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
let mut inspector = FakeInspector::default();
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::default();
let mut resumer = FakeResumer::default();
assert_eq!(
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
LinuxPtraceStop::Exit { host_pid: 4321 },
1234,
&mut inspector,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap(),
LinuxPtraceDispatchOutcome::Process(SupervisedProcessEventOutcome::Exited {
model_pid,
})
);
assert_eq!(
supervisor.model_pid_for_tracee(4321).unwrap_err(),
FdError::TraceeInstallFailed("unknown tracee pid")
);
assert_eq!(resumer.resumed, vec![4321]);
let second_model = fds.spawn_init();
supervisor.register_tracee(4322, second_model).unwrap();
assert_eq!(
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
LinuxPtraceStop::Exited {
host_pid: 4322,
code: 17,
},
1234,
&mut inspector,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap(),
LinuxPtraceDispatchOutcome::ProcessExited {
host_pid: 4322,
code: 17,
}
);
assert_eq!(
supervisor.model_pid_for_tracee(4322).unwrap_err(),
FdError::TraceeInstallFailed("unknown tracee pid")
);
}
#[test]
fn ptrace_dispatch_resumes_plain_signal_with_signal_delivery() {
let mut fds = KboxlikeFdSystem::new();
let mut supervisor = KboxlikeSyscallSupervisor::new();
let mut inspector = FakeInspector::default();
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::default();
let mut resumer = FakeResumer::default();
assert_eq!(
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
LinuxPtraceStop::Signal {
host_pid: 4321,
signal: libc::SIGSTOP,
},
1234,
&mut inspector,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap(),
LinuxPtraceDispatchOutcome::Signal {
host_pid: 4321,
signal: libc::SIGSTOP,
}
);
assert_eq!(resumer.signal_resumed, vec![(4321, libc::SIGSTOP)]);
assert!(resumer.resumed.is_empty());
}
#[test]
fn linux_wait_decoder_classifies_syscall_stop() {
assert_eq!(
decode_linux_ptrace_wait_status(
4321,
stopped_status(LINUX_WAIT_SYSCALL_STOP_SIGNAL),
None,
)
.unwrap(),
LinuxPtraceStop::Syscall { host_pid: 4321 }
);
}
#[test]
fn linux_wait_decoder_classifies_fork_clone_and_vfork_events() {
for (event, shares_fd_table) in [
(LINUX_PTRACE_EVENT_FORK, false),
(LINUX_PTRACE_EVENT_VFORK, false),
(LINUX_PTRACE_EVENT_CLONE, true),
] {
assert_eq!(
decode_linux_ptrace_wait_status(4321, ptrace_event_status(event), Some(4322))
.unwrap(),
LinuxPtraceStop::Fork {
parent_host_pid: 4321,
child_host_pid: 4322,
shares_fd_table,
}
);
}
}
#[test]
fn linux_wait_decoder_rejects_missing_or_invalid_fork_event_msg() {
assert_eq!(
decode_linux_ptrace_wait_status(
4321,
ptrace_event_status(LINUX_PTRACE_EVENT_CLONE),
None,
)
.unwrap_err(),
FdError::TraceeInstallFailed("missing ptrace event msg")
);
assert_eq!(
decode_linux_ptrace_wait_status(
4321,
ptrace_event_status(LINUX_PTRACE_EVENT_CLONE),
Some(0),
)
.unwrap_err(),
FdError::TraceeInstallFailed("invalid tracee pid")
);
}
#[test]
fn linux_wait_decoder_classifies_exec_exit_signal_and_process_end() {
assert_eq!(
decode_linux_ptrace_wait_status(
4321,
ptrace_event_status(LINUX_PTRACE_EVENT_EXEC),
None,
)
.unwrap(),
LinuxPtraceStop::Exec { host_pid: 4321 }
);
assert_eq!(
decode_linux_ptrace_wait_status(
4321,
ptrace_event_status(LINUX_PTRACE_EVENT_EXIT),
None,
)
.unwrap(),
LinuxPtraceStop::Exit { host_pid: 4321 }
);
assert_eq!(
decode_linux_ptrace_wait_status(4321, stopped_status(libc::SIGSTOP), None).unwrap(),
LinuxPtraceStop::Signal {
host_pid: 4321,
signal: libc::SIGSTOP,
}
);
assert_eq!(
decode_linux_ptrace_wait_status(4321, exited_status(17), None).unwrap(),
LinuxPtraceStop::Exited {
host_pid: 4321,
code: 17,
}
);
assert_eq!(
decode_linux_ptrace_wait_status(4321, signaled_status(libc::SIGKILL), None).unwrap(),
LinuxPtraceStop::Signaled {
host_pid: 4321,
signal: libc::SIGKILL,
}
);
}
#[test]
fn fork_event_registers_child_and_inherits_parent_fd_model() {
let mut fds = KboxlikeFdSystem::new();
let parent_model = fds.spawn_init_with_exe("/ms-playwright/chrome");
fds.insert_shadow_fd(
parent_model,
104,
Some(204),
ShadowObject {
kind: ShadowKind::LocalOnly,
supervisor_fd: 700,
},
false,
)
.unwrap();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, parent_model).unwrap();
assert_eq!(
supervisor
.handle_process_event(
&mut fds,
TraceeProcessEvent::Fork {
parent_host_pid: 4321,
child_host_pid: 4322,
shares_fd_table: false,
},
)
.unwrap(),
SupervisedProcessEventOutcome::Forked {
parent_model_pid: parent_model,
child_model_pid: parent_model + 1,
}
);
let child_model = supervisor.model_pid_for_tracee(4322).unwrap();
assert_eq!(
fds.current_exe(child_model).unwrap(),
Some("/ms-playwright/chrome")
);
assert_eq!(
fds.descriptor(child_model, 104).unwrap().shadow,
Some(ShadowObject {
kind: ShadowKind::LocalOnly,
supervisor_fd: 700,
})
);
}
#[test]
fn clone_event_registers_child_on_shared_fd_model() {
let mut fds = KboxlikeFdSystem::new();
let parent_model = fds.spawn_init_with_exe("/usr/local/bin/node");
fds.insert_plain_fd(parent_model, 10, Some(10), false)
.unwrap();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, parent_model).unwrap();
assert_eq!(
supervisor
.handle_process_event(
&mut fds,
TraceeProcessEvent::Fork {
parent_host_pid: 4321,
child_host_pid: 4322,
shares_fd_table: true,
},
)
.unwrap(),
SupervisedProcessEventOutcome::Forked {
parent_model_pid: parent_model,
child_model_pid: parent_model,
}
);
assert_eq!(supervisor.model_pid_for_tracee(4322).unwrap(), parent_model);
fds.insert_plain_fd(parent_model, 12, Some(12), false)
.unwrap();
assert_eq!(
fds.route_fd_operation(parent_model, 12, FdOperation::Write)
.unwrap(),
FdRoute::Lkl { fd: 12 }
);
assert_eq!(
supervisor
.handle_process_event(&mut fds, TraceeProcessEvent::Exit { host_pid: 4322 })
.unwrap(),
SupervisedProcessEventOutcome::Exited {
model_pid: parent_model,
}
);
assert_eq!(
supervisor.model_pid_for_tracee(4322).unwrap_err(),
FdError::TraceeInstallFailed("unknown tracee pid")
);
assert_eq!(
fds.current_exe(parent_model).unwrap(),
Some("/usr/local/bin/node")
);
assert_eq!(supervisor.model_pid_for_tracee(4321).unwrap(), parent_model);
}
#[test]
fn supervisor_lists_registered_tracees_for_snapshot_capture() {
let mut fds = KboxlikeFdSystem::new();
let parent_model = fds.spawn_init();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(5000, parent_model).unwrap();
supervisor
.handle_process_event(
&mut fds,
TraceeProcessEvent::Fork {
parent_host_pid: 5000,
child_host_pid: 5001,
shares_fd_table: true,
},
)
.unwrap();
let forked_model = match supervisor
.handle_process_event(
&mut fds,
TraceeProcessEvent::Fork {
parent_host_pid: 5000,
child_host_pid: 6000,
shares_fd_table: false,
},
)
.unwrap()
{
SupervisedProcessEventOutcome::Forked {
child_model_pid, ..
} => child_model_pid,
other => panic!("unexpected event outcome: {other:?}"),
};
assert_eq!(
supervisor.registered_tracees(),
vec![
(5000, parent_model),
(5001, parent_model),
(6000, forked_model)
]
);
assert_eq!(
supervisor.registered_tracee_identities(),
vec![
KboxlikeTraceeIdentity {
host_pid: 5000,
model_pid: parent_model,
model_thread_id: 1,
},
KboxlikeTraceeIdentity {
host_pid: 5001,
model_pid: parent_model,
model_thread_id: 2,
},
KboxlikeTraceeIdentity {
host_pid: 6000,
model_pid: forked_model,
model_thread_id: 3,
},
]
);
}
#[test]
fn exec_event_applies_cloexec_and_updates_executable_identity() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init_with_exe("/ms-playwright/chrome");
fds.insert_plain_fd(model_pid, 10, Some(110), false)
.unwrap();
fds.insert_plain_fd(model_pid, 11, Some(111), true).unwrap();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
assert_eq!(
supervisor
.handle_process_event(
&mut fds,
TraceeProcessEvent::Exec {
host_pid: 4321,
guest_exe: "/usr/lib/chrome_crashpad_handler".to_owned(),
},
)
.unwrap(),
SupervisedProcessEventOutcome::Execed { model_pid }
);
assert_eq!(
fds.current_exe(model_pid).unwrap(),
Some("/usr/lib/chrome_crashpad_handler")
);
assert_eq!(fds.descriptor(model_pid, 10).unwrap().lkl_fd, Some(110));
assert_eq!(
fds.descriptor(model_pid, 11).unwrap_err(),
FdError::BadFd(11)
);
}
#[test]
fn exit_event_unregisters_tracee_and_releases_model_process() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init();
fds.insert_plain_fd(model_pid, 10, Some(110), false)
.unwrap();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
supervisor.advance_after_resume(4321, SyscallStopPhase::Entry);
assert_eq!(
supervisor
.handle_process_event(&mut fds, TraceeProcessEvent::Exit { host_pid: 4321 })
.unwrap(),
SupervisedProcessEventOutcome::Exited { model_pid }
);
assert_eq!(
supervisor.model_pid_for_tracee(4321).unwrap_err(),
FdError::TraceeInstallFailed("unknown tracee pid")
);
assert_eq!(
supervisor.next_syscall_stop_phase(4321).unwrap_err(),
FdError::TraceeInstallFailed("unknown tracee pid")
);
assert_eq!(
fds.current_exe(model_pid),
Err(FdError::BadProcess(model_pid))
);
}
#[test]
fn process_events_reject_unknown_or_duplicate_tracees() {
let mut fds = KboxlikeFdSystem::new();
let parent_model = fds.spawn_init();
let existing_model = fds.fork(parent_model).unwrap();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, parent_model).unwrap();
supervisor.register_tracee(4322, existing_model).unwrap();
assert_eq!(
supervisor
.handle_process_event(
&mut fds,
TraceeProcessEvent::Fork {
parent_host_pid: 9999,
child_host_pid: 4323,
shares_fd_table: false,
},
)
.unwrap_err(),
FdError::TraceeInstallFailed("unknown tracee pid")
);
assert_eq!(
supervisor
.handle_process_event(
&mut fds,
TraceeProcessEvent::Fork {
parent_host_pid: 4321,
child_host_pid: 4322,
shares_fd_table: false,
},
)
.unwrap_err(),
FdError::TraceeInstallFailed("tracee pid already registered")
);
}
#[test]
fn syscall_stop_phase_alternates_entry_exit_per_tracee() {
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, 7).unwrap();
supervisor.register_tracee(4322, 8).unwrap();
assert_eq!(
supervisor.next_syscall_stop_phase(4321).unwrap(),
SyscallStopPhase::Entry
);
supervisor.advance_after_resume(4321, SyscallStopPhase::Entry);
assert_eq!(
supervisor.next_syscall_stop_phase(4322).unwrap(),
SyscallStopPhase::Entry
);
supervisor.advance_after_resume(4322, SyscallStopPhase::Entry);
assert_eq!(
supervisor.next_syscall_stop_phase(4321).unwrap(),
SyscallStopPhase::Exit
);
supervisor.advance_after_resume(4321, SyscallStopPhase::Exit);
assert_eq!(
supervisor.next_syscall_stop_phase(4321).unwrap(),
SyscallStopPhase::Entry
);
}
#[test]
fn unregister_tracee_clears_inflight_phase() {
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, 7).unwrap();
assert_eq!(
supervisor.next_syscall_stop_phase(4321).unwrap(),
SyscallStopPhase::Entry
);
supervisor.advance_after_resume(4321, SyscallStopPhase::Entry);
assert_eq!(supervisor.unregister_tracee(4321), Some(7));
supervisor.register_tracee(4321, 8).unwrap();
assert_eq!(
supervisor.next_syscall_stop_phase(4321).unwrap(),
SyscallStopPhase::Entry
);
}
#[test]
fn syscall_stop_rejects_unknown_tracee_before_resume() {
let mut supervisor = KboxlikeSyscallSupervisor::new();
let mut fds = KboxlikeFdSystem::new();
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::default();
let mut resumer = FakeResumer::default();
assert_eq!(
supervisor
.handle_syscall_stop(
&mut fds,
4321,
mmap_syscall(104),
1234,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap_err(),
FdError::TraceeInstallFailed("unknown tracee pid")
);
assert!(resumer.resumed.is_empty());
}
#[test]
fn syscall_stop_installs_shadow_on_entry_then_skips_exit() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init();
fds.insert_shadow_fd(
model_pid,
104,
Some(204),
ShadowObject {
kind: ShadowKind::LocalOnly,
supervisor_fd: 700,
},
false,
)
.unwrap();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::with_returns([0x8000, 901, 104, 0, 0, 0]);
let mut resumer = FakeResumer::default();
assert_eq!(
supervisor
.handle_syscall_stop(
&mut fds,
4321,
mmap_syscall(104),
1234,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap(),
SupervisedSyscallStopOutcome::Entry(SyscallStopOutcome::InstalledShadow)
);
assert_eq!(
supervisor
.handle_syscall_stop(
&mut fds,
4321,
mmap_syscall(104),
1234,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap(),
SupervisedSyscallStopOutcome::Exit
);
assert_eq!(
memory.writes,
vec![(0x8000, b"/proc/1234/fd/700\0".to_vec())]
);
assert_eq!(resumer.resumed, vec![4321, 4321]);
}
#[test]
fn syscall_stop_does_not_resume_after_install_error() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init();
fds.insert_shadow_fd(
model_pid,
104,
Some(204),
ShadowObject {
kind: ShadowKind::LocalOnly,
supervisor_fd: 700,
},
false,
)
.unwrap();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::with_returns([-1]);
let mut resumer = FakeResumer::default();
assert_eq!(
supervisor
.handle_syscall_stop(
&mut fds,
4321,
mmap_syscall(104),
1234,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap_err(),
FdError::TraceeInstallFailed("tracee mmap failed")
);
assert!(resumer.resumed.is_empty());
assert_eq!(
supervisor.next_syscall_stop_phase(4321).unwrap(),
SyscallStopPhase::Entry
);
}
#[test]
fn syscall_stop_returns_resume_error_after_successful_entry_work() {
let mut fds = KboxlikeFdSystem::new();
let model_pid = fds.spawn_init();
fds.insert_plain_fd(model_pid, 104, Some(204), false)
.unwrap();
let mut supervisor = KboxlikeSyscallSupervisor::new();
supervisor.register_tracee(4321, model_pid).unwrap();
let mut memory = FakeMemory::default();
let mut tracee = FakeTracee::default();
let mut resumer = FakeResumer {
fail: true,
..FakeResumer::default()
};
assert_eq!(
supervisor
.handle_syscall_stop(
&mut fds,
4321,
mmap_syscall(104),
1234,
&mut memory,
&mut tracee,
&mut resumer,
)
.unwrap_err(),
FdError::TraceeInstallFailed("resume failed")
);
assert_eq!(
supervisor.next_syscall_stop_phase(4321).unwrap(),
SyscallStopPhase::Entry
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn ptrace_syscall_resumer_rejects_invalid_pid() {
let mut resumer = PtraceSyscallStopResumer;
assert_eq!(
resumer.resume_syscall_stop(0).unwrap_err(),
FdError::TraceeInstallFailed("invalid tracee pid")
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn wait_for_traceme_sigstop(child: i32) -> 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(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kill_and_reap_tracee(child: i32) {
unsafe {
libc::kill(child, libc::SIGKILL);
let mut status = 0;
loop {
let waited = libc::waitpid(child, &mut status, 0);
if waited == child || waited < 0 {
break;
}
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn run_live_ptrace_exec(
argv: &[&str],
max_stops: usize,
) -> Result<LinuxPtraceLoopSummary, FdError> {
use std::ffi::CString;
if argv.is_empty() {
return Err(FdError::TraceeInstallFailed("live exec argv is empty"));
}
let argv_cstrings = argv
.iter()
.map(|arg| {
CString::new(*arg)
.map_err(|_| FdError::TraceeInstallFailed("argv contains nul byte"))
})
.collect::<Result<Vec<_>, _>>()?;
let mut argv_ptrs = argv_cstrings
.iter()
.map(|arg| arg.as_ptr())
.collect::<Vec<_>>();
argv_ptrs.push(std::ptr::null());
let path = argv_cstrings[0].as_ptr();
let child = unsafe { libc::fork() };
if child < 0 {
return Err(FdError::TraceeInstallFailed("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::execv(path, argv_ptrs.as_ptr());
libc::_exit(112);
}
}
let result = (|| {
wait_for_traceme_sigstop(child)?;
let mut fds = KboxlikeFdSystem::new();
let mut supervisor = KboxlikeSyscallSupervisor::new();
let mut reader = LinuxPtraceReader::default();
let mut resumer = PtraceSyscallStopResumer;
configure_initial_linux_ptrace_tracee(
&mut supervisor,
&mut fds,
child,
"/proc/self/exe",
&mut reader,
&mut resumer,
)?;
let mut access_factory = LinuxPtraceTraceeAccessFactory;
run_linux_ptrace_dispatch_loop(
&mut supervisor,
&mut fds,
std::process::id() as i32,
&mut reader,
&mut access_factory,
max_stops,
)
})();
if result.is_err() {
kill_and_reap_tracee(child);
}
result
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn capture_live_ptrace_exec_snapshot_on_sigstop(
argv: &[&str],
min_tracees: usize,
max_stops: usize,
) -> Result<(i32, KboxlikeStoppedTraceeSnapshot), FdError> {
use std::ffi::CString;
if argv.is_empty() {
return Err(FdError::TraceeInstallFailed("live exec argv is empty"));
}
let argv_cstrings = argv
.iter()
.map(|arg| {
CString::new(*arg)
.map_err(|_| FdError::TraceeInstallFailed("argv contains nul byte"))
})
.collect::<Result<Vec<_>, _>>()?;
let mut argv_ptrs = argv_cstrings
.iter()
.map(|arg| arg.as_ptr())
.collect::<Vec<_>>();
argv_ptrs.push(std::ptr::null());
let path = argv_cstrings[0].as_ptr();
let child = unsafe { libc::fork() };
if child < 0 {
return Err(FdError::TraceeInstallFailed("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::execv(path, argv_ptrs.as_ptr());
libc::_exit(112);
}
}
let result = (|| {
wait_for_traceme_sigstop(child)?;
let mut fds = KboxlikeFdSystem::new();
let mut supervisor = KboxlikeSyscallSupervisor::new();
let mut reader = LinuxPtraceReader::default();
let mut resumer = PtraceSyscallStopResumer;
configure_initial_linux_ptrace_tracee(
&mut supervisor,
&mut fds,
child,
"/proc/self/exe",
&mut reader,
&mut resumer,
)?;
let mut access_factory = LinuxPtraceTraceeAccessFactory;
let mut parked_tracees = BTreeSet::new();
let mut delivered_sigstop = false;
for _ in 0..max_stops {
let stop = read_linux_ptrace_stop(&mut reader)?;
let host_pid = stop.host_pid();
if matches!(
stop,
LinuxPtraceStop::Signal {
signal: libc::SIGSTOP,
..
}
) && supervisor
.registered_tracee_identities()
.iter()
.any(|identity| identity.host_pid == host_pid)
{
if !delivered_sigstop {
delivered_sigstop = true;
resumer.resume_signal_stop(host_pid, libc::SIGSTOP)?;
continue;
}
parked_tracees.insert(host_pid);
let registered = supervisor.registered_tracee_identities();
if parked_tracees.len() >= min_tracees
&& registered
.iter()
.all(|identity| parked_tracees.contains(&identity.host_pid))
{
let snapshot = capture_stopped_tracees(&supervisor, &mut reader)?;
if !snapshot
.threads
.iter()
.all(|thread| parked_tracees.contains(&thread.host_pid))
{
return Err(FdError::TraceeInstallFailed(
"snapshot included an unparked tracee",
));
}
return Ok((child, snapshot));
}
continue;
}
let mut access = access_factory.access_for(host_pid)?;
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
stop,
std::process::id() as i32,
&mut reader,
&mut access.writer,
&mut access.executor,
&mut access.resumer,
)?;
}
Err(FdError::TraceeInstallFailed(
"live exec tracees did not all stop on SIGSTOP",
))
})();
if result.is_err() {
kill_and_reap_tracee(child);
}
result
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn capture_live_ptrace_exec_snapshot_after_sigstop_request(
argv: &[&str],
min_tracees: usize,
max_stops: usize,
stop_after: std::time::Duration,
) -> Result<(i32, KboxlikeStoppedTraceeSnapshot), FdError> {
use std::ffi::CString;
if argv.is_empty() {
return Err(FdError::TraceeInstallFailed("live exec argv is empty"));
}
let argv_cstrings = argv
.iter()
.map(|arg| {
CString::new(*arg)
.map_err(|_| FdError::TraceeInstallFailed("argv contains nul byte"))
})
.collect::<Result<Vec<_>, _>>()?;
let mut argv_ptrs = argv_cstrings
.iter()
.map(|arg| arg.as_ptr())
.collect::<Vec<_>>();
argv_ptrs.push(std::ptr::null());
let path = argv_cstrings[0].as_ptr();
let child = unsafe { libc::fork() };
if child < 0 {
return Err(FdError::TraceeInstallFailed("fork failed"));
}
if child == 0 {
unsafe {
libc::setpgid(0, 0);
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::execv(path, argv_ptrs.as_ptr());
libc::_exit(112);
}
}
let result = (|| {
wait_for_traceme_sigstop(child)?;
let mut fds = KboxlikeFdSystem::new();
let mut supervisor = KboxlikeSyscallSupervisor::new();
let mut reader = LinuxPtraceReader::default();
let mut resumer = PtraceSyscallStopResumer;
configure_initial_linux_ptrace_tracee(
&mut supervisor,
&mut fds,
child,
"/proc/self/exe",
&mut reader,
&mut resumer,
)?;
let mut access_factory = LinuxPtraceTraceeAccessFactory;
let mut parked_tracees = BTreeSet::new();
let mut stop_requested = false;
let started = std::time::Instant::now();
let stopper_child = child;
let _stopper = std::thread::spawn(move || {
std::thread::sleep(stop_after);
eprintln!("LIVE_CHROMIUM_SIGSTOP_REQUEST pgid={stopper_child}");
unsafe {
libc::kill(-stopper_child, libc::SIGSTOP);
libc::kill(stopper_child, libc::SIGSTOP);
}
});
for _ in 0..max_stops {
let stop = read_linux_ptrace_stop(&mut reader)?;
if !stop_requested && started.elapsed() >= stop_after {
stop_requested = true;
request_sigstop_for_registered_tracees(&supervisor, &parked_tracees)?;
}
let host_pid = stop.host_pid();
if stop_requested {
request_sigstop_for_registered_tracees(&supervisor, &parked_tracees)?;
}
if matches!(
stop,
LinuxPtraceStop::Signal {
signal: libc::SIGSTOP,
..
}
) && supervisor
.registered_tracee_identities()
.iter()
.any(|identity| identity.host_pid == host_pid)
{
parked_tracees.insert(host_pid);
let registered = supervisor.registered_tracee_identities();
eprintln!(
"LIVE_CHROMIUM_SIGSTOP_PARKED host_pid={} parked={} registered={}",
host_pid,
parked_tracees.len(),
registered.len()
);
if parked_tracees.len() >= min_tracees
&& registered
.iter()
.all(|identity| parked_tracees.contains(&identity.host_pid))
{
eprintln!("LIVE_CHROMIUM_SIGSTOP_CAPTURE tracees={}", registered.len());
let snapshot = capture_stopped_tracees(&supervisor, &mut reader)?;
if !snapshot
.threads
.iter()
.all(|thread| parked_tracees.contains(&thread.host_pid))
{
return Err(FdError::TraceeInstallFailed(
"snapshot included an unparked tracee",
));
}
return Ok((child, snapshot));
}
continue;
}
let mut access = access_factory.access_for(host_pid)?;
dispatch_linux_ptrace_stop(
&mut supervisor,
&mut fds,
stop,
std::process::id() as i32,
&mut reader,
&mut access.writer,
&mut access.executor,
&mut access.resumer,
)?;
}
Err(FdError::TraceeInstallFailed(
"live exec tracees did not stop after SIGSTOP request",
))
})();
if result.is_err() {
kill_and_reap_tracee_group(child);
}
result
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn request_sigstop_for_registered_tracees(
supervisor: &KboxlikeSyscallSupervisor,
parked_tracees: &BTreeSet<i32>,
) -> Result<(), FdError> {
for identity in supervisor.registered_tracee_identities() {
if parked_tracees.contains(&identity.host_pid) {
continue;
}
let ret = unsafe { libc::kill(identity.host_pid, libc::SIGSTOP) };
if ret < 0 {
return Err(FdError::TraceeInstallFailed(
"failed to request tracee SIGSTOP",
));
}
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kill_and_reap_tracee_group(child: i32) {
unsafe {
let mut pids = live_process_group_pids(child);
if !pids.contains(&child) {
pids.push(child);
}
libc::kill(-child, libc::SIGKILL);
for pid in &pids {
libc::kill(*pid, libc::SIGKILL);
libc::ptrace(
libc::PTRACE_SYSCALL,
*pid,
std::ptr::null_mut::<libc::c_void>(),
libc::SIGKILL as usize as *mut libc::c_void,
);
}
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while std::time::Instant::now() <= deadline {
let mut status = 0;
let waited = libc::waitpid(-child, &mut status, libc::__WALL | libc::WNOHANG);
if waited == 0 {
std::thread::sleep(std::time::Duration::from_millis(1));
continue;
}
if waited < 0 {
break;
}
}
let root_deadline = std::time::Instant::now() + std::time::Duration::from_secs(1);
while std::time::Instant::now() <= root_deadline {
let mut status = 0;
let waited = libc::waitpid(child, &mut status, libc::__WALL | libc::WNOHANG);
if waited == 0 {
std::thread::sleep(std::time::Duration::from_millis(1));
} else {
break;
}
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn live_process_group_pids(pgid: i32) -> Vec<i32> {
let Ok(entries) = std::fs::read_dir("/proc") else {
return Vec::new();
};
let mut pids = Vec::new();
for entry in entries.flatten() {
let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
continue;
};
let Ok(pid) = name.parse::<i32>() else {
continue;
};
let Ok(stat) = std::fs::read_to_string(entry.path().join("stat")) else {
continue;
};
let Some((_, stat_tail)) = stat.rsplit_once(") ") else {
continue;
};
let fields = stat_tail.split_whitespace().collect::<Vec<_>>();
if fields.len() < 3 {
continue;
}
if fields[2].parse::<i32>().ok() == Some(pgid) {
pids.push(pid);
}
}
pids
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn print_live_chromium_fd_inventory(
snapshot: &KboxlikeStoppedTraceeSnapshot,
fd_system: &KboxlikeFdSystem,
) {
for table in &snapshot.fd_tables {
for fd in &table.fds {
let descriptor = fd_system.descriptor(fd.model_pid, fd.fd).ok();
let shadow = descriptor.and_then(|descriptor| descriptor.shadow);
let backing = fd_system
.shadow_backing(fd.model_pid, fd.fd)
.ok()
.flatten()
.cloned();
eprintln!(
"LIVE_CHROMIUM_FD host_pid={} model_pid={} model_thread={} fd={} kind={:?} cloexec={} flags={:?} shadow={:?} backing={:?} target={}",
fd.host_pid,
fd.model_pid,
fd.model_thread_id,
fd.fd,
fd.kind,
fd.cloexec,
fd.flags,
shadow,
backing,
fd.target,
);
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn print_live_restored_proc_fd_inventory(host_pids: impl IntoIterator<Item = i32>) {
for host_pid in host_pids {
let fd_dir = format!("/proc/{host_pid}/fd");
let Ok(entries) = std::fs::read_dir(&fd_dir) else {
eprintln!("LIVE_CHROMIUM_RESTORED_PROC_FD pid={host_pid} error=read_dir");
continue;
};
let mut fds = entries
.filter_map(|entry| entry.ok())
.filter_map(|entry| {
let name = entry.file_name();
let fd = name.to_string_lossy().parse::<i32>().ok()?;
Some((fd, entry.path()))
})
.collect::<Vec<_>>();
fds.sort_by_key(|(fd, _)| *fd);
for (fd, path) in fds {
let target = std::fs::read_link(&path)
.map(|target| target.display().to_string())
.unwrap_or_else(|err| format!("read_link_error:{err}"));
let fdinfo = std::fs::read_to_string(format!("/proc/{host_pid}/fdinfo/{fd}"))
.unwrap_or_default();
let flags = fdinfo
.lines()
.find_map(|line| line.strip_prefix("flags:"))
.map(str::trim)
.unwrap_or("unknown");
eprintln!(
"LIVE_CHROMIUM_RESTORED_PROC_FD pid={host_pid} fd={fd} flags={flags} target={target}"
);
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn print_live_restored_code_comparison(
snapshot: &KboxlikeStoppedTraceeSnapshot,
replacement_host_pids: &BTreeMap<ModelThreadId, i32>,
) {
for thread in &snapshot.threads {
let Some(replacement_host_pid) =
replacement_host_pids.get(&thread.model_thread_id).copied()
else {
continue;
};
let Some(restored_regs) = ptrace_getregs_for_log(replacement_host_pid) else {
eprintln!(
"LIVE_CHROMIUM_RESTORED_CODE_COMPARE model_thread={} source_pid={} restored_pid={} regs=unavailable",
thread.model_thread_id, thread.host_pid, replacement_host_pid
);
continue;
};
let restored_rip = restored_regs.rip;
let captured_rip = thread.regs.rip;
let compare_start = restored_rip.saturating_sub(16);
let captured_start = captured_rip.saturating_sub(16);
let source_bytes_at_restored =
read_process_memory_hex(thread.host_pid, compare_start, 64);
let restored_bytes_at_restored =
read_process_memory_hex(replacement_host_pid, compare_start, 64);
let source_bytes_at_captured =
read_process_memory_hex(thread.host_pid, captured_start, 64);
eprintln!(
"LIVE_CHROMIUM_RESTORED_CODE_COMPARE model_thread={} source_pid={} restored_pid={} captured_rip=0x{:x} restored_rip=0x{:x} captured_rax=0x{:x} restored_rax=0x{:x} source_map_at_restored={} restored_map_at_restored={} source_bytes_at_restored={} restored_bytes_at_restored={} source_bytes_at_captured={}",
thread.model_thread_id,
thread.host_pid,
replacement_host_pid,
captured_rip,
restored_rip,
thread.regs.rax,
restored_regs.rax,
proc_map_line_for_addr(thread.host_pid, restored_rip).unwrap_or_else(|| "none".to_owned()),
proc_map_line_for_addr(replacement_host_pid, restored_rip).unwrap_or_else(|| "none".to_owned()),
source_bytes_at_restored,
restored_bytes_at_restored,
source_bytes_at_captured,
);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn read_process_memory_hex(pid: i32, addr: u64, len: usize) -> String {
match read_process_memory(pid, addr, len) {
Ok(bytes) => hex_bytes(&bytes),
Err(err) => format!("error:{err}"),
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn read_process_memory(pid: i32, addr: u64, len: usize) -> Result<Vec<u8>, String> {
if len == 0 {
return Ok(Vec::new());
}
let mut bytes = vec![0u8; len];
let local = libc::iovec {
iov_base: bytes.as_mut_ptr() as *mut std::ffi::c_void,
iov_len: bytes.len(),
};
let remote = libc::iovec {
iov_base: addr as *mut std::ffi::c_void,
iov_len: bytes.len(),
};
let read = unsafe { libc::process_vm_readv(pid, &local, 1, &remote, 1, 0) };
if read < 0 {
return Err(format!(
"process_vm_readv_errno_{}",
std::io::Error::last_os_error()
.raw_os_error()
.unwrap_or_default()
));
}
if read as usize != len {
return Err(format!("partial_{read}_of_{len}"));
}
Ok(bytes)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn hex_bytes(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
use std::fmt::Write as _;
let _ = write!(out, "{byte:02x}");
}
out
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn live_pid_state(host_pid: i32) -> Option<char> {
let stat = std::fs::read_to_string(format!("/proc/{host_pid}/stat")).ok()?;
let end_comm = stat.rfind(") ")?;
stat[end_comm + 2..].chars().next()
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn read_devtools_active_port(profile_dir: &std::path::Path) -> Option<u16> {
let raw = std::fs::read_to_string(profile_dir.join("DevToolsActivePort")).ok()?;
raw.lines().next()?.trim().parse::<u16>().ok()
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn probe_devtools_version(port: u16, timeout: std::time::Duration) -> Result<String, String> {
let addr = std::net::SocketAddr::from((std::net::Ipv4Addr::LOCALHOST, port));
let mut stream =
std::net::TcpStream::connect_timeout(&addr, timeout).map_err(|err| err.to_string())?;
stream
.set_nonblocking(false)
.map_err(|err| err.to_string())?;
stream
.set_read_timeout(Some(timeout))
.map_err(|err| err.to_string())?;
stream
.set_write_timeout(Some(timeout))
.map_err(|err| err.to_string())?;
std::io::Write::write_all(
&mut stream,
b"GET /json/version HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n",
)
.map_err(|err| err.to_string())?;
let mut reader = std::io::BufReader::new(stream);
let mut status_line = String::new();
std::io::BufRead::read_line(&mut reader, &mut status_line)
.map_err(|err| err.to_string())?;
Ok(status_line.trim_end().to_owned())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn probe_devtools_version_until(
port: u16,
timeout: std::time::Duration,
) -> Result<String, String> {
let deadline = std::time::Instant::now() + timeout;
let mut last_err = None;
while std::time::Instant::now() < deadline {
match probe_devtools_version(port, std::time::Duration::from_millis(250)) {
Ok(status) if status.starts_with("HTTP/1.1 200") => return Ok(status),
Ok(status) => last_err = Some(format!("unexpected status line: {status}")),
Err(err) => last_err = Some(err),
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
Err(last_err.unwrap_or_else(|| "timed out before first probe".to_owned()))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn try_reap_pid_status(host_pid: i32) -> Option<String> {
let mut status = 0;
let waited = unsafe { libc::waitpid(host_pid, &mut status, libc::WNOHANG | libc::__WALL) };
if waited != host_pid {
return None;
}
if libc::WIFEXITED(status) {
return Some(format!("exit({}) raw={status}", libc::WEXITSTATUS(status)));
}
if libc::WIFSIGNALED(status) {
return Some(format!(
"signal({}) core={} raw={status}",
libc::WTERMSIG(status),
libc::WCOREDUMP(status),
));
}
if libc::WIFSTOPPED(status) {
return Some(format!(
"stopped({}) event={} raw={status} {}",
libc::WSTOPSIG(status),
status >> 16,
ptrace_stop_details(host_pid),
));
}
Some(format!("unknown raw={status}"))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn poll_restored_pid_statuses(
host_pids: impl IntoIterator<Item = i32>,
timeout: std::time::Duration,
) -> BTreeMap<i32, Option<String>> {
let mut pending = host_pids.into_iter().collect::<BTreeSet<_>>();
let mut statuses = BTreeMap::new();
let deadline = std::time::Instant::now() + timeout;
while !pending.is_empty() && std::time::Instant::now() < deadline {
let mut progressed = false;
for host_pid in pending.iter().copied().collect::<Vec<_>>() {
if let Some(status) = try_reap_pid_status(host_pid) {
if std::env::var_os("SUPERMACHINE_KBOXLIKE_RESTORE_SINGLESTEP_FIRST").is_some()
&& status.starts_with("stopped(5) event=0")
{
eprintln!("LIVE_CHROMIUM_RESTORED_STEP pid={host_pid} status={status}");
let ret = unsafe {
libc::ptrace(
libc::PTRACE_CONT,
host_pid,
std::ptr::null_mut::<std::ffi::c_void>(),
std::ptr::null_mut::<std::ffi::c_void>(),
)
};
if ret == 0 {
progressed = true;
continue;
}
}
if std::env::var_os("SUPERMACHINE_KBOXLIKE_CONTINUE_RESTORED_SIGTRAP").is_some()
&& status.starts_with("stopped(5) event=0")
{
eprintln!(
"LIVE_CHROMIUM_RESTORED_SIGTRAP_CONTINUE pid={host_pid} status={status}"
);
let ret = unsafe {
libc::ptrace(
libc::PTRACE_CONT,
host_pid,
std::ptr::null_mut::<std::ffi::c_void>(),
std::ptr::null_mut::<std::ffi::c_void>(),
)
};
if ret == 0 {
progressed = true;
continue;
}
}
statuses.insert(host_pid, Some(status));
pending.remove(&host_pid);
progressed = true;
}
}
if !progressed {
std::thread::sleep(std::time::Duration::from_millis(1));
}
}
for host_pid in pending {
statuses.insert(host_pid, None);
}
statuses
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn ptrace_stop_details(host_pid: i32) -> String {
let mut regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
let regs_ret = unsafe {
libc::ptrace(
libc::PTRACE_GETREGS,
host_pid,
std::ptr::null_mut::<std::ffi::c_void>(),
&mut regs as *mut _ as *mut std::ffi::c_void,
)
};
let mut siginfo: libc::siginfo_t = unsafe { std::mem::zeroed() };
let siginfo_ret = unsafe {
libc::ptrace(
libc::PTRACE_GETSIGINFO,
host_pid,
std::ptr::null_mut::<std::ffi::c_void>(),
&mut siginfo as *mut _ as *mut std::ffi::c_void,
)
};
let regs_text = if regs_ret == 0 {
format!(
"rip=0x{:x} rsp=0x{:x} rax=0x{:x} rip_map={} rsp_map={} stack_words={}",
regs.rip,
regs.rsp,
regs.rax,
proc_map_line_for_addr(host_pid, regs.rip).unwrap_or_else(|| "none".to_owned()),
proc_map_line_for_addr(host_pid, regs.rsp).unwrap_or_else(|| "none".to_owned()),
ptrace_peek_words(host_pid, regs.rsp, 6),
)
} else {
"regs=unavailable".to_owned()
};
let siginfo_text = if siginfo_ret == 0 {
let addr = unsafe { siginfo.si_addr() as usize };
format!(
"siginfo_signo={} code={} addr=0x{:x} addr_map={}",
siginfo.si_signo,
siginfo.si_code,
addr,
proc_map_line_for_addr(host_pid, addr as u64).unwrap_or_else(|| "none".to_owned()),
)
} else {
"siginfo=unavailable".to_owned()
};
let mut event_msg = 0u64;
let event_ret = unsafe {
libc::ptrace(
libc::PTRACE_GETEVENTMSG,
host_pid,
std::ptr::null_mut::<std::ffi::c_void>(),
&mut event_msg as *mut _ as *mut std::ffi::c_void,
)
};
let event_text = if event_ret == 0 {
format!("event_msg=0x{event_msg:x}")
} else {
"event_msg=unavailable".to_owned()
};
format!("{regs_text} {siginfo_text} {event_text}")
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn proc_map_line_for_addr(host_pid: i32, addr: u64) -> Option<String> {
let maps = std::fs::read_to_string(format!("/proc/{host_pid}/maps")).ok()?;
for line in maps.lines() {
let (range, _) = line.split_once(' ')?;
let (start, end) = range.split_once('-')?;
let start = u64::from_str_radix(start, 16).ok()?;
let end = u64::from_str_radix(end, 16).ok()?;
if addr >= start && addr < end {
return Some(line.to_owned());
}
}
None
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn ptrace_peek_words(host_pid: i32, addr: u64, count: usize) -> String {
let mut words = Vec::new();
for index in 0..count {
let Some(word_addr) = addr.checked_add((index * std::mem::size_of::<u64>()) as u64)
else {
break;
};
let word = unsafe {
libc::ptrace(
libc::PTRACE_PEEKDATA,
host_pid,
word_addr as usize as *mut std::ffi::c_void,
std::ptr::null_mut::<std::ffi::c_void>(),
)
};
words.push(format!("0x{word_addr:x}=0x{:x}", word as u64));
}
words.join(",")
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Debug, Default)]
struct PtraceContinueRestoredTraceeResumer;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl RestoredTraceeResumer for PtraceContinueRestoredTraceeResumer {
fn resume_restored_tracee(&mut self, host_pid: i32) -> Result<(), FdError> {
let trace_syscalls =
std::env::var_os("SUPERMACHINE_KBOXLIKE_TRACE_RESTORED_SYSCALLS").is_some();
let options = if trace_syscalls {
libc::PTRACE_O_TRACEEXIT | libc::PTRACE_O_TRACESYSGOOD
} else {
libc::PTRACE_O_TRACEEXIT
};
let options_ret = unsafe {
libc::ptrace(
libc::PTRACE_SETOPTIONS,
host_pid,
std::ptr::null_mut::<std::ffi::c_void>(),
(options as usize) as *mut std::ffi::c_void,
)
};
if options_ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace setoptions failed"));
}
let resume_request = if trace_syscalls {
libc::PTRACE_SYSCALL
} else if std::env::var_os("SUPERMACHINE_KBOXLIKE_RESTORE_SINGLESTEP_FIRST").is_some() {
libc::PTRACE_SINGLESTEP
} else {
libc::PTRACE_CONT
};
let ret = unsafe {
libc::ptrace(
resume_request,
host_pid,
std::ptr::null_mut::<std::ffi::c_void>(),
std::ptr::null_mut::<std::ffi::c_void>(),
)
};
if ret < 0 {
return Err(FdError::TraceeInstallFailed("ptrace continue failed"));
}
Ok(())
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug)]
struct RestoredSyscallEntry {
nr: u64,
args: [u64; 6],
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn trace_restored_syscalls_until_stop(
host_pids: impl IntoIterator<Item = i32>,
timeout: std::time::Duration,
) -> BTreeMap<i32, Option<String>> {
let mut pending = host_pids.into_iter().collect::<BTreeSet<_>>();
let mut statuses = BTreeMap::new();
let mut syscall_entries = BTreeMap::<i32, Option<RestoredSyscallEntry>>::new();
for host_pid in &pending {
syscall_entries.insert(*host_pid, None);
}
let deadline = std::time::Instant::now() + timeout;
while !pending.is_empty() && std::time::Instant::now() < deadline {
let mut progressed = false;
for host_pid in pending.iter().copied().collect::<Vec<_>>() {
let mut status = 0;
let waited =
unsafe { libc::waitpid(host_pid, &mut status, libc::WNOHANG | libc::__WALL) };
if waited != host_pid {
continue;
}
progressed = true;
if libc::WIFSTOPPED(status) && libc::WSTOPSIG(status) == (libc::SIGTRAP | 0x80) {
trace_restored_syscall_stop(host_pid, &mut syscall_entries);
let ret = unsafe {
libc::ptrace(
libc::PTRACE_SYSCALL,
host_pid,
std::ptr::null_mut::<std::ffi::c_void>(),
std::ptr::null_mut::<std::ffi::c_void>(),
)
};
if ret < 0 {
statuses.insert(
host_pid,
Some(format!("ptrace_syscall_continue_failed raw={status}")),
);
pending.remove(&host_pid);
}
continue;
}
if should_continue_restored_sigtrap(status) {
eprintln!(
"LIVE_CHROMIUM_RESTORED_SIGTRAP_CONTINUE pid={host_pid} status={}",
format_wait_status(host_pid, status),
);
let ret = unsafe {
libc::ptrace(
libc::PTRACE_SYSCALL,
host_pid,
std::ptr::null_mut::<std::ffi::c_void>(),
std::ptr::null_mut::<std::ffi::c_void>(),
)
};
if ret == 0 {
continue;
}
}
let status = format_wait_status(host_pid, status);
eprintln!("LIVE_CHROMIUM_RESTORED_TRACE_STOP pid={host_pid} status={status}");
statuses.insert(host_pid, Some(status));
pending.remove(&host_pid);
}
if !progressed {
std::thread::sleep(std::time::Duration::from_millis(1));
}
}
for host_pid in pending {
statuses.insert(host_pid, None);
}
statuses
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn trace_restored_syscall_stop(
host_pid: i32,
syscall_entries: &mut BTreeMap<i32, Option<RestoredSyscallEntry>>,
) {
let Some(regs) = ptrace_getregs_for_log(host_pid) else {
eprintln!("LIVE_CHROMIUM_RESTORED_SYSCALL pid={host_pid} regs=unavailable");
return;
};
let current = syscall_entries.entry(host_pid).or_insert(None);
if let Some(entry) = current.take() {
if watched_restored_syscall(entry.nr) || watched_restored_fd_result(regs.rax as i64) {
eprintln!(
"LIVE_CHROMIUM_RESTORED_SYSCALL_EXIT pid={host_pid} nr={} name={} args={:?} ret={}",
entry.nr,
restored_syscall_name(entry.nr),
entry.args,
regs.rax as i64,
);
}
} else {
let entry = RestoredSyscallEntry {
nr: regs.orig_rax,
args: [regs.rdi, regs.rsi, regs.rdx, regs.r10, regs.r8, regs.r9],
};
if watched_restored_syscall(entry.nr) {
eprintln!(
"LIVE_CHROMIUM_RESTORED_SYSCALL_ENTER pid={host_pid} nr={} name={} args={:?}",
entry.nr,
restored_syscall_name(entry.nr),
entry.args,
);
}
*current = Some(entry);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn watched_restored_fd_result(ret: i64) -> bool {
(3..=16).contains(&ret) || ret < 0
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn watched_restored_syscall(nr: u64) -> bool {
matches!(
nr,
2 | 3 | 32 | 33 | 41 | 42 | 43 | 53 | 72 | 257 | 288 | 292
)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn restored_syscall_name(nr: u64) -> &'static str {
match nr {
2 => "open",
3 => "close",
32 => "dup",
33 => "dup2",
41 => "socket",
42 => "connect",
43 => "accept",
53 => "socketpair",
72 => "fcntl",
257 => "openat",
288 => "accept4",
292 => "dup3",
_ => "other",
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn ptrace_getregs_for_log(host_pid: i32) -> Option<libc::user_regs_struct> {
let mut regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
let ret = unsafe {
libc::ptrace(
libc::PTRACE_GETREGS,
host_pid,
std::ptr::null_mut::<std::ffi::c_void>(),
&mut regs as *mut _ as *mut std::ffi::c_void,
)
};
(ret == 0).then_some(regs)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn format_wait_status(host_pid: i32, status: i32) -> String {
if libc::WIFEXITED(status) {
return format!("exit({}) raw={status}", libc::WEXITSTATUS(status));
}
if libc::WIFSIGNALED(status) {
return format!(
"signal({}) core={} raw={status}",
libc::WTERMSIG(status),
libc::WCOREDUMP(status),
);
}
if libc::WIFSTOPPED(status) {
return format!(
"stopped({}) event={} raw={status} {}",
libc::WSTOPSIG(status),
status >> 16,
ptrace_stop_details(host_pid),
);
}
format!("unknown raw={status}")
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn should_continue_restored_sigtrap(status: i32) -> bool {
std::env::var_os("SUPERMACHINE_KBOXLIKE_CONTINUE_RESTORED_SIGTRAP").is_some()
&& libc::WIFSTOPPED(status)
&& libc::WSTOPSIG(status) == libc::SIGTRAP
&& (status >> 16) == 0
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn assert_live_ptrace_exec_exits_zero(argv: &[&str], max_stops: usize) {
let summary = run_live_ptrace_exec(argv, max_stops)
.unwrap_or_else(|err| panic!("live ptrace dispatch failed for {argv:?}: {err:?}"));
assert!(summary.dispatched > 0);
assert!(matches!(
summary.terminal,
Some(LinuxPtraceDispatchOutcome::ProcessExited { code: 0, .. })
));
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission and a Linux x86_64 tracee"]
fn live_ptrace_dispatch_runs_bin_true_to_exit() {
assert_live_ptrace_exec_exits_zero(&["/bin/true"], 2048);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission, Linux x86_64, and /usr/local/bin/node"]
fn live_node_sigstop_snapshot_captures_process_tree() {
let script =
"setTimeout(()=>process.kill(process.pid,'SIGSTOP'),25);setInterval(()=>{},1000)";
let (child, snapshot) = capture_live_ptrace_exec_snapshot_on_sigstop(
&["/usr/local/bin/node", "-e", script],
2,
50000,
)
.unwrap();
let result: Result<(), FdError> = {
assert!(
snapshot.threads.len() >= 2,
"expected Node to register at least two tracees, got {}",
snapshot.threads.len()
);
assert_eq!(snapshot.thread_kernel_states.len(), snapshot.threads.len());
assert_eq!(snapshot.memory_maps.len(), snapshot.threads.len());
assert!(snapshot
.threads
.iter()
.any(|thread| thread.host_pid == child));
assert!(snapshot
.threads
.iter()
.all(|thread| thread.regs.fs_base != 0));
assert!(snapshot
.thread_kernel_states
.iter()
.any(|state| state.clear_child_tid.is_some()));
assert!(snapshot
.thread_kernel_states
.iter()
.any(|state| state.robust_list_head.is_some()));
assert!(snapshot.memory_maps.iter().any(|tracee_maps| {
tracee_maps.maps.iter().any(|entry| {
entry
.path
.as_deref()
.is_some_and(|path| path.contains("/node"))
})
}));
assert!(snapshot
.memory_maps
.iter()
.any(|tracee_maps| tracee_maps.dump_candidate_bytes > 0));
Ok(())
};
kill_and_reap_tracee(child);
result.unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission, Linux x86_64, and kernel fd objects"]
fn live_kernel_object_snapshot_inventories_chromium_like_fds() {
let (child, snapshot) =
capture_live_source_snapshot_at_pause(live_kernel_object_source_child_main, 1).unwrap();
let mut replacement_factory = LinuxStoppedTraceeReplacementFactory::default();
let result: Result<(), FdError> = (|| {
assert_eq!(snapshot.threads.len(), 1);
assert_eq!(snapshot.fd_tables.len(), 1);
let table = snapshot
.fd_tables
.iter()
.find(|table| table.host_pid == child)
.ok_or(FdError::TraceeInstallFailed(
"missing kernel-object fd table",
))?;
let mut counts = std::collections::BTreeMap::<KboxlikeFdTargetKind, usize>::new();
for fd in &table.fds {
*counts.entry(fd.kind).or_default() += 1;
}
let pipe = *counts.get(&KboxlikeFdTargetKind::Pipe).unwrap_or(&0);
let socket = *counts.get(&KboxlikeFdTargetKind::Socket).unwrap_or(&0);
let eventfd = *counts.get(&KboxlikeFdTargetKind::EventFd).unwrap_or(&0);
let eventpoll = *counts.get(&KboxlikeFdTargetKind::EventPoll).unwrap_or(&0);
let timerfd = *counts.get(&KboxlikeFdTargetKind::TimerFd).unwrap_or(&0);
let memfd = *counts.get(&KboxlikeFdTargetKind::Memfd).unwrap_or(&0);
let cloexec = table.fds.iter().filter(|fd| fd.cloexec).count();
let pipe_plans = plan_pipe_shadow_backings(&snapshot.fd_tables);
let socketpair_plans = plan_socketpair_shadow_backings(&snapshot.fd_tables);
let timerfd_plans = plan_timerfd_shadow_backings(&snapshot.fd_tables);
let epoll_interests = table
.fds
.iter()
.map(|fd| fd.epoll_interests.len())
.sum::<usize>();
let targets = table
.fds
.iter()
.map(|fd| format!("{}={}", fd.fd, fd.target))
.collect::<Vec<_>>()
.join(",");
eprintln!(
"LIVE_KERNEL_OBJECT_FD_INVENTORY tracees={} fd_tables={} fds={} pipe={} socket={} eventfd={} eventpoll={} timerfd={} memfd={} cloexec={} pipe_plans={} socketpair_plans={} timerfd_plans={} epoll_interests={} targets={}",
snapshot.threads.len(),
snapshot.fd_tables.len(),
table.fds.len(),
pipe,
socket,
eventfd,
eventpoll,
timerfd,
memfd,
cloexec,
pipe_plans.len(),
socketpair_plans.len(),
timerfd_plans.len(),
epoll_interests,
targets,
);
assert!(pipe >= 2, "expected pipe fds in {targets}");
assert!(socket >= 2, "expected socketpair fds in {targets}");
assert!(eventfd >= 1, "expected eventfd in {targets}");
assert!(eventpoll >= 1, "expected epoll fd in {targets}");
assert!(timerfd >= 1, "expected timerfd in {targets}");
assert!(memfd >= 1, "expected memfd in {targets}");
assert!(cloexec >= 6, "expected cloexec state in {targets}");
assert!(snapshot.memory_maps.iter().any(|tracee_maps| {
tracee_maps.maps.iter().any(|entry| {
entry
.path
.as_deref()
.is_some_and(|path| path.contains("memfd:kboxlike-kernel-objects"))
})
}));
let mut fd_system = KboxlikeFdSystem::new();
let model_pid = fd_system.spawn_init();
assert_eq!(model_pid, table.model_pid);
let mut watched_fd_plans = pipe_plans.clone();
watched_fd_plans.extend(plan_eventfd_shadow_backings(&snapshot.fd_tables));
watched_fd_plans.extend(timerfd_plans.clone());
watched_fd_plans.extend(socketpair_plans.clone());
assert_eq!(
apply_fd_shadow_backing_plans(&mut fd_system, &watched_fd_plans)?,
watched_fd_plans.len()
);
assert_eq!(
timerfd_plans.len(),
1,
"expected one restorable timerfd plan in {targets}"
);
assert_eq!(
socketpair_plans.len(),
2,
"expected one restorable socketpair in {targets}"
);
assert!(
pipe_plans.len() >= 2,
"expected at least one restorable pipe pair in {targets}"
);
fd_system.set_socket_shutdown(model_pid, socketpair_plans[0].fd, libc::SHUT_WR)?;
let mut planned_by_backing = std::collections::BTreeMap::<String, Vec<i32>>::new();
for plan in &pipe_plans {
if let ShadowBacking::HostPassthrough { label } = &plan.backing {
planned_by_backing
.entry(label.clone())
.or_default()
.push(plan.fd);
}
}
let duplicate_pipe_plan_fds = planned_by_backing
.values()
.find(|fds| fds.len() >= 2)
.ok_or(FdError::TraceeInstallFailed(
"expected duplicated pipe endpoint plan",
))?;
let first_duplicate = fd_system
.descriptor(model_pid, duplicate_pipe_plan_fds[0])?
.open_file;
for fd in duplicate_pipe_plan_fds {
assert_eq!(
fd_system.descriptor(model_pid, *fd)?.open_file,
first_duplicate,
"duplicated pipe endpoint fd {fd} did not share open-file identity"
);
}
for plan in &pipe_plans {
assert!(matches!(
fd_system.route_fd_operation(model_pid, plan.fd, FdOperation::Generic),
Ok(FdRoute::HostPassthrough { shadow })
if shadow.kind == ShadowKind::HostPassthrough
));
}
let queued_data_plans =
plan_fd_queued_data_replay(&snapshot.fd_tables, &fd_system, 4096);
assert!(
queued_data_plans.iter().any(|plan| plan.bytes == b"pipeq"),
"expected queued pipe bytes in {queued_data_plans:?}"
);
assert!(
queued_data_plans.iter().any(|plan| plan.bytes == b"sockq"),
"expected queued socketpair bytes in {queued_data_plans:?}"
);
let socket_options_plans =
plan_fd_socket_options_replay(&snapshot.fd_tables, &fd_system);
let socket_shutdown_plans =
plan_fd_socket_shutdown_replay(&snapshot.fd_tables, &fd_system);
let socket_write_shutdown_plan = socket_shutdown_plans
.iter()
.find(|plan| plan.shutdown.write_closed)
.ok_or(FdError::TraceeInstallFailed(
"missing captured socket write shutdown",
))?
.clone();
let passcred_plan = socket_options_plans
.iter()
.find(|plan| {
plan.int_options.contains(&KboxlikeSocketIntOption {
level: libc::SOL_SOCKET,
name: libc::SO_PASSCRED,
value: 1,
})
})
.ok_or(FdError::TraceeInstallFailed(
"missing captured SO_PASSCRED socket option",
))?
.clone();
let scm_rights_plans =
plan_fd_scm_rights_replay(&snapshot.fd_tables, &fd_system, 16, 4);
let scm_rights_plan = scm_rights_plans
.iter()
.find(|plan| plan.data == b"r" && plan.passed_fds.len() == 1)
.ok_or(FdError::TraceeInstallFailed(
"missing captured SCM_RIGHTS message",
))?
.clone();
let scm_credentials =
scm_rights_plan
.credentials
.clone()
.ok_or(FdError::TraceeInstallFailed(
"missing captured SCM_CREDENTIALS",
))?;
assert_eq!(scm_credentials.pid, child);
assert_eq!(scm_credentials.uid, unsafe { libc::getuid() });
assert_eq!(scm_credentials.gid, unsafe { libc::getgid() });
assert_eq!(
scm_rights_plan
.credential_identity
.map(|identity| identity.host_pid),
Some(child)
);
let scm_credential_identity =
scm_rights_plan
.credential_identity
.ok_or(FdError::TraceeInstallFailed(
"missing captured SCM_CREDENTIALS identity",
))?;
let epoll_plans = plan_epoll_shadow_backings(&snapshot.fd_tables, &fd_system);
assert_eq!(epoll_plans.len(), 1, "expected one epoll plan in {targets}");
assert!(
epoll_plans[0].interests.len() >= 2,
"expected eventfd and pipe epoll interests in {targets}"
);
assert!(epoll_plans[0].interests.iter().any(|interest| {
interest.target_kind == Some(KboxlikeFdTargetKind::EventFd)
&& matches!(
interest.target_backing,
Some(ShadowBacking::HostPassthrough { ref label })
if label.starts_with("eventfd:")
)
}));
assert!(epoll_plans[0].interests.iter().any(|interest| {
interest.target_kind == Some(KboxlikeFdTargetKind::Pipe)
&& matches!(
interest.target_backing,
Some(ShadowBacking::HostPassthrough { ref label })
if label.starts_with("pipe:")
)
}));
assert!(epoll_plans[0].interests.iter().any(|interest| {
interest.target_kind == Some(KboxlikeFdTargetKind::TimerFd)
&& matches!(
interest.target_backing,
Some(ShadowBacking::HostPassthrough { ref label })
if label.starts_with("timerfd:")
)
}));
let epoll_fd_plans = epoll_plans
.iter()
.map(|plan| plan.fd_plan.clone())
.collect::<Vec<_>>();
assert_eq!(
apply_fd_shadow_backing_plans(&mut fd_system, &epoll_fd_plans)?,
epoll_fd_plans.len()
);
assert!(matches!(
fd_system.route_fd_operation(model_pid, epoll_plans[0].fd_plan.fd, FdOperation::Generic),
Ok(FdRoute::HostPassthrough { shadow })
if shadow.kind == ShadowKind::HostPassthrough
));
let eventfd_interest = epoll_plans[0]
.interests
.iter()
.find(|interest| interest.target_kind == Some(KboxlikeFdTargetKind::EventFd))
.ok_or(FdError::TraceeInstallFailed(
"missing eventfd epoll interest",
))?
.clone();
let memory_restore = KboxlikeMaterializedMemoryRestore {
actions: Vec::new(),
};
let syscall_page = choose_restore_syscall_page(&memory_restore)?;
let mut rebuilder = HostShadowRebuilder::new(std::env::temp_dir());
let mut access_factory = ScratchTraceeRestoreAccessFactory {
syscall_page,
clear_existing_mappings: false,
cleared_pids: BTreeSet::new(),
};
let mut resumer = PtraceDetachRestoredTraceeResumer;
let restore_result = restore_stopped_kboxlike_snapshot_with_replacements(
fd_system.snapshot(),
KboxlikeTraceeRestoreInputs {
memory_restore: &memory_restore,
threads: &snapshot.threads,
kernel_states: &snapshot.thread_kernel_states,
shared_dumps: &[],
supervisor_pid: std::process::id() as i32,
},
KboxlikeFdKernelReplayInputs {
epoll_plans: &epoll_plans,
socket_options_plans: &socket_options_plans,
queued_data_plans: &queued_data_plans,
socket_shutdown_plans: &socket_shutdown_plans,
scm_rights_plans: &scm_rights_plans,
},
&mut rebuilder,
&mut replacement_factory,
&mut access_factory,
&mut resumer,
)?;
let replacement_credential_pid = *restore_result
.replacement_host_pids
.get(&scm_credential_identity.model_thread_id)
.ok_or(FdError::TraceeInstallFailed(
"missing replacement SCM_CREDENTIALS pid",
))?;
assert_eq!(restore_result.summary.replacements.tracees_created, 1);
assert_eq!(restore_result.summary.tracee.tracees_restored, 1);
assert_eq!(
restore_result.summary.tracee.registers.registers_restored,
1
);
assert_eq!(restore_result.summary.resume.tracees_resumed, 1);
assert_eq!(
restore_result.summary.fd_kernel,
KboxlikeFdKernelReplaySummary {
epoll_interests: epoll_plans[0].interests.len(),
socket_options: socket_options_plans
.iter()
.map(|plan| plan.int_options.len())
.sum::<usize>(),
queued_bytes: queued_data_plans
.iter()
.map(|plan| plan.bytes.len())
.sum::<usize>(),
socket_shutdowns: socket_shutdown_plans.len(),
scm_rights_messages: scm_rights_plans.len(),
}
);
let rebuilt = &restore_result.fd_system;
let rebuilt_epoll_shadow = rebuilt
.descriptor(model_pid, epoll_plans[0].fd_plan.fd)?
.shadow
.ok_or(FdError::NoBackingFd(epoll_plans[0].fd_plan.fd))?;
let epoll_handle = rebuilder.dup_handle(rebuilt_epoll_shadow.supervisor_fd)?;
let rebuilt_timer_shadow = rebuilt
.descriptor(model_pid, timerfd_plans[0].fd)?
.shadow
.ok_or(FdError::NoBackingFd(timerfd_plans[0].fd))?;
let timer_handle = rebuilder.dup_handle(rebuilt_timer_shadow.supervisor_fd)?;
let passcred_shadow = rebuilt
.descriptor(model_pid, passcred_plan.fd)?
.shadow
.ok_or(FdError::NoBackingFd(passcred_plan.fd))?;
let passcred_handle = rebuilder.dup_handle(passcred_shadow.supervisor_fd)?;
assert_eq!(
read_socket_int_option(
passcred_handle.as_raw_fd(),
libc::SOL_SOCKET,
libc::SO_PASSCRED
)?,
1
);
for plan in &queued_data_plans {
let receiver_shadow = rebuilt
.descriptor(model_pid, plan.receiver_fd)?
.shadow
.ok_or(FdError::NoBackingFd(plan.receiver_fd))?;
let receiver = rebuilder.dup_handle(receiver_shadow.supervisor_fd)?;
let mut queued_buf = vec![0u8; plan.bytes.len()];
let queued_read = unsafe {
libc::read(
receiver.as_raw_fd(),
queued_buf.as_mut_ptr() as *mut libc::c_void,
queued_buf.len(),
)
};
assert_eq!(queued_read, queued_buf.len() as isize);
assert_eq!(queued_buf, plan.bytes);
}
let shutdown_peer_plan = queued_data_plans
.iter()
.find(|plan| plan.injector_fd == socket_write_shutdown_plan.fd)
.ok_or(FdError::TraceeInstallFailed(
"missing queued data peer for socket shutdown",
))?;
let shutdown_peer_shadow = rebuilt
.descriptor(model_pid, shutdown_peer_plan.receiver_fd)?
.shadow
.ok_or(FdError::NoBackingFd(shutdown_peer_plan.receiver_fd))?;
let shutdown_peer = rebuilder.dup_handle(shutdown_peer_shadow.supervisor_fd)?;
let mut eof_buf = [0u8; 1];
let eof_read = unsafe {
libc::read(
shutdown_peer.as_raw_fd(),
eof_buf.as_mut_ptr() as *mut libc::c_void,
eof_buf.len(),
)
};
assert_eq!(
eof_read, 0,
"expected socket peer EOF after replayed shutdown"
);
let mut timer_state = libc::itimerspec {
it_interval: libc::timespec {
tv_sec: 0,
tv_nsec: 0,
},
it_value: libc::timespec {
tv_sec: 0,
tv_nsec: 0,
},
};
if unsafe { libc::timerfd_gettime(timer_handle.as_raw_fd(), &mut timer_state) } < 0 {
return Err(FdError::TraceeInstallFailed(
"failed to read rebuilt timerfd state",
));
}
assert_eq!(timer_state.it_interval.tv_sec, 2);
assert_eq!(timer_state.it_interval.tv_nsec, 123_456_789);
assert!(
timer_state.it_value.tv_sec > 0 || timer_state.it_value.tv_nsec > 0,
"expected rebuilt timerfd to remain armed"
);
let mut events = [libc::epoll_event { events: 0, u64: 0 }; 4];
let ready =
unsafe { libc::epoll_wait(epoll_handle.as_raw_fd(), events.as_mut_ptr(), 4, 0) };
assert!(
ready > 0,
"expected rebuilt epoll to report eventfd readiness"
);
let eventfd_data_ready = events[..ready as usize].iter().any(|event| {
let data = unsafe { std::ptr::addr_of!(event.u64).read_unaligned() };
data == eventfd_interest.data
});
assert!(
eventfd_data_ready,
"rebuilt epoll did not return captured eventfd data"
);
let scm_receiver_shadow = rebuilt
.descriptor(model_pid, scm_rights_plan.receiver_fd)?
.shadow
.ok_or(FdError::NoBackingFd(scm_rights_plan.receiver_fd))?;
let scm_receiver = rebuilder.dup_handle(scm_receiver_shadow.supervisor_fd)?;
let (scm_data, passed_fd, replayed_credentials) =
recv_one_scm_right(scm_receiver.as_raw_fd(), scm_rights_plan.data.len())?;
assert_eq!(scm_data, scm_rights_plan.data);
assert_eq!(
replayed_credentials,
Some(KboxlikeUnixCredentials {
pid: replacement_credential_pid,
uid: scm_credentials.uid,
gid: scm_credentials.gid,
})
);
let mut passed_eventfd_value = 0u64;
let passed_eventfd_read = unsafe {
libc::read(
passed_fd.as_raw_fd(),
&mut passed_eventfd_value as *mut u64 as *mut libc::c_void,
std::mem::size_of::<u64>(),
)
};
assert_eq!(passed_eventfd_read, std::mem::size_of::<u64>() as isize);
assert_eq!(passed_eventfd_value, 7);
Ok(())
})();
replacement_factory.kill_created_tracees();
kill_and_reap_tracee(child);
result.unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn read_socket_int_option(fd: i32, level: i32, name: i32) -> Result<i32, FdError> {
let mut value = 0 as libc::c_int;
let mut len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
let rc = unsafe {
libc::getsockopt(
fd,
level,
name,
&mut value as *mut libc::c_int as *mut libc::c_void,
&mut len,
)
};
if rc < 0 || len as usize != std::mem::size_of::<libc::c_int>() {
return Err(FdError::TraceeInstallFailed(
"failed to read rebuilt socket option",
));
}
Ok(value)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn recv_one_scm_right(
fd: i32,
len: usize,
) -> Result<
(
Vec<u8>,
std::os::fd::OwnedFd,
Option<KboxlikeUnixCredentials>,
),
FdError,
> {
let mut data = vec![0u8; len];
let mut iov = libc::iovec {
iov_base: data.as_mut_ptr() as *mut libc::c_void,
iov_len: data.len(),
};
let mut control = vec![
0u8;
unsafe {
libc::CMSG_SPACE(std::mem::size_of::<libc::c_int>() as u32)
+ libc::CMSG_SPACE(std::mem::size_of::<libc::ucred>() as u32)
} as usize
];
let mut msg = libc::msghdr {
msg_name: std::ptr::null_mut(),
msg_namelen: 0,
msg_iov: &mut iov,
msg_iovlen: 1,
msg_control: control.as_mut_ptr() as *mut libc::c_void,
msg_controllen: control.len(),
msg_flags: 0,
};
let read = unsafe { libc::recvmsg(fd, &mut msg, libc::MSG_DONTWAIT) };
if read != len as isize {
return Err(FdError::TraceeInstallFailed(
"failed to receive SCM_RIGHTS payload",
));
}
data.truncate(read as usize);
let mut passed = None;
let mut credentials = None;
let mut cmsg = unsafe { libc::CMSG_FIRSTHDR(&msg) };
while !cmsg.is_null() {
let header = unsafe { &*cmsg };
if header.cmsg_level == libc::SOL_SOCKET && header.cmsg_type == libc::SCM_RIGHTS {
passed =
Some(unsafe { (libc::CMSG_DATA(cmsg) as *const libc::c_int).read_unaligned() });
} else if header.cmsg_level == libc::SOL_SOCKET
&& header.cmsg_type == libc::SCM_CREDENTIALS
{
let creds =
unsafe { (libc::CMSG_DATA(cmsg) as *const libc::ucred).read_unaligned() };
credentials = Some(KboxlikeUnixCredentials {
pid: creds.pid,
uid: creds.uid,
gid: creds.gid,
});
}
cmsg = unsafe { libc::CMSG_NXTHDR(&msg, cmsg) };
}
let passed = passed.ok_or(FdError::TraceeInstallFailed("missing SCM_RIGHTS cmsg"))?;
if passed < 0 {
return Err(FdError::TraceeInstallFailed("invalid SCM_RIGHTS fd"));
}
Ok((
data,
unsafe { std::os::fd::OwnedFd::from_raw_fd(passed) },
credentials,
))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn byte_backed_memory_action_key(
action: &KboxlikeMemoryRestoreAction,
) -> Option<(i32, ProcessId, u64)> {
match action {
KboxlikeMemoryRestoreAction::DumpAnonymousPrivate {
host_pid,
model_pid,
start,
..
}
| KboxlikeMemoryRestoreAction::DumpPrivateFileCow {
host_pid,
model_pid,
start,
..
} => Some((*host_pid, *model_pid, *start)),
_ => None,
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn filter_byte_backed_memory_dumps(
plan_actions: &[KboxlikeMemoryRestoreAction],
dumps: &KboxlikeMemoryDumpCapture,
) -> KboxlikeMemoryDumpCapture {
let required = plan_actions
.iter()
.filter_map(byte_backed_memory_action_key)
.collect::<BTreeSet<_>>();
KboxlikeMemoryDumpCapture {
dumps: dumps
.dumps
.iter()
.filter(|dump| required.contains(&(dump.host_pid, dump.model_pid, dump.start)))
.cloned()
.collect(),
failures: dumps
.failures
.iter()
.filter(|failure| {
required.contains(&(failure.host_pid, failure.model_pid, failure.start))
})
.cloned()
.collect(),
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn materialized_memory_action_range(
action: &KboxlikeMaterializedMemoryRestoreAction,
) -> Option<(u64, u64)> {
match action {
KboxlikeMaterializedMemoryRestoreAction::ByteBacked { start, bytes, .. } => {
Some((*start, bytes.len() as u64))
}
KboxlikeMaterializedMemoryRestoreAction::RemapCleanFile { start, len, .. }
| KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject {
start, len, ..
}
| KboxlikeMaterializedMemoryRestoreAction::SkipKernelSynthetic { start, len, .. }
| KboxlikeMaterializedMemoryRestoreAction::SkipInaccessible { start, len, .. } => {
Some((*start, *len))
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn ranges_overlap(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 choose_restore_syscall_page(
restore: &KboxlikeMaterializedMemoryRestore,
) -> Result<u64, FdError> {
const PAGE_LEN: u64 = 8192;
const CANDIDATES: &[u64] = &[
0x7000_0000_0000,
0x6800_0000_0000,
0x6000_0000_0000,
0x5800_0000_0000,
0x5000_0000_0000,
];
CANDIDATES
.iter()
.copied()
.find(|candidate| {
restore.actions.iter().all(|action| {
materialized_memory_action_range(action).is_none_or(|(start, len)| {
!ranges_overlap(*candidate, PAGE_LEN, start, len)
})
})
})
.ok_or(FdError::TraceeInstallFailed(
"no scratch syscall page outside restore ranges",
))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn set_nonblocking_fd(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(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn read_exact_tracee_memory(pid: i32, addr: u64, bytes: &mut [u8]) -> Result<(), FdError> {
let local = libc::iovec {
iov_base: bytes.as_mut_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 read = unsafe { libc::process_vm_readv(pid, &local, 1, &remote, 1, 0) };
if read != bytes.len() as isize {
return Err(FdError::TraceeInstallFailed("process_vm_readv failed"));
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn read_i32_from_tracee_memory(pid: i32, addr: u64) -> Result<i32, FdError> {
let mut bytes = [0u8; std::mem::size_of::<i32>()];
read_exact_tracee_memory(pid, addr, &mut bytes)?;
Ok(i32::from_ne_bytes(bytes))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn robust_list_for_tid(tid: i32) -> Result<(u64, u64), FdError> {
let mut head: *mut libc::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))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn read_pipe_bytes(fd: i32, out: &mut [u8]) -> Result<(), FdError> {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
let mut offset = 0;
while std::time::Instant::now() <= deadline {
let read = unsafe {
libc::read(
fd,
out[offset..].as_mut_ptr() as *mut libc::c_void,
out.len() - offset,
)
};
if read > 0 {
offset += read as usize;
if offset == out.len() {
return Ok(());
}
} else if read < 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));
}
Err(FdError::TraceeInstallFailed(
"restored controlled path did not write expected pipe bytes",
))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn wait_for_tracee_i32_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_memory(pid, addr)? == 0 {
return Ok(());
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
Err(FdError::TraceeInstallFailed(
"captured clear_child_tid was not cleared after replacement thread exit",
))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
struct ScratchTraceeRestoreAccessFactory {
syscall_page: u64,
clear_existing_mappings: bool,
cleared_pids: BTreeSet<i32>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeRestoreAccessFactory for ScratchTraceeRestoreAccessFactory {
type Writer = ProcessVmMemoryWriter;
type Executor = PtraceScratchSyscallExecutor;
type RegisterWriter = PtraceRegisterWriter;
fn access_for_restore(
&mut self,
host_pid: i32,
) -> Result<TraceeRestoreAccess<Self::Writer, Self::Executor, Self::RegisterWriter>, FdError>
{
let mut executor = PtraceScratchSyscallExecutor::new(host_pid, self.syscall_page)?;
if self.clear_existing_mappings && self.cleared_pids.insert(host_pid) {
let unmapped = executor.unmap_existing_mappings_except_scratch()?;
if std::env::var_os("SUPERMACHINE_KBOXLIKE_TRACE_MEMORY_RESTORE").is_some() {
eprintln!(
"KBOXLIVE_REPLACEMENT_CLEARED pid={} unmapped={unmapped}",
host_pid,
);
}
}
Ok(TraceeRestoreAccess {
writer: ProcessVmMemoryWriter::new(host_pid)?,
executor,
register_writer: PtraceRegisterWriter::new(host_pid)?,
})
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission, Linux x86_64, and /usr/local/bin/node"]
fn live_node_sigstop_snapshot_classifies_memory_restore_plan() {
let script =
"setTimeout(()=>process.kill(process.pid,'SIGSTOP'),25);setInterval(()=>{},1000)";
let (child, snapshot) = capture_live_ptrace_exec_snapshot_on_sigstop(
&["/usr/local/bin/node", "-e", script],
2,
50000,
)
.unwrap();
let result: Result<(), FdError> = {
let mut seen_model_pids = BTreeSet::new();
let unique_memory_maps = snapshot
.memory_maps
.iter()
.filter(|tracee_maps| seen_model_pids.insert(tracee_maps.model_pid))
.cloned()
.collect::<Vec<_>>();
let duplicate_map_sets = snapshot.memory_maps.len() - unique_memory_maps.len();
let plan = classify_memory_restore_actions(&unique_memory_maps);
let mut dump_anonymous_private = 0usize;
let mut dump_private_file_cow = 0usize;
let mut remap_clean_file = 0usize;
let mut recreate_shared_object = 0usize;
let mut skip_kernel_synthetic = 0usize;
let mut skip_inaccessible = 0usize;
let mut byte_backed_bytes = 0u64;
let mut shared_bytes = 0u64;
for action in &plan.actions {
match action {
KboxlikeMemoryRestoreAction::DumpAnonymousPrivate { len, .. } => {
dump_anonymous_private += 1;
byte_backed_bytes += *len;
}
KboxlikeMemoryRestoreAction::DumpPrivateFileCow { len, .. } => {
dump_private_file_cow += 1;
byte_backed_bytes += *len;
}
KboxlikeMemoryRestoreAction::RemapCleanFile { .. } => {
remap_clean_file += 1;
}
KboxlikeMemoryRestoreAction::RecreateSharedObject { len, .. } => {
recreate_shared_object += 1;
shared_bytes += *len;
}
KboxlikeMemoryRestoreAction::SkipKernelSynthetic { .. } => {
skip_kernel_synthetic += 1;
}
KboxlikeMemoryRestoreAction::SkipInaccessible { .. } => {
skip_inaccessible += 1;
}
}
}
let raw_dumps = capture_dump_candidate_memory(&KboxlikeStoppedTraceeSnapshot {
threads: Vec::new(),
thread_kernel_states: Vec::new(),
memory_maps: unique_memory_maps,
fd_tables: Vec::new(),
});
let byte_dumps = filter_byte_backed_memory_dumps(&plan.actions, &raw_dumps);
let shared_dumps = capture_shared_object_memory(&plan);
let materialized = materialize_memory_restore_actions(&plan, &byte_dumps).unwrap();
eprintln!(
"LIVE_NODE_MEMORY_PLAN tracees={} duplicate_map_sets={} actions={} dump_anon={} dump_cow={} remap_clean={} shared={} skip_kernel={} skip_inaccessible={} byte_action_bytes={} byte_dump_bytes={} byte_failures={} byte_failed_bytes={} raw_dump_bytes={} raw_failures={} shared_bytes={} shared_dump_bytes={} shared_failures={} shared_failed_bytes={} shared_duplicate_extents={} materialized_actions={}",
snapshot.threads.len(),
duplicate_map_sets,
plan.actions.len(),
dump_anonymous_private,
dump_private_file_cow,
remap_clean_file,
recreate_shared_object,
skip_kernel_synthetic,
skip_inaccessible,
byte_backed_bytes,
byte_dumps.captured_bytes(),
byte_dumps.failures.len(),
byte_dumps.failed_bytes(),
raw_dumps.captured_bytes(),
raw_dumps.failures.len(),
shared_bytes,
shared_dumps.captured_bytes(),
shared_dumps.failures.len(),
shared_dumps.failed_bytes(),
shared_dumps.skipped_duplicate_extents,
materialized.actions.len(),
);
assert!(duplicate_map_sets > 0);
assert!(dump_anonymous_private > 0);
assert!(dump_private_file_cow > 0);
assert!(remap_clean_file > 0);
assert!(skip_kernel_synthetic > 0);
assert_eq!(byte_dumps.failures, Vec::new());
assert_eq!(shared_dumps.failures, Vec::new());
assert_eq!(byte_dumps.captured_bytes(), byte_backed_bytes);
assert_eq!(materialized.actions.len(), plan.actions.len());
Ok(())
};
kill_and_reap_tracee(child);
result.unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission, Linux x86_64, and /usr/local/bin/node"]
fn live_node_sigstop_restore_applies_memory_to_replacement_tracees() {
let script =
"setTimeout(()=>process.kill(process.pid,'SIGSTOP'),25);setInterval(()=>{},1000)";
let (child, snapshot) = capture_live_ptrace_exec_snapshot_on_sigstop(
&["/usr/local/bin/node", "-e", script],
2,
50000,
)
.unwrap();
let mut replacement_factory = LinuxStoppedTraceeReplacementFactory::default();
let result: Result<(), FdError> = (|| {
let mut seen_model_pids = BTreeSet::new();
let unique_memory_maps = snapshot
.memory_maps
.iter()
.filter(|tracee_maps| seen_model_pids.insert(tracee_maps.model_pid))
.cloned()
.collect::<Vec<_>>();
let plan = classify_memory_restore_actions(&unique_memory_maps);
let raw_dumps = capture_dump_candidate_memory(&KboxlikeStoppedTraceeSnapshot {
threads: Vec::new(),
thread_kernel_states: Vec::new(),
memory_maps: unique_memory_maps,
fd_tables: Vec::new(),
});
let byte_dumps = filter_byte_backed_memory_dumps(&plan.actions, &raw_dumps);
let shared_dump_capture = capture_shared_object_memory(&plan);
if !byte_dumps.failures.is_empty() || !shared_dump_capture.failures.is_empty() {
return Err(FdError::TraceeInstallFailed(
"live Node memory capture had restore-required failures",
));
}
let materialized = materialize_memory_restore_actions(&plan, &byte_dumps).unwrap();
let syscall_page = choose_restore_syscall_page(&materialized)?;
let byte_backed_actions = materialized
.actions
.iter()
.filter(|action| {
matches!(
action,
KboxlikeMaterializedMemoryRestoreAction::ByteBacked { .. }
)
})
.count();
let clean_file_actions = materialized
.actions
.iter()
.filter(|action| {
matches!(
action,
KboxlikeMaterializedMemoryRestoreAction::RemapCleanFile { .. }
)
})
.count();
let shared_actions = materialized
.actions
.iter()
.filter(|action| {
matches!(
action,
KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject { .. }
)
})
.count();
let skipped_actions = materialized
.actions
.iter()
.filter(|action| {
matches!(
action,
KboxlikeMaterializedMemoryRestoreAction::SkipKernelSynthetic { .. }
| KboxlikeMaterializedMemoryRestoreAction::SkipInaccessible { .. }
)
})
.count();
let created =
create_stopped_replacement_tracee_set(&snapshot.threads, &mut replacement_factory)?;
let remapped = remap_stopped_tracee_restore_set_to_replacement_pids(
&materialized,
&snapshot.threads,
&shared_dump_capture.dumps,
&created.replacement_host_pids,
)?;
let mut access_factory = ScratchTraceeRestoreAccessFactory {
syscall_page,
clear_existing_mappings: false,
cleared_pids: BTreeSet::new(),
};
let restore_summary = apply_stopped_tracee_restore_set(
&remapped.restore,
&remapped.threads,
&remapped.shared_dumps,
std::process::id() as i32,
&mut access_factory,
)?;
eprintln!(
"LIVE_NODE_MEMORY_APPLY source_tracees={} replacements={} syscall_page=0x{:x} remapped_threads={} remapped_actions={} byte_backed={} clean_file={} shared={} skipped={} tracees_restored={} memory_byte={} memory_clean={} memory_shared={} memory_skipped={} memory_deferred={} memory_other={} registers_restored={} register_other={} shared_created={} shared_initialized={} shared_unsupported={}",
snapshot.threads.len(),
created.summary.tracees_created,
syscall_page,
remapped.summary.threads_remapped,
remapped.summary.memory_actions_remapped,
byte_backed_actions,
clean_file_actions,
shared_actions,
skipped_actions,
restore_summary.tracees_restored,
restore_summary.memory.byte_backed_applied,
restore_summary.memory.clean_file_remapped,
restore_summary.memory.shared_object_remapped,
restore_summary.memory.skipped_no_mapping,
restore_summary.memory.deferred_unsupported,
restore_summary.memory.other_tracee,
restore_summary.registers.registers_restored,
restore_summary.registers.other_tracee,
restore_summary.shared_objects.created_objects,
restore_summary.shared_objects.initialized_extents,
restore_summary.shared_objects.skipped_unsupported_objects,
);
assert_eq!(created.summary.tracees_created, snapshot.threads.len());
assert_eq!(remapped.summary.threads_remapped, snapshot.threads.len());
assert_eq!(
remapped.summary.memory_actions_remapped,
materialized.actions.len()
);
assert_eq!(restore_summary.tracees_restored, snapshot.threads.len());
assert_eq!(
restore_summary.memory.byte_backed_applied,
byte_backed_actions
);
assert_eq!(
restore_summary.memory.clean_file_remapped,
clean_file_actions
);
assert_eq!(
restore_summary.memory.shared_object_remapped,
shared_actions
);
assert_eq!(restore_summary.memory.skipped_no_mapping, skipped_actions);
assert_eq!(restore_summary.memory.deferred_unsupported, 0);
assert_eq!(
restore_summary.registers.registers_restored,
snapshot.threads.len()
);
Ok(())
})();
replacement_factory.kill_created_tracees();
kill_and_reap_tracee(child);
result.unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission, Linux x86_64, and /usr/local/bin/node"]
fn live_node_sigstop_restore_applies_kernel_state_and_controlled_continuation() {
const PIPE_BYTE: u8 = 0x4e;
const LEADER_STUB_OFFSET: u64 = 0;
const FOLLOWER_EXIT_STUB_OFFSET: u64 = 0x80;
const PIPE_BYTE_OFFSET: u64 = 0x100;
const LEADER_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, ];
const EXIT_THREAD_STUB: &[u8] = &[
0xb8, 0x3c, 0x00, 0x00, 0x00, 0x31, 0xff, 0x0f, 0x05, ];
let script =
"setTimeout(()=>process.kill(process.pid,'SIGSTOP'),25);setInterval(()=>{},1000)";
let (child, snapshot) = capture_live_ptrace_exec_snapshot_on_sigstop(
&["/usr/local/bin/node", "-e", script],
2,
50000,
)
.unwrap();
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_fd(pipe_fds[0]).unwrap();
let mut replacement_factory = LinuxStoppedTraceeReplacementFactory::default();
let result: Result<(), FdError> = (|| {
let leader_thread = snapshot
.threads
.iter()
.find(|thread| thread.host_pid == child)
.ok_or(FdError::TraceeInstallFailed(
"live Node snapshot missing leader thread",
))?;
let follower_kernel = snapshot
.thread_kernel_states
.iter()
.find(|state| state.host_pid != child && state.clear_child_tid.is_some())
.ok_or(FdError::TraceeInstallFailed(
"live Node snapshot missing follower kernel state",
))?;
let follower_thread = snapshot
.threads
.iter()
.find(|thread| thread.host_pid == follower_kernel.host_pid)
.ok_or(FdError::TraceeInstallFailed(
"live Node snapshot missing follower thread",
))?;
let follower_clear_tid =
follower_kernel
.clear_child_tid
.ok_or(FdError::TraceeInstallFailed(
"live Node follower clear_child_tid was missing",
))?;
let mut seen_model_pids = BTreeSet::new();
let unique_memory_maps = snapshot
.memory_maps
.iter()
.filter(|tracee_maps| seen_model_pids.insert(tracee_maps.model_pid))
.cloned()
.collect::<Vec<_>>();
let plan = classify_memory_restore_actions(&unique_memory_maps);
let raw_dumps = capture_dump_candidate_memory(&KboxlikeStoppedTraceeSnapshot {
threads: Vec::new(),
thread_kernel_states: Vec::new(),
memory_maps: unique_memory_maps,
fd_tables: Vec::new(),
});
let byte_dumps = filter_byte_backed_memory_dumps(&plan.actions, &raw_dumps);
let shared_dump_capture = capture_shared_object_memory(&plan);
if !byte_dumps.failures.is_empty() || !shared_dump_capture.failures.is_empty() {
return Err(FdError::TraceeInstallFailed(
"live Node memory capture had restore-required failures",
));
}
let materialized = materialize_memory_restore_actions(&plan, &byte_dumps).unwrap();
let syscall_page = choose_restore_syscall_page(&materialized)?;
let created =
create_stopped_replacement_tracee_set(&snapshot.threads, &mut replacement_factory)?;
let replacement_leader = *created
.replacement_host_pids
.get(&leader_thread.model_thread_id)
.ok_or(FdError::TraceeInstallFailed(
"missing Node replacement leader",
))?;
let replacement_follower = *created
.replacement_host_pids
.get(&follower_thread.model_thread_id)
.ok_or(FdError::TraceeInstallFailed(
"missing Node replacement follower",
))?;
let remapped = remap_stopped_tracee_restore_set_to_replacement_pids(
&materialized,
&snapshot.threads,
&shared_dump_capture.dumps,
&created.replacement_host_pids,
)?;
let remapped_kernel_states = remap_thread_kernel_states_to_replacement_pids(
&snapshot.thread_kernel_states,
&created.replacement_host_pids,
)?;
let mut access_factory = ScratchTraceeRestoreAccessFactory {
syscall_page,
clear_existing_mappings: false,
cleared_pids: BTreeSet::new(),
};
let restore_summary = apply_stopped_tracee_restore_set(
&remapped.restore,
&remapped.threads,
&remapped.shared_dumps,
std::process::id() as i32,
&mut access_factory,
)?;
assert_eq!(restore_summary.tracees_restored, snapshot.threads.len());
for tracee_pid in created.replacement_host_pids.values().copied() {
let mut executor = PtraceScratchSyscallExecutor::new(tracee_pid, syscall_page)?;
apply_thread_kernel_state_restore_for_tracee(
&remapped_kernel_states,
tracee_pid,
&mut executor,
)?;
}
let mut robust_applied = 0usize;
for state in &remapped_kernel_states {
if let Some(robust_head) = state.robust_list_head {
assert_eq!(
robust_list_for_tid(state.host_pid)?,
(robust_head, state.robust_list_len)
);
robust_applied += 1;
}
}
if read_i32_from_tracee_memory(replacement_leader, follower_clear_tid)? == 0 {
return Err(FdError::TraceeInstallFailed(
"live Node follower clear_child_tid was zero before controlled exit",
));
}
let mut stub_page = vec![0xcc; 4096];
stub_page[LEADER_STUB_OFFSET as usize
..LEADER_STUB_OFFSET as usize + LEADER_WRITE_AND_PAUSE_STUB.len()]
.copy_from_slice(LEADER_WRITE_AND_PAUSE_STUB);
stub_page[FOLLOWER_EXIT_STUB_OFFSET as usize
..FOLLOWER_EXIT_STUB_OFFSET as usize + EXIT_THREAD_STUB.len()]
.copy_from_slice(EXIT_THREAD_STUB);
stub_page[PIPE_BYTE_OFFSET as usize] = PIPE_BYTE;
let mut writer = ProcessVmMemoryWriter::new(replacement_leader)?;
writer.write_bytes(syscall_page, &stub_page)?;
for thread in &remapped.threads {
let mut regs = thread.regs;
if thread.host_pid == replacement_leader {
regs.rip = syscall_page + LEADER_STUB_OFFSET;
regs.rdi = pipe_fds[1] as u64;
regs.rsi = syscall_page + PIPE_BYTE_OFFSET;
} else {
regs.rip = syscall_page + FOLLOWER_EXIT_STUB_OFFSET;
}
let mut register_writer = PtraceRegisterWriter::new(thread.host_pid)?;
register_writer.write_regs(®s)?;
}
let mut resumer = PtraceDetachRestoredTraceeResumer;
for tracee_pid in created.replacement_host_pids.values().copied() {
resumer.resume_restored_tracee(tracee_pid)?;
}
let mut pipe_byte = [0u8; 1];
read_pipe_bytes(pipe_fds[0], &mut pipe_byte)?;
assert_eq!(pipe_byte[0], PIPE_BYTE);
wait_for_tracee_i32_zero(replacement_leader, follower_clear_tid)?;
eprintln!(
"LIVE_NODE_CONTROLLED_CONTINUATION source_tracees={} replacements={} leader={} follower={} syscall_page=0x{:x} robust_applied={} pipe_byte=0x{:x} clear_tid=0x{:x}",
snapshot.threads.len(),
created.summary.tracees_created,
replacement_leader,
replacement_follower,
syscall_page,
robust_applied,
pipe_byte[0],
follower_clear_tid,
);
Ok(())
})();
replacement_factory.kill_created_tracees();
kill_and_reap_tracee(child);
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_supervisor_snapshot_captures_real_clone_thread_kernel_state() {
fn state_addrs() -> (u64, u64, u64, u64) {
unsafe {
let state = live_source_state_ptr();
(
std::ptr::addr_of_mut!((*state).leader_clear_tid) as u64,
std::ptr::addr_of_mut!((*state).follower_clear_tid) as u64,
std::ptr::addr_of_mut!((*state).leader_robust_list) as u64,
std::ptr::addr_of_mut!((*state).follower_robust_list) as u64,
)
}
}
let (child, snapshot) = capture_live_source_clone_thread_snapshot().unwrap();
let result: Result<(), FdError> = (|| {
assert_eq!(snapshot.threads.len(), 2);
assert_eq!(snapshot.thread_kernel_states.len(), 2);
let (leader_clear, follower_clear, leader_robust, follower_robust) = state_addrs();
let leader_state = snapshot
.thread_kernel_states
.iter()
.find(|state| state.host_pid == child)
.ok_or(FdError::TraceeInstallFailed(
"snapshot missing source leader kernel state",
))?;
let follower_state = snapshot
.thread_kernel_states
.iter()
.find(|state| state.host_pid != child)
.ok_or(FdError::TraceeInstallFailed(
"snapshot missing source follower kernel state",
))?;
assert_eq!(leader_state.clear_child_tid, Some(leader_clear));
assert_eq!(leader_state.robust_list_head, Some(leader_robust));
assert_eq!(leader_state.robust_list_len, LIVE_SOURCE_ROBUST_LIST_LEN);
assert_eq!(follower_state.clear_child_tid, Some(follower_clear));
assert_eq!(follower_state.robust_list_head, Some(follower_robust));
assert_eq!(follower_state.robust_list_len, LIVE_SOURCE_ROBUST_LIST_LEN);
assert!(snapshot
.threads
.iter()
.all(|thread| thread.regs.fs_base != 0));
Ok(())
})();
kill_and_reap_tracee(child);
result.unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission, Linux x86_64, and glibc pthread tracing"]
fn live_supervisor_snapshot_captures_real_pthread_kernel_state() {
let (child, snapshot) = capture_live_source_pthread_snapshot().unwrap();
let result: Result<(), FdError> = (|| {
assert_eq!(snapshot.threads.len(), 2);
assert_eq!(snapshot.thread_kernel_states.len(), 2);
let leader_thread = snapshot
.threads
.iter()
.find(|thread| thread.host_pid == child)
.ok_or(FdError::TraceeInstallFailed(
"pthread snapshot missing leader thread",
))?;
let follower_thread = snapshot
.threads
.iter()
.find(|thread| thread.host_pid != child)
.ok_or(FdError::TraceeInstallFailed(
"pthread snapshot missing follower thread",
))?;
assert_ne!(leader_thread.regs.fs_base, 0);
assert_ne!(follower_thread.regs.fs_base, 0);
assert_ne!(leader_thread.regs.fs_base, follower_thread.regs.fs_base);
let follower_state = snapshot
.thread_kernel_states
.iter()
.find(|state| state.host_pid == follower_thread.host_pid)
.ok_or(FdError::TraceeInstallFailed(
"pthread snapshot missing follower kernel state",
))?;
let follower_clear_tid =
follower_state
.clear_child_tid
.ok_or(FdError::TraceeInstallFailed(
"pthread follower clear_child_tid was missing",
))?;
assert_ne!(follower_clear_tid, 0);
let robust_head =
follower_state
.robust_list_head
.ok_or(FdError::TraceeInstallFailed(
"pthread follower robust-list head was missing",
))?;
assert_ne!(robust_head, 0);
assert_eq!(follower_state.robust_list_len, LIVE_SOURCE_ROBUST_LIST_LEN);
Ok(())
})();
kill_and_reap_tracee(child);
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_clone_snapshot_restores_into_replacement_group() {
const CODE_ADDR: u64 = 0x7c00_0000_0000;
const STATE_ADDR: u64 = 0x7c00_0000_1000;
const PIPE_BYTE_OFFSET: u64 = 0x40;
const EXIT_STUB_OFFSET: u64 = 0x80;
const LEADER_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, ];
const EXIT_THREAD_STUB: &[u8] = &[
0xb8, 0x3c, 0x00, 0x00, 0x00, 0x31, 0xff, 0x0f, 0x05, ];
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 read_exact_tracee(pid: i32, addr: u64, bytes: &mut [u8]) -> Result<(), FdError> {
let local = libc::iovec {
iov_base: bytes.as_mut_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 read = unsafe { libc::process_vm_readv(pid, &local, 1, &remote, 1, 0) };
if read != bytes.len() as isize {
return Err(FdError::TraceeInstallFailed("process_vm_readv failed"));
}
Ok(())
}
fn read_u8_from_tracee(pid: i32, addr: u64) -> Result<u8, FdError> {
let mut byte = 0u8;
read_exact_tracee(pid, addr, std::slice::from_mut(&mut byte))?;
Ok(byte)
}
fn read_i32_from_tracee(pid: i32, addr: u64) -> Result<i32, FdError> {
let mut bytes = [0u8; std::mem::size_of::<i32>()];
read_exact_tracee(pid, addr, &mut bytes)?;
Ok(i32::from_ne_bytes(bytes))
}
fn robust_list_for_tid(tid: i32) -> Result<(u64, u64), FdError> {
let mut head: *mut libc::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_one_pipe_byte(fd: i32) -> Result<u8, FdError> {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while std::time::Instant::now() <= deadline {
let mut byte = 0u8;
let read = unsafe { libc::read(fd, &mut byte as *mut _ as *mut libc::c_void, 1) };
if read == 1 {
return Ok(byte);
}
if read < 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));
}
Err(FdError::TraceeInstallFailed(
"restored leader did not write pipe byte",
))
}
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(
"captured clear_child_tid was not cleared after replacement follower exit",
))
}
let (source_leader, snapshot) = capture_live_source_clone_thread_snapshot().unwrap();
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 mut replacement_factory = LinuxStoppedTraceeReplacementFactory::default();
let result: Result<(), FdError> = (|| {
let source_leader_thread = snapshot
.threads
.iter()
.find(|thread| thread.host_pid == source_leader)
.ok_or(FdError::TraceeInstallFailed(
"captured snapshot missing source leader thread",
))?;
let source_follower_thread = snapshot
.threads
.iter()
.find(|thread| thread.host_pid != source_leader)
.ok_or(FdError::TraceeInstallFailed(
"captured snapshot missing source follower thread",
))?;
if source_leader_thread.regs.fs_base == 0 {
return Err(FdError::TraceeInstallFailed(
"captured leader FS base was zero",
));
}
let expected_fs_byte =
read_u8_from_tracee(source_leader, source_leader_thread.regs.fs_base)?;
let source_follower_kernel = snapshot
.thread_kernel_states
.iter()
.find(|state| state.host_pid == source_follower_thread.host_pid)
.ok_or(FdError::TraceeInstallFailed(
"captured snapshot missing source follower kernel state",
))?;
let source_follower_clear_tid =
source_follower_kernel
.clear_child_tid
.ok_or(FdError::TraceeInstallFailed(
"captured follower clear_child_tid was missing",
))?;
let mut controlled_threads = snapshot.threads.clone();
for thread in &mut controlled_threads {
if thread.host_pid == source_leader {
thread.regs.rip = CODE_ADDR;
thread.regs.rdi = pipe_fds[1] as u64;
thread.regs.rsi = STATE_ADDR + PIPE_BYTE_OFFSET;
} else {
thread.regs.rip = CODE_ADDR + EXIT_STUB_OFFSET;
}
}
let created = create_stopped_replacement_tracee_set(
&controlled_threads,
&mut replacement_factory,
)?;
let replacement_leader = *created
.replacement_host_pids
.get(&source_leader_thread.model_thread_id)
.ok_or(FdError::TraceeInstallFailed(
"missing live-captured replacement leader",
))?;
let replacement_follower = *created
.replacement_host_pids
.get(&source_follower_thread.model_thread_id)
.ok_or(FdError::TraceeInstallFailed(
"missing live-captured replacement follower",
))?;
let mut code_page = vec![0xcc; 0x100];
code_page[..LEADER_FS_WRITE_AND_PAUSE_STUB.len()]
.copy_from_slice(LEADER_FS_WRITE_AND_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 state_page = vec![0u8; 0x100];
let action_for =
|start, prot, bytes| KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: source_leader,
model_pid: source_leader_thread.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 remapped_restore = remap_stopped_tracee_restore_set_to_replacement_pids(
&restore,
&controlled_threads,
&[],
&created.replacement_host_pids,
)?;
let remapped_kernel_states = remap_thread_kernel_states_to_replacement_pids(
&snapshot.thread_kernel_states,
&created.replacement_host_pids,
)?;
let mut access_factory = LinuxTraceeRestoreAccessFactory;
let restore_summary = apply_stopped_tracee_restore_set(
&remapped_restore.restore,
&remapped_restore.threads,
&remapped_restore.shared_dumps,
std::process::id() as i32,
&mut access_factory,
)?;
assert_eq!(restore_summary.tracees_restored, 2);
assert_eq!(restore_summary.memory.byte_backed_applied, 2);
assert_eq!(restore_summary.registers.registers_restored, 2);
for tracee_pid in [replacement_leader, replacement_follower] {
let mut executor = PtraceSyscallExecutor::new(tracee_pid)?;
apply_thread_kernel_state_restore_for_tracee(
&remapped_kernel_states,
tracee_pid,
&mut executor,
)?;
}
for state in &remapped_kernel_states {
if let Some(robust_head) = state.robust_list_head {
assert_eq!(
robust_list_for_tid(state.host_pid)?,
(robust_head, state.robust_list_len)
);
}
}
if read_i32_from_tracee(replacement_leader, source_follower_clear_tid)? == 0 {
return Err(FdError::TraceeInstallFailed(
"captured follower clear_child_tid was zero before replacement exit",
));
}
let mut resumer = PtraceDetachRestoredTraceeResumer;
resumer.resume_restored_tracee(replacement_leader)?;
resumer.resume_restored_tracee(replacement_follower)?;
let restored_fs_byte = read_one_pipe_byte(pipe_fds[0])?;
assert_eq!(restored_fs_byte, expected_fs_byte);
wait_for_clear_child_tid_zero(replacement_leader, source_follower_clear_tid)?;
Ok(())
})();
replacement_factory.kill_created_tracees();
kill_and_reap_tracee(source_leader);
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, Linux x86_64, and glibc pthread tracing"]
fn live_captured_pthread_snapshot_restores_clear_tid_page() {
const CODE_ADDR: u64 = 0x7c00_0000_0000;
const STATE_ADDR: u64 = 0x7c00_0000_1000;
const PIPE_BYTE_OFFSET: u64 = 0x40;
const EXIT_STUB_OFFSET: u64 = 0x80;
const PAGE_SIZE: u64 = 4096;
const LEADER_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, ];
const EXIT_THREAD_STUB: &[u8] = &[
0xb8, 0x3c, 0x00, 0x00, 0x00, 0x31, 0xff, 0x0f, 0x05, ];
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 read_exact_tracee(pid: i32, addr: u64, bytes: &mut [u8]) -> Result<(), FdError> {
let local = libc::iovec {
iov_base: bytes.as_mut_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 read = unsafe { libc::process_vm_readv(pid, &local, 1, &remote, 1, 0) };
if read != bytes.len() as isize {
return Err(FdError::TraceeInstallFailed("process_vm_readv failed"));
}
Ok(())
}
fn read_u8_from_tracee(pid: i32, addr: u64) -> Result<u8, FdError> {
let mut byte = 0u8;
read_exact_tracee(pid, addr, std::slice::from_mut(&mut byte))?;
Ok(byte)
}
fn read_i32_from_tracee(pid: i32, addr: u64) -> Result<i32, FdError> {
let mut bytes = [0u8; std::mem::size_of::<i32>()];
read_exact_tracee(pid, addr, &mut bytes)?;
Ok(i32::from_ne_bytes(bytes))
}
fn robust_list_for_tid(tid: i32) -> Result<(u64, u64), FdError> {
let mut head: *mut libc::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_one_pipe_byte(fd: i32) -> Result<u8, FdError> {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while std::time::Instant::now() <= deadline {
let mut byte = 0u8;
let read = unsafe { libc::read(fd, &mut byte as *mut _ as *mut libc::c_void, 1) };
if read == 1 {
return Ok(byte);
}
if read < 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));
}
Err(FdError::TraceeInstallFailed(
"restored leader did not write pipe byte",
))
}
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(
"captured pthread clear_child_tid was not cleared after replacement follower exit",
))
}
let (source_leader, snapshot) = capture_live_source_pthread_snapshot().unwrap();
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 mut replacement_factory = LinuxStoppedTraceeReplacementFactory::default();
let result: Result<(), FdError> = (|| {
let source_leader_thread = snapshot
.threads
.iter()
.find(|thread| thread.host_pid == source_leader)
.ok_or(FdError::TraceeInstallFailed(
"captured pthread snapshot missing source leader thread",
))?;
let source_follower_thread = snapshot
.threads
.iter()
.find(|thread| thread.host_pid != source_leader)
.ok_or(FdError::TraceeInstallFailed(
"captured pthread snapshot missing source follower thread",
))?;
if source_leader_thread.regs.fs_base == 0 {
return Err(FdError::TraceeInstallFailed(
"captured pthread leader FS base was zero",
));
}
let expected_fs_byte =
read_u8_from_tracee(source_leader, source_leader_thread.regs.fs_base)?;
let source_follower_kernel = snapshot
.thread_kernel_states
.iter()
.find(|state| state.host_pid == source_follower_thread.host_pid)
.ok_or(FdError::TraceeInstallFailed(
"captured pthread snapshot missing source follower kernel state",
))?;
let source_follower_clear_tid =
source_follower_kernel
.clear_child_tid
.ok_or(FdError::TraceeInstallFailed(
"captured pthread follower clear_child_tid was missing",
))?;
let clear_tid_page_start = source_follower_clear_tid & !(PAGE_SIZE - 1);
let mut clear_tid_page = vec![0u8; PAGE_SIZE as usize];
read_exact_tracee(source_leader, clear_tid_page_start, &mut clear_tid_page)?;
let mut controlled_threads = snapshot.threads.clone();
for thread in &mut controlled_threads {
if thread.host_pid == source_leader {
thread.regs.rip = CODE_ADDR;
thread.regs.rdi = pipe_fds[1] as u64;
thread.regs.rsi = STATE_ADDR + PIPE_BYTE_OFFSET;
} else {
thread.regs.rip = CODE_ADDR + EXIT_STUB_OFFSET;
}
}
let created = create_stopped_replacement_tracee_set(
&controlled_threads,
&mut replacement_factory,
)?;
let replacement_leader = *created
.replacement_host_pids
.get(&source_leader_thread.model_thread_id)
.ok_or(FdError::TraceeInstallFailed(
"missing pthread replacement leader",
))?;
let replacement_follower = *created
.replacement_host_pids
.get(&source_follower_thread.model_thread_id)
.ok_or(FdError::TraceeInstallFailed(
"missing pthread replacement follower",
))?;
let mut code_page = vec![0xcc; 0x100];
code_page[..LEADER_FS_WRITE_AND_PAUSE_STUB.len()]
.copy_from_slice(LEADER_FS_WRITE_AND_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 state_page = vec![0u8; 0x100];
let action_for =
|start, prot, bytes| KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: source_leader,
model_pid: source_leader_thread.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),
action_for(
clear_tid_page_start,
libc::PROT_READ | libc::PROT_WRITE,
clear_tid_page,
),
],
};
let remapped_restore = remap_stopped_tracee_restore_set_to_replacement_pids(
&restore,
&controlled_threads,
&[],
&created.replacement_host_pids,
)?;
let remapped_kernel_states = remap_thread_kernel_states_to_replacement_pids(
&snapshot.thread_kernel_states,
&created.replacement_host_pids,
)?;
let mut access_factory = LinuxTraceeRestoreAccessFactory;
let restore_summary = apply_stopped_tracee_restore_set(
&remapped_restore.restore,
&remapped_restore.threads,
&remapped_restore.shared_dumps,
std::process::id() as i32,
&mut access_factory,
)?;
assert_eq!(restore_summary.tracees_restored, 2);
assert_eq!(restore_summary.memory.byte_backed_applied, 3);
assert_eq!(restore_summary.registers.registers_restored, 2);
for tracee_pid in [replacement_leader, replacement_follower] {
let mut executor = PtraceSyscallExecutor::new(tracee_pid)?;
apply_thread_kernel_state_restore_for_tracee(
&remapped_kernel_states,
tracee_pid,
&mut executor,
)?;
}
for state in &remapped_kernel_states {
if let Some(robust_head) = state.robust_list_head {
assert_eq!(
robust_list_for_tid(state.host_pid)?,
(robust_head, state.robust_list_len)
);
}
}
if read_i32_from_tracee(replacement_leader, source_follower_clear_tid)? == 0 {
return Err(FdError::TraceeInstallFailed(
"captured pthread follower clear_child_tid was zero before replacement exit",
));
}
let mut resumer = PtraceDetachRestoredTraceeResumer;
resumer.resume_restored_tracee(replacement_leader)?;
resumer.resume_restored_tracee(replacement_follower)?;
let restored_fs_byte = read_one_pipe_byte(pipe_fds[0])?;
assert_eq!(restored_fs_byte, expected_fs_byte);
wait_for_clear_child_tid_zero(replacement_leader, source_follower_clear_tid)?;
Ok(())
})();
replacement_factory.kill_created_tracees();
kill_and_reap_tracee(source_leader);
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, Linux x86_64, and glibc pthread tracing"]
fn live_captured_pthread_snapshot_continues_source_code_path() {
const PAGE_SIZE: u64 = 4096;
const LEADER_STACK_ADDR: u64 = 0x7d00_0000_0000;
const FOLLOWER_STACK_ADDR: u64 = 0x7d00_0001_0000;
const STACK_SIZE: u64 = PAGE_SIZE;
const RESTORED_STACK_ENTRY_ALIGN: u64 = 8;
const SOURCE_MARKER: u8 = 0x5a;
const FOLLOWER_MARKER: u32 = 0xfeed_cafe;
fn continuation_state_addr() -> u64 {
std::ptr::addr_of_mut!(LIVE_PTHREAD_CONTINUATION_STATE) as u64
}
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 read_exact_tracee(pid: i32, addr: u64, bytes: &mut [u8]) -> Result<(), FdError> {
let local = libc::iovec {
iov_base: bytes.as_mut_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 read = unsafe { libc::process_vm_readv(pid, &local, 1, &remote, 1, 0) };
if read != bytes.len() as isize {
return Err(FdError::TraceeInstallFailed("process_vm_readv failed"));
}
Ok(())
}
fn read_u8_from_tracee(pid: i32, addr: u64) -> Result<u8, FdError> {
let mut byte = 0u8;
read_exact_tracee(pid, addr, std::slice::from_mut(&mut byte))?;
Ok(byte)
}
fn read_i32_from_tracee(pid: i32, addr: u64) -> Result<i32, FdError> {
let mut bytes = [0u8; std::mem::size_of::<i32>()];
read_exact_tracee(pid, addr, &mut bytes)?;
Ok(i32::from_ne_bytes(bytes))
}
fn read_u32_from_tracee(pid: i32, addr: u64) -> Result<u32, FdError> {
let mut bytes = [0u8; std::mem::size_of::<u32>()];
read_exact_tracee(pid, addr, &mut bytes)?;
Ok(u32::from_ne_bytes(bytes))
}
fn robust_list_for_tid(tid: i32) -> Result<(u64, u64), FdError> {
let mut head: *mut libc::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_pipe_bytes(fd: i32, out: &mut [u8]) -> Result<(), FdError> {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
let mut offset = 0;
while std::time::Instant::now() <= deadline {
let read = unsafe {
libc::read(
fd,
out[offset..].as_mut_ptr() as *mut libc::c_void,
out.len() - offset,
)
};
if read > 0 {
offset += read as usize;
if offset == out.len() {
return Ok(());
}
} else if read < 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));
}
Err(FdError::TraceeInstallFailed(
"restored source-code path did not write expected pipe bytes",
))
}
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(
"captured pthread clear_child_tid was not cleared after source-code follower exit",
))
}
let (source_leader, snapshot) = capture_live_source_pthread_snapshot().unwrap();
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 mut replacement_factory = LinuxStoppedTraceeReplacementFactory::default();
let result: Result<(), FdError> = (|| {
let source_leader_thread = snapshot
.threads
.iter()
.find(|thread| thread.host_pid == source_leader)
.ok_or(FdError::TraceeInstallFailed(
"captured source-code snapshot missing source leader thread",
))?;
let source_follower_thread = snapshot
.threads
.iter()
.find(|thread| thread.host_pid != source_leader)
.ok_or(FdError::TraceeInstallFailed(
"captured source-code snapshot missing source follower thread",
))?;
if source_leader_thread.regs.fs_base == 0 {
return Err(FdError::TraceeInstallFailed(
"captured source-code leader FS base was zero",
));
}
let expected_fs_byte =
read_u8_from_tracee(source_leader, source_leader_thread.regs.fs_base)?;
let source_follower_kernel = snapshot
.thread_kernel_states
.iter()
.find(|state| state.host_pid == source_follower_thread.host_pid)
.ok_or(FdError::TraceeInstallFailed(
"captured source-code snapshot missing source follower kernel state",
))?;
let source_follower_clear_tid =
source_follower_kernel
.clear_child_tid
.ok_or(FdError::TraceeInstallFailed(
"captured source-code follower clear_child_tid was missing",
))?;
let state_addr = continuation_state_addr();
let state_page_start = state_addr & !(PAGE_SIZE - 1);
let state_marker_offset = state_addr - state_page_start
+ std::mem::offset_of!(LivePthreadContinuationState, source_marker) as u64;
let state_leader_fs_offset = state_addr - state_page_start
+ std::mem::offset_of!(LivePthreadContinuationState, leader_fs_byte) as u64;
let state_follower_marker_addr = state_addr
+ std::mem::offset_of!(LivePthreadContinuationState, follower_marker) as u64;
let clear_tid_page_start = source_follower_clear_tid & !(PAGE_SIZE - 1);
let mut state_page = vec![0u8; PAGE_SIZE as usize];
read_exact_tracee(source_leader, state_page_start, &mut state_page)?;
if state_page[state_marker_offset as usize] != SOURCE_MARKER {
return Err(FdError::TraceeInstallFailed(
"source continuation state marker was not captured",
));
}
let mut clear_tid_page = vec![0u8; PAGE_SIZE as usize];
read_exact_tracee(source_leader, clear_tid_page_start, &mut clear_tid_page)?;
let mut controlled_threads = snapshot.threads.clone();
for thread in &mut controlled_threads {
if thread.host_pid == source_leader {
thread.regs.rip = live_pthread_source_leader_continue as usize as u64;
thread.regs.rdi = pipe_fds[1] as u64;
thread.regs.rsi = state_addr;
thread.regs.rsp = LEADER_STACK_ADDR + STACK_SIZE - RESTORED_STACK_ENTRY_ALIGN;
} else {
thread.regs.rip = live_pthread_source_follower_exit as usize as u64;
thread.regs.rdi = state_addr;
thread.regs.rsp = FOLLOWER_STACK_ADDR + STACK_SIZE - RESTORED_STACK_ENTRY_ALIGN;
}
}
let created = create_stopped_replacement_tracee_set(
&controlled_threads,
&mut replacement_factory,
)?;
let replacement_leader = *created
.replacement_host_pids
.get(&source_leader_thread.model_thread_id)
.ok_or(FdError::TraceeInstallFailed(
"missing source-code replacement leader",
))?;
let replacement_follower = *created
.replacement_host_pids
.get(&source_follower_thread.model_thread_id)
.ok_or(FdError::TraceeInstallFailed(
"missing source-code replacement follower",
))?;
let action_for =
|start, prot, bytes| KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: source_leader,
model_pid: source_leader_thread.model_pid,
start,
prot,
bytes,
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate { label: None },
};
let mut actions = vec![
action_for(
state_page_start,
libc::PROT_READ | libc::PROT_WRITE,
state_page,
),
action_for(
LEADER_STACK_ADDR,
libc::PROT_READ | libc::PROT_WRITE,
vec![0u8; STACK_SIZE as usize],
),
action_for(
FOLLOWER_STACK_ADDR,
libc::PROT_READ | libc::PROT_WRITE,
vec![0u8; STACK_SIZE as usize],
),
];
if clear_tid_page_start != state_page_start {
actions.push(action_for(
clear_tid_page_start,
libc::PROT_READ | libc::PROT_WRITE,
clear_tid_page,
));
}
let expected_byte_backed = actions.len();
let restore = KboxlikeMaterializedMemoryRestore { actions };
let remapped_restore = remap_stopped_tracee_restore_set_to_replacement_pids(
&restore,
&controlled_threads,
&[],
&created.replacement_host_pids,
)?;
let remapped_kernel_states = remap_thread_kernel_states_to_replacement_pids(
&snapshot.thread_kernel_states,
&created.replacement_host_pids,
)?;
let mut access_factory = LinuxTraceeRestoreAccessFactory;
let restore_summary = apply_stopped_tracee_restore_set(
&remapped_restore.restore,
&remapped_restore.threads,
&remapped_restore.shared_dumps,
std::process::id() as i32,
&mut access_factory,
)?;
assert_eq!(restore_summary.tracees_restored, 2);
assert_eq!(
restore_summary.memory.byte_backed_applied,
expected_byte_backed
);
assert_eq!(restore_summary.registers.registers_restored, 2);
for tracee_pid in [replacement_leader, replacement_follower] {
let mut executor = PtraceSyscallExecutor::new(tracee_pid)?;
apply_thread_kernel_state_restore_for_tracee(
&remapped_kernel_states,
tracee_pid,
&mut executor,
)?;
}
for state in &remapped_kernel_states {
if let Some(robust_head) = state.robust_list_head {
assert_eq!(
robust_list_for_tid(state.host_pid)?,
(robust_head, state.robust_list_len)
);
}
}
if read_i32_from_tracee(replacement_leader, source_follower_clear_tid)? == 0 {
return Err(FdError::TraceeInstallFailed(
"source-code follower clear_child_tid was zero before replacement exit",
));
}
if read_u8_from_tracee(replacement_leader, state_page_start + state_marker_offset)?
!= SOURCE_MARKER
{
return Err(FdError::TraceeInstallFailed(
"restored source data marker was missing before resume",
));
}
let mut resumer = PtraceDetachRestoredTraceeResumer;
resumer.resume_restored_tracee(replacement_leader)?;
resumer.resume_restored_tracee(replacement_follower)?;
let mut pipe_bytes = [0u8; 2];
read_pipe_bytes(pipe_fds[0], &mut pipe_bytes)?;
assert_eq!(pipe_bytes, [SOURCE_MARKER, expected_fs_byte]);
wait_for_clear_child_tid_zero(replacement_leader, source_follower_clear_tid)?;
assert_eq!(
read_u8_from_tracee(
replacement_leader,
state_page_start + state_leader_fs_offset
)?,
expected_fs_byte
);
assert_eq!(
read_u32_from_tracee(replacement_leader, state_follower_marker_addr)?,
FOLLOWER_MARKER
);
Ok(())
})();
replacement_factory.kill_created_tracees();
kill_and_reap_tracee(source_leader);
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, Linux x86_64, and /usr/local/bin/node"]
fn live_ptrace_dispatch_runs_node_to_exit() {
assert_live_ptrace_exec_exits_zero(
&["/usr/local/bin/node", "-e", "process.exit(0)"],
20000,
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission, Linux x86_64, and a Chromium binary"]
fn live_ptrace_dispatch_runs_chromium_version_to_exit() {
let chrome = std::env::var("SUPERMACHINE_KBOXLIVE_CHROME").unwrap_or_else(|_| {
"/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome".to_owned()
});
assert_live_ptrace_exec_exits_zero(&[chrome.as_str(), "--version"], 20000);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission, Linux x86_64, and a Chromium binary"]
fn live_ptrace_dispatch_runs_chromium_headless_to_exit() {
let chrome = std::env::var("SUPERMACHINE_KBOXLIVE_CHROME").unwrap_or_else(|_| {
"/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome".to_owned()
});
assert_live_ptrace_exec_exits_zero(
&[
chrome.as_str(),
"--headless=new",
"--disable-gpu",
"--disable-dev-shm-usage",
"--no-sandbox",
"--dump-dom",
"data:text/html,<title>kboxlike</title><body>ok</body>",
],
200000,
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission, Linux x86_64, and a Chromium binary"]
fn live_chromium_sigstop_snapshot_materializes_memory_restore_plan() {
let chrome = std::env::var("SUPERMACHINE_KBOXLIVE_CHROME").unwrap_or_else(|_| {
"/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome".to_owned()
});
let profile_dir = std::env::temp_dir().join(format!(
"kboxlike-chromium-profile-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&profile_dir).unwrap();
let user_data_dir = format!("--user-data-dir={}", profile_dir.display());
let argv = vec![
chrome.as_str(),
"--headless=new",
"--disable-gpu",
"--disable-dev-shm-usage",
"--no-sandbox",
"--remote-debugging-port=0",
user_data_dir.as_str(),
"about:blank",
];
let (child, snapshot) = capture_live_ptrace_exec_snapshot_after_sigstop_request(
&argv,
2,
200000,
std::time::Duration::from_millis(750),
)
.unwrap();
let result: Result<(), FdError> = (|| {
let memory = super::super::runtime::materialize_runtime_memory(&snapshot)?;
memory.trace_summary(&snapshot, "LIVE_CHROMIUM_MEMORY_PLAN");
assert!(
snapshot.threads.len() >= 2,
"expected Chromium process tree, got {} tracees",
snapshot.threads.len()
);
assert_eq!(snapshot.fd_tables.len(), snapshot.threads.len());
assert!(!memory.materialized.actions.is_empty());
assert!(memory.dump_anonymous_private > 0 || memory.dump_private_file_cow > 0);
assert!(memory.remap_clean_file > 0);
assert_eq!(memory.byte_dump_bytes, memory.byte_backed_bytes);
assert_eq!(memory.materialized.actions.len(), memory.actions);
Ok(())
})();
kill_and_reap_tracee_group(child);
let _ = std::fs::remove_dir_all(&profile_dir);
result.unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires ptrace permission, Linux x86_64, and a Chromium binary"]
fn live_chromium_sigstop_snapshot_attempts_full_restore_helper() {
let chrome = std::env::var("SUPERMACHINE_KBOXLIVE_CHROME").unwrap_or_else(|_| {
"/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome".to_owned()
});
let profile_dir = std::env::temp_dir().join(format!(
"kboxlike-chromium-full-restore-profile-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&profile_dir).unwrap();
let user_data_dir = format!("--user-data-dir={}", profile_dir.display());
let argv = vec![
chrome.clone(),
"--headless=new".to_owned(),
"--disable-gpu".to_owned(),
"--disable-dev-shm-usage".to_owned(),
"--no-sandbox".to_owned(),
"--remote-debugging-port=0".to_owned(),
user_data_dir,
"about:blank".to_owned(),
];
let captured = super::capture_live_ptrace_rootfs_exec_snapshot_after_sigstop_request(
&super::KboxlikeRootfsExecConfig {
rootfs: std::path::PathBuf::from("/"),
argv,
env: std::env::vars().collect(),
cwd: "/".to_owned(),
user: None,
mounts: Vec::new(),
},
2,
200000,
super::KboxlikeCaptureSettle {
stop_after: std::time::Duration::from_millis(750),
ready_file: None,
ready_pattern: None,
},
)
.unwrap();
let child = captured.root_host_pid;
let snapshot = captured.snapshot;
let mut replacement_factory =
LinuxStoppedTraceeReplacementFactory::with_static_replacement_path("/bin/busybox");
let result: Result<(), FdError> = (|| {
let mut resumer = PtraceDetachRestoredTraceeResumer;
let runtime_restore =
super::super::runtime::restore_runtime_snapshot_with_replacements(
&snapshot,
&chrome,
super::super::runtime::KboxlikeRuntimeRestoreOptions {
trace_label: Some("LIVE_CHROMIUM_FULL_RESTORE"),
..Default::default()
},
&mut replacement_factory,
&mut resumer,
)
.inspect_err(|err| {
eprintln!("LIVE_CHROMIUM_FULL_RESTORE_FAILURE err={err:?}");
})?;
let restore_result = runtime_restore.restore;
eprintln!(
"LIVE_CHROMIUM_FULL_RESTORE_SUMMARY replacements={} tracee_restored={} memory_byte={} memory_clean={} memory_shared={} memory_deferred={} shared_created={} shared_initialized={} shared_unsupported={} fd_installed={} fd_epoll={} fd_options={} fd_queued_bytes={} fd_shutdown={} fd_scm={} resumed={}",
restore_result.summary.replacements.tracees_created,
restore_result.summary.tracee.tracees_restored,
restore_result.summary.tracee.memory.byte_backed_applied,
restore_result.summary.tracee.memory.clean_file_remapped,
restore_result.summary.tracee.memory.shared_object_remapped,
restore_result.summary.tracee.memory.deferred_unsupported,
restore_result.summary.tracee.shared_objects.created_objects,
restore_result.summary.tracee.shared_objects.initialized_extents,
restore_result.summary.tracee.shared_objects.skipped_unsupported_objects,
restore_result.summary.fd_tracee_installs,
restore_result.summary.fd_kernel.epoll_interests,
restore_result.summary.fd_kernel.socket_options,
restore_result.summary.fd_kernel.queued_bytes,
restore_result.summary.fd_kernel.socket_shutdowns,
restore_result.summary.fd_kernel.scm_rights_messages,
restore_result.summary.resume.tracees_resumed,
);
assert_eq!(
restore_result.summary.replacements.tracees_created,
snapshot.threads.len()
);
assert_eq!(
restore_result.summary.resume.tracees_resumed,
snapshot.threads.len()
);
let immediate_statuses =
if std::env::var_os("SUPERMACHINE_KBOXLIKE_TRACE_RESTORED_SYSCALLS").is_some() {
trace_restored_syscalls_until_stop(
restore_result.replacement_host_pids.values().copied(),
std::time::Duration::from_millis(500),
)
} else {
poll_restored_pid_statuses(
restore_result.replacement_host_pids.values().copied(),
std::time::Duration::from_millis(500),
)
};
if std::env::var_os("SUPERMACHINE_KBOXLIKE_TRACE_FD_RESTORE").is_some() {
print_live_restored_proc_fd_inventory(
restore_result.replacement_host_pids.values().copied(),
);
print_live_restored_code_comparison(
&snapshot,
&restore_result.replacement_host_pids,
);
}
let restored_states = restore_result
.replacement_host_pids
.values()
.copied()
.map(|host_pid| (host_pid, live_pid_state(host_pid)))
.collect::<Vec<_>>();
let alive = restored_states
.iter()
.filter(|(_, state)| state.is_some_and(|state| state != 'Z'))
.count();
eprintln!(
"LIVE_CHROMIUM_RESTORED_HEALTH alive={} total={} states={:?}",
alive,
restored_states.len(),
restored_states,
);
for (host_pid, state) in &restored_states {
let status = immediate_statuses.get(host_pid).cloned().flatten();
eprintln!(
"LIVE_CHROMIUM_RESTORED_EXIT pid={} state={:?} status={:?}",
host_pid, state, status,
);
}
let crashed = immediate_statuses.values().any(|status| {
status
.as_ref()
.is_some_and(|status| !status.starts_with("exit(0)"))
});
let primary_host_pid = snapshot
.threads
.first()
.and_then(|thread| {
restore_result
.replacement_host_pids
.get(&thread.model_thread_id)
})
.copied()
.expect("restored primary Chromium tracee");
assert!(live_pid_state(primary_host_pid).is_some_and(|state| state != 'Z'));
assert!(!crashed);
let port = read_devtools_active_port(&profile_dir)
.expect("restored Chromium DevTools active port");
let status = probe_devtools_version_until(port, std::time::Duration::from_secs(3))
.expect("restored Chromium DevTools /json/version response");
eprintln!("LIVE_CHROMIUM_RESTORED_DEVTOOLS port={port} ok=true {status}");
Ok(())
})();
replacement_factory.kill_created_tracees();
super::kill_and_reap_live_process_group(child);
let _ = std::fs::remove_dir_all(&profile_dir);
result.unwrap();
}
}