#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use super::dispatch::KboxlikeTraceeIdentity;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use super::fd::{
FdError, KboxlikeFdSystem, KboxlikeSocketShutdownState, ModelThreadId, ProcessId,
ShadowBacking, ShadowKind, ShadowObject,
};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use super::memory::{read_proc_maps_for_pid, KboxlikeMapEntry};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use super::supervisor::{KboxlikeSyscallSupervisor, TraceeRegisterReader};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use std::collections::{BTreeMap, BTreeSet};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use std::fs;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use std::path::PathBuf;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug)]
pub(crate) struct KboxlikeThreadSnapshot {
pub(crate) host_pid: i32,
pub(crate) model_pid: ProcessId,
pub(crate) model_thread_id: ModelThreadId,
pub(crate) regs: libc::user_regs_struct,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct KboxlikeThreadKernelStateSnapshot {
pub(crate) host_pid: i32,
pub(crate) model_pid: ProcessId,
pub(crate) model_thread_id: ModelThreadId,
pub(crate) clear_child_tid: Option<u64>,
pub(crate) robust_list_head: Option<u64>,
pub(crate) robust_list_len: u64,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KboxlikeTraceeMapSnapshot {
pub(crate) host_pid: i32,
pub(crate) model_pid: ProcessId,
pub(crate) model_thread_id: ModelThreadId,
pub(crate) maps: Vec<KboxlikeMapEntry>,
pub(crate) dump_candidate_bytes: u64,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum KboxlikeFdTargetKind {
RegularPath,
Device,
Pipe,
Socket,
EventFd,
EventPoll,
TimerFd,
Inotify,
SignalFd,
Memfd,
AnonInode,
Unknown,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KboxlikeEpollInterestSnapshot {
pub(crate) target_fd: i32,
pub(crate) events: u32,
pub(crate) data: u64,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KboxlikeTimerFdSnapshot {
pub(crate) clockid: i32,
pub(crate) ticks: u64,
pub(crate) settime_flags: i32,
pub(crate) value_sec: i64,
pub(crate) value_nsec: i64,
pub(crate) interval_sec: i64,
pub(crate) interval_nsec: i64,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KboxlikeFdSnapshot {
pub(crate) host_pid: i32,
pub(crate) model_pid: ProcessId,
pub(crate) model_thread_id: ModelThreadId,
pub(crate) fd: i32,
pub(crate) target: String,
pub(crate) kind: KboxlikeFdTargetKind,
pub(crate) object_identity: Option<KboxlikeFdObjectIdentity>,
pub(crate) flags: Option<u64>,
pub(crate) cloexec: bool,
pub(crate) eventfd_count: Option<u64>,
pub(crate) eventfd_semaphore: Option<bool>,
pub(crate) timerfd: Option<KboxlikeTimerFdSnapshot>,
pub(crate) epoll_interests: Vec<KboxlikeEpollInterestSnapshot>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct KboxlikeFdObjectIdentity {
pub(crate) mnt_id: Option<u64>,
pub(crate) ino: Option<u64>,
pub(crate) eventfd_id: Option<u64>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KboxlikeTraceeFdSnapshot {
pub(crate) host_pid: i32,
pub(crate) model_pid: ProcessId,
pub(crate) model_thread_id: ModelThreadId,
pub(crate) fds: Vec<KboxlikeFdSnapshot>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KboxlikeFdShadowBackingPlan {
pub(crate) host_pid: i32,
pub(crate) model_pid: ProcessId,
pub(crate) model_thread_id: ModelThreadId,
pub(crate) fd: i32,
pub(crate) target: String,
pub(crate) cloexec: bool,
pub(crate) backing: ShadowBacking,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KboxlikeEpollInterestBackingPlan {
pub(crate) target_fd: i32,
pub(crate) events: u32,
pub(crate) data: u64,
pub(crate) target: Option<String>,
pub(crate) target_kind: Option<KboxlikeFdTargetKind>,
pub(crate) target_backing: Option<ShadowBacking>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KboxlikeEpollShadowBackingPlan {
pub(crate) fd_plan: KboxlikeFdShadowBackingPlan,
pub(crate) interests: Vec<KboxlikeEpollInterestBackingPlan>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KboxlikeFdQueuedDataPlan {
pub(crate) host_pid: i32,
pub(crate) model_pid: ProcessId,
pub(crate) model_thread_id: ModelThreadId,
pub(crate) receiver_fd: i32,
pub(crate) injector_fd: i32,
pub(crate) target: String,
pub(crate) receiver_backing: ShadowBacking,
pub(crate) injector_backing: ShadowBacking,
pub(crate) bytes: Vec<u8>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KboxlikeSocketIntOption {
pub(crate) level: i32,
pub(crate) name: i32,
pub(crate) value: i32,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KboxlikeFdSocketOptionsPlan {
pub(crate) host_pid: i32,
pub(crate) model_pid: ProcessId,
pub(crate) model_thread_id: ModelThreadId,
pub(crate) fd: i32,
pub(crate) target: String,
pub(crate) backing: ShadowBacking,
pub(crate) int_options: Vec<KboxlikeSocketIntOption>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KboxlikeFdSocketShutdownPlan {
pub(crate) host_pid: i32,
pub(crate) model_pid: ProcessId,
pub(crate) model_thread_id: ModelThreadId,
pub(crate) fd: i32,
pub(crate) target: String,
pub(crate) backing: ShadowBacking,
pub(crate) shutdown: KboxlikeSocketShutdownState,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KboxlikeUnixCredentials {
pub(crate) pid: i32,
pub(crate) uid: u32,
pub(crate) gid: u32,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KboxlikeFdScmRightsPlan {
pub(crate) host_pid: i32,
pub(crate) model_pid: ProcessId,
pub(crate) model_thread_id: ModelThreadId,
pub(crate) receiver_fd: i32,
pub(crate) injector_fd: i32,
pub(crate) target: String,
pub(crate) receiver_backing: ShadowBacking,
pub(crate) injector_backing: ShadowBacking,
pub(crate) data: Vec<u8>,
pub(crate) passed_fds: Vec<i32>,
pub(crate) passed_backings: Vec<ShadowBacking>,
pub(crate) credentials: Option<KboxlikeUnixCredentials>,
pub(crate) credential_identity: Option<KboxlikeTraceeIdentity>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, Default)]
pub(crate) struct KboxlikeStoppedTraceeSnapshot {
pub(crate) threads: Vec<KboxlikeThreadSnapshot>,
pub(crate) thread_kernel_states: Vec<KboxlikeThreadKernelStateSnapshot>,
pub(crate) memory_maps: Vec<KboxlikeTraceeMapSnapshot>,
pub(crate) fd_tables: Vec<KboxlikeTraceeFdSnapshot>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KboxlikeMemoryDump {
pub(crate) host_pid: i32,
pub(crate) model_pid: ProcessId,
pub(crate) start: u64,
pub(crate) bytes: Vec<u8>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KboxlikeMemoryDumpFailure {
pub(crate) host_pid: i32,
pub(crate) model_pid: ProcessId,
pub(crate) start: u64,
pub(crate) len: u64,
pub(crate) bytes_read: usize,
pub(crate) reason: &'static str,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct KboxlikeMemoryDumpCapture {
pub(crate) dumps: Vec<KboxlikeMemoryDump>,
pub(crate) failures: Vec<KboxlikeMemoryDumpFailure>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl KboxlikeMemoryDumpCapture {
pub(crate) fn captured_bytes(&self) -> u64 {
self.dumps.iter().map(|dump| dump.bytes.len() as u64).sum()
}
pub(crate) fn failed_bytes(&self) -> u64 {
self.failures.iter().map(|failure| failure.len).sum()
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct KboxlikeSharedMemoryDump {
pub(crate) host_pid: i32,
pub(crate) model_pid: ProcessId,
pub(crate) start: u64,
pub(crate) kind: KboxlikeSharedMappingKind,
pub(crate) path: Option<String>,
pub(crate) file_offset: u64,
pub(crate) dev: String,
pub(crate) inode: u64,
pub(crate) bytes: Vec<u8>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KboxlikeSharedMemoryDumpFailure {
pub(crate) host_pid: i32,
pub(crate) model_pid: ProcessId,
pub(crate) start: u64,
pub(crate) len: u64,
pub(crate) kind: KboxlikeSharedMappingKind,
pub(crate) path: Option<String>,
pub(crate) file_offset: u64,
pub(crate) dev: String,
pub(crate) inode: u64,
pub(crate) bytes_read: usize,
pub(crate) reason: &'static str,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct KboxlikeSharedMemoryDumpCapture {
pub(crate) dumps: Vec<KboxlikeSharedMemoryDump>,
pub(crate) failures: Vec<KboxlikeSharedMemoryDumpFailure>,
pub(crate) skipped_duplicate_extents: usize,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl KboxlikeSharedMemoryDumpCapture {
pub(crate) fn captured_bytes(&self) -> u64 {
self.dumps.iter().map(|dump| dump.bytes.len() as u64).sum()
}
pub(crate) fn failed_bytes(&self) -> u64 {
self.failures.iter().map(|failure| failure.len).sum()
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KboxlikeMemoryReadError {
pub(crate) bytes_read: usize,
pub(crate) reason: &'static str,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KboxlikeMemoryRestorePlan {
pub(crate) actions: Vec<KboxlikeMemoryRestoreAction>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum KboxlikeMemoryRestoreAction {
SkipKernelSynthetic {
host_pid: i32,
model_pid: ProcessId,
start: u64,
len: u64,
label: String,
},
SkipInaccessible {
host_pid: i32,
model_pid: ProcessId,
start: u64,
len: u64,
prot: i32,
},
DumpAnonymousPrivate {
host_pid: i32,
model_pid: ProcessId,
start: u64,
len: u64,
prot: i32,
label: Option<String>,
},
RemapCleanFile {
host_pid: i32,
model_pid: ProcessId,
start: u64,
len: u64,
prot: i32,
path: PathBuf,
file_offset: u64,
dev: String,
inode: u64,
},
DumpPrivateFileCow {
host_pid: i32,
model_pid: ProcessId,
start: u64,
len: u64,
prot: i32,
path: PathBuf,
file_offset: u64,
dev: String,
inode: u64,
reason: &'static str,
},
RecreateSharedObject {
host_pid: i32,
model_pid: ProcessId,
start: u64,
len: u64,
prot: i32,
kind: KboxlikeSharedMappingKind,
path: Option<String>,
file_offset: u64,
dev: String,
inode: u64,
},
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum KboxlikeSharedMappingKind {
Memfd,
Device,
File,
Anonymous,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KboxlikeMaterializedMemoryRestore {
pub(crate) actions: Vec<KboxlikeMaterializedMemoryRestoreAction>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum KboxlikeMaterializedMemoryRestoreAction {
ByteBacked {
host_pid: i32,
model_pid: ProcessId,
start: u64,
prot: i32,
bytes: Vec<u8>,
source: KboxlikeByteBackedRestoreSource,
},
RemapCleanFile {
host_pid: i32,
model_pid: ProcessId,
start: u64,
len: u64,
prot: i32,
path: PathBuf,
file_offset: u64,
dev: String,
inode: u64,
},
RecreateSharedObject {
host_pid: i32,
model_pid: ProcessId,
start: u64,
len: u64,
prot: i32,
kind: KboxlikeSharedMappingKind,
path: Option<String>,
file_offset: u64,
dev: String,
inode: u64,
},
SkipKernelSynthetic {
host_pid: i32,
model_pid: ProcessId,
start: u64,
len: u64,
label: String,
},
SkipInaccessible {
host_pid: i32,
model_pid: ProcessId,
start: u64,
len: u64,
prot: i32,
},
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum KboxlikeByteBackedRestoreSource {
AnonymousPrivate {
label: Option<String>,
},
PrivateFileCow {
path: PathBuf,
file_offset: u64,
dev: String,
inode: u64,
reason: &'static str,
},
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum KboxlikeRestoreMaterializeError {
MissingDump {
host_pid: i32,
model_pid: ProcessId,
start: u64,
len: u64,
},
DuplicateDump {
host_pid: i32,
model_pid: ProcessId,
start: u64,
},
ReusedDump {
host_pid: i32,
model_pid: ProcessId,
start: u64,
},
DumpLengthMismatch {
host_pid: i32,
model_pid: ProcessId,
start: u64,
expected: u64,
actual: usize,
},
UnusedDump {
host_pid: i32,
model_pid: ProcessId,
start: u64,
len: usize,
},
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) trait TraceeMapsReader {
fn maps_for_pid(&mut self, host_pid: i32) -> Result<Vec<KboxlikeMapEntry>, FdError>;
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) trait TraceeMemoryReader {
fn read_exact(
&mut self,
host_pid: i32,
addr: u64,
len: usize,
) -> Result<Vec<u8>, KboxlikeMemoryReadError>;
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) trait TraceeFdTableReader {
fn fd_table_for_tracee(
&mut self,
tracee: &KboxlikeTraceeIdentity,
) -> Result<KboxlikeTraceeFdSnapshot, FdError>;
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Debug, Default)]
pub(crate) struct ProcTraceeMapsReader;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Debug, Default)]
pub(crate) struct ProcessVmTraceeMemoryReader;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Debug, Default)]
pub(crate) struct ProcTraceeFdTableReader;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Debug, Default)]
struct EmptyTraceeFdTableReader;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeMapsReader for ProcTraceeMapsReader {
fn maps_for_pid(&mut self, host_pid: i32) -> Result<Vec<KboxlikeMapEntry>, FdError> {
read_proc_maps_for_pid(host_pid)
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeFdTableReader for ProcTraceeFdTableReader {
fn fd_table_for_tracee(
&mut self,
tracee: &KboxlikeTraceeIdentity,
) -> Result<KboxlikeTraceeFdSnapshot, FdError> {
read_proc_fd_table_for_tracee(tracee)
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeFdTableReader for EmptyTraceeFdTableReader {
fn fd_table_for_tracee(
&mut self,
tracee: &KboxlikeTraceeIdentity,
) -> Result<KboxlikeTraceeFdSnapshot, FdError> {
Ok(KboxlikeTraceeFdSnapshot {
host_pid: tracee.host_pid,
model_pid: tracee.model_pid,
model_thread_id: tracee.model_thread_id,
fds: Vec::new(),
})
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl TraceeMemoryReader for ProcessVmTraceeMemoryReader {
fn read_exact(
&mut self,
host_pid: i32,
addr: u64,
len: usize,
) -> Result<Vec<u8>, KboxlikeMemoryReadError> {
process_vm_read_exact(host_pid, addr, len)
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn capture_stopped_tracees(
supervisor: &KboxlikeSyscallSupervisor,
regs: &mut impl TraceeRegisterReader,
) -> Result<KboxlikeStoppedTraceeSnapshot, FdError> {
let mut maps = ProcTraceeMapsReader;
let mut fds = ProcTraceeFdTableReader;
capture_stopped_tracees_with_kernel_states_and_fds(
supervisor.registered_tracee_identities(),
supervisor.registered_thread_kernel_states(),
regs,
&mut maps,
&mut fds,
)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn capture_stopped_tracees_with(
tracees: Vec<KboxlikeTraceeIdentity>,
regs: &mut impl TraceeRegisterReader,
maps: &mut impl TraceeMapsReader,
) -> Result<KboxlikeStoppedTraceeSnapshot, FdError> {
let mut fds = EmptyTraceeFdTableReader;
capture_stopped_tracees_with_kernel_states_and_fds(tracees, Vec::new(), regs, maps, &mut fds)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn capture_stopped_tracees_with_kernel_states(
tracees: Vec<KboxlikeTraceeIdentity>,
kernel_states: Vec<KboxlikeThreadKernelStateSnapshot>,
regs: &mut impl TraceeRegisterReader,
maps: &mut impl TraceeMapsReader,
) -> Result<KboxlikeStoppedTraceeSnapshot, FdError> {
let mut fds = EmptyTraceeFdTableReader;
capture_stopped_tracees_with_kernel_states_and_fds(tracees, kernel_states, regs, maps, &mut fds)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn capture_stopped_tracees_with_kernel_states_and_fds(
tracees: Vec<KboxlikeTraceeIdentity>,
kernel_states: Vec<KboxlikeThreadKernelStateSnapshot>,
regs: &mut impl TraceeRegisterReader,
maps: &mut impl TraceeMapsReader,
fd_tables: &mut impl TraceeFdTableReader,
) -> Result<KboxlikeStoppedTraceeSnapshot, FdError> {
let mut threads = Vec::with_capacity(tracees.len());
let mut thread_kernel_states = Vec::with_capacity(tracees.len());
let mut memory_maps = Vec::with_capacity(tracees.len());
let mut captured_fd_tables = Vec::with_capacity(tracees.len());
let mut kernel_states_by_thread = BTreeMap::new();
for state in kernel_states {
if kernel_states_by_thread
.insert(state.model_thread_id, state)
.is_some()
{
return Err(FdError::TraceeInstallFailed(
"duplicate thread kernel state snapshot",
));
}
}
for tracee in tracees {
let thread_regs = regs.thread_regs(tracee.host_pid)?;
let entries = maps.maps_for_pid(tracee.host_pid)?;
let fd_table = fd_tables.fd_table_for_tracee(&tracee)?;
if fd_table.host_pid != tracee.host_pid
|| fd_table.model_pid != tracee.model_pid
|| fd_table.model_thread_id != tracee.model_thread_id
{
return Err(FdError::TraceeInstallFailed("fd table identity mismatch"));
}
let dump_candidate_bytes = entries
.iter()
.filter(|entry| entry.is_dump_candidate())
.map(KboxlikeMapEntry::len)
.sum();
threads.push(KboxlikeThreadSnapshot {
host_pid: tracee.host_pid,
model_pid: tracee.model_pid,
model_thread_id: tracee.model_thread_id,
regs: thread_regs,
});
let kernel_state = kernel_states_by_thread
.remove(&tracee.model_thread_id)
.unwrap_or(KboxlikeThreadKernelStateSnapshot {
host_pid: tracee.host_pid,
model_pid: tracee.model_pid,
model_thread_id: tracee.model_thread_id,
..KboxlikeThreadKernelStateSnapshot::default()
});
if kernel_state.host_pid != tracee.host_pid
|| kernel_state.model_pid != tracee.model_pid
|| kernel_state.model_thread_id != tracee.model_thread_id
{
return Err(FdError::TraceeInstallFailed(
"thread kernel state identity mismatch",
));
}
thread_kernel_states.push(kernel_state);
memory_maps.push(KboxlikeTraceeMapSnapshot {
host_pid: tracee.host_pid,
model_pid: tracee.model_pid,
model_thread_id: tracee.model_thread_id,
maps: entries,
dump_candidate_bytes,
});
captured_fd_tables.push(fd_table);
}
Ok(KboxlikeStoppedTraceeSnapshot {
threads,
thread_kernel_states,
memory_maps,
fd_tables: captured_fd_tables,
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn read_proc_fd_table_for_tracee(
tracee: &KboxlikeTraceeIdentity,
) -> Result<KboxlikeTraceeFdSnapshot, FdError> {
let mut fds = Vec::new();
let dir = format!("/proc/{}/fd", tracee.host_pid);
let entries = fs::read_dir(dir)
.map_err(|_| FdError::TraceeInstallFailed("failed to read proc fd directory"))?;
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 {
continue;
}
let target = fs::read_link(entry.path())
.map(|path| path.to_string_lossy().into_owned())
.unwrap_or_else(|_| "<unreadable>".to_string());
let fdinfo = read_proc_fdinfo(tracee.host_pid, fd);
let flags = fdinfo.as_deref().and_then(parse_proc_fdinfo_flags);
let object_identity = fdinfo
.as_deref()
.and_then(parse_proc_fdinfo_object_identity);
let eventfd_count = fdinfo.as_deref().and_then(parse_proc_fdinfo_eventfd_count);
let eventfd_semaphore = fdinfo
.as_deref()
.and_then(parse_proc_fdinfo_eventfd_semaphore);
let timerfd = fdinfo.as_deref().and_then(parse_proc_fdinfo_timerfd);
let epoll_interests = fdinfo
.as_deref()
.map(parse_proc_fdinfo_epoll_interests)
.unwrap_or_default();
fds.push(KboxlikeFdSnapshot {
host_pid: tracee.host_pid,
model_pid: tracee.model_pid,
model_thread_id: tracee.model_thread_id,
fd,
kind: classify_fd_target(&target),
target,
cloexec: flags.is_some_and(|flags| flags & libc::O_CLOEXEC as u64 != 0),
object_identity,
flags,
eventfd_count,
eventfd_semaphore,
timerfd,
epoll_interests,
});
}
fds.sort_by_key(|fd| fd.fd);
Ok(KboxlikeTraceeFdSnapshot {
host_pid: tracee.host_pid,
model_pid: tracee.model_pid,
model_thread_id: tracee.model_thread_id,
fds,
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn read_proc_fdinfo(host_pid: i32, fd: i32) -> Option<String> {
fs::read_to_string(format!("/proc/{host_pid}/fdinfo/{fd}")).ok()
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn parse_proc_fdinfo_object_identity(fdinfo: &str) -> Option<KboxlikeFdObjectIdentity> {
let mnt_id = parse_proc_fdinfo_decimal_field(fdinfo, "mnt_id:");
let ino = parse_proc_fdinfo_decimal_field(fdinfo, "ino:");
let eventfd_id = parse_proc_fdinfo_decimal_field(fdinfo, "eventfd-id:");
if mnt_id.is_none() && ino.is_none() && eventfd_id.is_none() {
return None;
}
Some(KboxlikeFdObjectIdentity {
mnt_id,
ino,
eventfd_id,
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn parse_proc_fdinfo_decimal_field(fdinfo: &str, prefix: &str) -> Option<u64> {
fdinfo.lines().find_map(|line| {
let value = line.strip_prefix(prefix)?.trim();
value.parse::<u64>().ok()
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn parse_proc_fdinfo_flags(fdinfo: &str) -> Option<u64> {
fdinfo.lines().find_map(|line| {
let value = line.strip_prefix("flags:")?.trim();
u64::from_str_radix(value, 8).ok()
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn parse_proc_fdinfo_eventfd_count(fdinfo: &str) -> Option<u64> {
fdinfo.lines().find_map(|line| {
let value = line.strip_prefix("eventfd-count:")?.trim();
u64::from_str_radix(value, 16)
.ok()
.or_else(|| value.parse::<u64>().ok())
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn parse_proc_fdinfo_eventfd_semaphore(fdinfo: &str) -> Option<bool> {
fdinfo.lines().find_map(|line| {
let value = line.strip_prefix("eventfd-semaphore:")?.trim();
Some(value != "0")
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn parse_proc_fdinfo_timerfd(fdinfo: &str) -> Option<KboxlikeTimerFdSnapshot> {
let mut clockid = None;
let mut ticks = None;
let mut settime_flags = None;
let mut it_value = None;
let mut it_interval = None;
for line in fdinfo.lines() {
let line = line.trim();
if let Some(value) = line.strip_prefix("clockid:") {
clockid = value.trim().parse::<i32>().ok();
} else if let Some(value) = line.strip_prefix("ticks:") {
ticks = value.trim().parse::<u64>().ok();
} else if let Some(value) = line.strip_prefix("settime flags:") {
let value = value.trim();
settime_flags = i32::from_str_radix(value, 16)
.ok()
.or_else(|| value.parse::<i32>().ok());
} else if let Some(value) = line.strip_prefix("it_value:") {
it_value = parse_timerfd_timespec(value.trim());
} else if let Some(value) = line.strip_prefix("it_interval:") {
it_interval = parse_timerfd_timespec(value.trim());
}
}
let (value_sec, value_nsec) = it_value?;
let (interval_sec, interval_nsec) = it_interval?;
Some(KboxlikeTimerFdSnapshot {
clockid: clockid?,
ticks: ticks?,
settime_flags: settime_flags?,
value_sec,
value_nsec,
interval_sec,
interval_nsec,
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn parse_timerfd_timespec(value: &str) -> Option<(i64, i64)> {
let value = value.strip_prefix('(')?.strip_suffix(')')?;
let (sec, nsec) = value.split_once(',')?;
Some((sec.trim().parse().ok()?, nsec.trim().parse().ok()?))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn parse_proc_fdinfo_epoll_interests(fdinfo: &str) -> Vec<KboxlikeEpollInterestSnapshot> {
fdinfo
.lines()
.filter_map(parse_proc_fdinfo_epoll_interest_line)
.collect()
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn parse_proc_fdinfo_epoll_interest_line(line: &str) -> Option<KboxlikeEpollInterestSnapshot> {
let line = line.trim_start();
let rest = line.strip_prefix("tfd:")?.trim_start();
let mut fields = rest.split_whitespace();
let target_fd = fields.next()?.parse::<i32>().ok()?;
let mut events = None;
let mut data = None;
while let Some(field) = fields.next() {
match field {
"events:" => {
let value = fields.next()?;
events = u32::from_str_radix(value, 16)
.ok()
.or_else(|| value.parse::<u32>().ok());
}
"data:" => {
let value = fields.next()?;
data = u64::from_str_radix(value, 16)
.ok()
.or_else(|| value.parse::<u64>().ok());
}
_ => {}
}
}
Some(KboxlikeEpollInterestSnapshot {
target_fd,
events: events?,
data: data?,
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn classify_fd_target(target: &str) -> KboxlikeFdTargetKind {
if target.starts_with("pipe:[") {
return KboxlikeFdTargetKind::Pipe;
}
if target.starts_with("socket:[") {
return KboxlikeFdTargetKind::Socket;
}
if target.starts_with("/memfd:") || target.contains("/memfd:") {
return KboxlikeFdTargetKind::Memfd;
}
if target.starts_with("/dev/") {
return KboxlikeFdTargetKind::Device;
}
if target.starts_with('/') {
return KboxlikeFdTargetKind::RegularPath;
}
if target == "anon_inode:[eventfd]" {
return KboxlikeFdTargetKind::EventFd;
}
if target == "anon_inode:[eventpoll]" {
return KboxlikeFdTargetKind::EventPoll;
}
if target == "anon_inode:[timerfd]" {
return KboxlikeFdTargetKind::TimerFd;
}
if target == "anon_inode:inotify" || target == "anon_inode:[inotify]" {
return KboxlikeFdTargetKind::Inotify;
}
if target == "anon_inode:[signalfd]" {
return KboxlikeFdTargetKind::SignalFd;
}
if target.starts_with("anon_inode:") {
return KboxlikeFdTargetKind::AnonInode;
}
KboxlikeFdTargetKind::Unknown
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn plan_pipe_shadow_backings(
fd_tables: &[KboxlikeTraceeFdSnapshot],
) -> Vec<KboxlikeFdShadowBackingPlan> {
let mut by_target = BTreeMap::<String, Vec<&KboxlikeFdSnapshot>>::new();
for table in fd_tables {
for fd in &table.fds {
if fd.kind == KboxlikeFdTargetKind::Pipe {
by_target.entry(fd.target.clone()).or_default().push(fd);
}
}
}
let mut plans = Vec::new();
for (target, fds) in by_target {
let Some(group) = pipe_shadow_group(&target) else {
continue;
};
let read_ends = fds
.iter()
.copied()
.filter(|fd| pipe_access_endpoint(fd.flags) == Some(0))
.collect::<Vec<_>>();
let write_ends = fds
.iter()
.copied()
.filter(|fd| pipe_access_endpoint(fd.flags) == Some(1))
.collect::<Vec<_>>();
if read_ends.is_empty() || write_ends.is_empty() {
continue;
}
for (fds, endpoint) in [(read_ends, 0u8), (write_ends, 1u8)] {
for fd in fds {
plans.push(KboxlikeFdShadowBackingPlan {
host_pid: fd.host_pid,
model_pid: fd.model_pid,
model_thread_id: fd.model_thread_id,
fd: fd.fd,
target: fd.target.clone(),
cloexec: fd.cloexec,
backing: ShadowBacking::HostPassthrough {
label: format!("pipe:{group}:{endpoint}"),
},
});
}
}
}
plans.sort_by_key(|plan| (plan.model_pid, plan.model_thread_id, plan.fd));
plans
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn plan_eventfd_shadow_backings(
fd_tables: &[KboxlikeTraceeFdSnapshot],
) -> Vec<KboxlikeFdShadowBackingPlan> {
let mut plans = Vec::new();
for table in fd_tables {
for fd in &table.fds {
if fd.kind != KboxlikeFdTargetKind::EventFd {
continue;
}
let Some(count) = fd.eventfd_count else {
continue;
};
let Ok(initval) = u32::try_from(count) else {
continue;
};
let mut flags = 0;
if fd
.flags
.is_some_and(|flags| flags & libc::O_NONBLOCK as u64 != 0)
{
flags |= libc::EFD_NONBLOCK;
}
if fd.eventfd_semaphore == Some(true) {
flags |= libc::EFD_SEMAPHORE;
}
plans.push(KboxlikeFdShadowBackingPlan {
host_pid: fd.host_pid,
model_pid: fd.model_pid,
model_thread_id: fd.model_thread_id,
fd: fd.fd,
target: fd.target.clone(),
cloexec: fd.cloexec,
backing: ShadowBacking::HostPassthrough {
label: format!("eventfd:init={initval},flags={flags}"),
},
});
}
}
plans.sort_by_key(|plan| (plan.model_pid, plan.model_thread_id, plan.fd));
plans
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn plan_timerfd_shadow_backings(
fd_tables: &[KboxlikeTraceeFdSnapshot],
) -> Vec<KboxlikeFdShadowBackingPlan> {
let mut plans = Vec::new();
for table in fd_tables {
for fd in &table.fds {
if fd.kind != KboxlikeFdTargetKind::TimerFd {
continue;
}
let Some(timerfd) = &fd.timerfd else {
continue;
};
let mut flags = 0;
if fd
.flags
.is_some_and(|flags| flags & libc::O_NONBLOCK as u64 != 0)
{
flags |= libc::TFD_NONBLOCK;
}
plans.push(KboxlikeFdShadowBackingPlan {
host_pid: fd.host_pid,
model_pid: fd.model_pid,
model_thread_id: fd.model_thread_id,
fd: fd.fd,
target: fd.target.clone(),
cloexec: fd.cloexec,
backing: ShadowBacking::HostPassthrough {
label: format!(
"timerfd:clockid={},flags={},value_sec={},value_nsec={},interval_sec={},interval_nsec={}",
timerfd.clockid,
flags,
timerfd.value_sec,
timerfd.value_nsec,
timerfd.interval_sec,
timerfd.interval_nsec
),
},
});
}
}
plans.sort_by_key(|plan| (plan.model_pid, plan.model_thread_id, plan.fd));
plans
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn plan_socketpair_shadow_backings(
fd_tables: &[KboxlikeTraceeFdSnapshot],
) -> Vec<KboxlikeFdShadowBackingPlan> {
let mut plans = Vec::new();
for table in fd_tables {
let mut socket_fds_by_target = BTreeMap::<String, Vec<&KboxlikeFdSnapshot>>::new();
for fd in &table.fds {
if fd.kind == KboxlikeFdTargetKind::Socket {
socket_fds_by_target
.entry(fd.target.clone())
.or_default()
.push(fd);
}
}
if socket_fds_by_target.len() != 2 {
continue;
}
let Some(group) = socketpair_shadow_group(socket_fds_by_target.keys()) else {
continue;
};
for (endpoint, (_target, fds)) in socket_fds_by_target.into_iter().enumerate() {
for fd in fds {
plans.push(KboxlikeFdShadowBackingPlan {
host_pid: fd.host_pid,
model_pid: fd.model_pid,
model_thread_id: fd.model_thread_id,
fd: fd.fd,
target: fd.target.clone(),
cloexec: fd.cloexec,
backing: ShadowBacking::HostPassthrough {
label: format!("socketpair:{group}:{endpoint}"),
},
});
}
}
}
plans.sort_by_key(|plan| (plan.model_pid, plan.model_thread_id, plan.fd));
plans
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn plan_live_socket_shadow_backings(
fd_tables: &[KboxlikeTraceeFdSnapshot],
) -> Vec<KboxlikeFdShadowBackingPlan> {
let mut plans = Vec::new();
for table in fd_tables {
for fd in &table.fds {
if fd.kind != KboxlikeFdTargetKind::Socket {
continue;
}
plans.push(KboxlikeFdShadowBackingPlan {
host_pid: fd.host_pid,
model_pid: fd.model_pid,
model_thread_id: fd.model_thread_id,
fd: fd.fd,
target: fd.target.clone(),
cloexec: fd.cloexec,
backing: ShadowBacking::HostPassthrough {
label: format!("livefd:{}:{}", fd.host_pid, fd.fd),
},
});
}
}
plans.sort_by_key(|plan| (plan.model_pid, plan.model_thread_id, plan.fd));
plans
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn plan_epoll_shadow_backings(
fd_tables: &[KboxlikeTraceeFdSnapshot],
fd_system: &KboxlikeFdSystem,
) -> Vec<KboxlikeEpollShadowBackingPlan> {
let mut plans = Vec::new();
for table in fd_tables {
for fd in &table.fds {
if fd.kind != KboxlikeFdTargetKind::EventPoll {
continue;
}
let interests = fd
.epoll_interests
.iter()
.map(|interest| {
let target = table.fds.iter().find(|candidate| {
candidate.model_pid == fd.model_pid && candidate.fd == interest.target_fd
});
KboxlikeEpollInterestBackingPlan {
target_fd: interest.target_fd,
events: interest.events,
data: interest.data,
target: target.map(|target| target.target.clone()),
target_kind: target.map(|target| target.kind),
target_backing: fd_system
.shadow_backing(fd.model_pid, interest.target_fd)
.ok()
.flatten()
.cloned(),
}
})
.collect::<Vec<_>>();
plans.push(KboxlikeEpollShadowBackingPlan {
fd_plan: KboxlikeFdShadowBackingPlan {
host_pid: fd.host_pid,
model_pid: fd.model_pid,
model_thread_id: fd.model_thread_id,
fd: fd.fd,
target: fd.target.clone(),
cloexec: fd.cloexec,
backing: ShadowBacking::HostPassthrough {
label: format!("epoll:epoll{}_{}", fd.host_pid, fd.fd),
},
},
interests,
});
}
}
plans.sort_by_key(|plan| {
(
plan.fd_plan.model_pid,
plan.fd_plan.model_thread_id,
plan.fd_plan.fd,
)
});
plans
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn plan_fd_queued_data_replay(
fd_tables: &[KboxlikeTraceeFdSnapshot],
fd_system: &KboxlikeFdSystem,
max_bytes_per_fd: usize,
) -> Vec<KboxlikeFdQueuedDataPlan> {
if max_bytes_per_fd == 0 {
return Vec::new();
}
let mut plans = Vec::new();
let mut captured_receivers = BTreeSet::<(ProcessId, String)>::new();
for table in fd_tables {
for fd in &table.fds {
if !matches!(
fd.kind,
KboxlikeFdTargetKind::Pipe | KboxlikeFdTargetKind::Socket
) {
continue;
}
if fd.kind == KboxlikeFdTargetKind::Pipe && pipe_access_endpoint(fd.flags) != Some(0) {
continue;
}
let Some(receiver_backing) = fd_system
.shadow_backing(fd.model_pid, fd.fd)
.ok()
.flatten()
.cloned()
else {
continue;
};
let Some(injector_backing) = peer_stream_backing(&receiver_backing) else {
continue;
};
let receiver_identity = shadow_backing_identity(&receiver_backing);
if !captured_receivers.insert((fd.model_pid, receiver_identity)) {
continue;
}
let Some(injector_fd) =
find_fd_with_backing(table, fd_system, fd.model_pid, &injector_backing)
else {
continue;
};
let Some(bytes) =
capture_queued_fd_bytes(fd.host_pid, fd.fd, fd.kind, max_bytes_per_fd)
else {
continue;
};
if bytes.is_empty() {
continue;
}
plans.push(KboxlikeFdQueuedDataPlan {
host_pid: fd.host_pid,
model_pid: fd.model_pid,
model_thread_id: fd.model_thread_id,
receiver_fd: fd.fd,
injector_fd,
target: fd.target.clone(),
receiver_backing,
injector_backing,
bytes,
});
}
}
plans.sort_by_key(|plan| (plan.model_pid, plan.model_thread_id, plan.receiver_fd));
plans
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn plan_fd_socket_options_replay(
fd_tables: &[KboxlikeTraceeFdSnapshot],
fd_system: &KboxlikeFdSystem,
) -> Vec<KboxlikeFdSocketOptionsPlan> {
let mut plans = Vec::new();
let mut captured_endpoints = BTreeSet::<(ProcessId, String)>::new();
for table in fd_tables {
for fd in &table.fds {
if fd.kind != KboxlikeFdTargetKind::Socket {
continue;
}
let Some(backing) = fd_system
.shadow_backing(fd.model_pid, fd.fd)
.ok()
.flatten()
.cloned()
else {
continue;
};
if !is_socketpair_backing(&backing) {
continue;
}
let backing_identity = shadow_backing_identity(&backing);
if !captured_endpoints.insert((fd.model_pid, backing_identity)) {
continue;
}
let Some(int_options) = capture_socket_int_options(fd.host_pid, fd.fd) else {
continue;
};
if int_options.is_empty() {
continue;
}
plans.push(KboxlikeFdSocketOptionsPlan {
host_pid: fd.host_pid,
model_pid: fd.model_pid,
model_thread_id: fd.model_thread_id,
fd: fd.fd,
target: fd.target.clone(),
backing,
int_options,
});
}
}
plans.sort_by_key(|plan| (plan.model_pid, plan.model_thread_id, plan.fd));
plans
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn plan_fd_socket_shutdown_replay(
fd_tables: &[KboxlikeTraceeFdSnapshot],
fd_system: &KboxlikeFdSystem,
) -> Vec<KboxlikeFdSocketShutdownPlan> {
let mut plans = Vec::new();
let mut captured_endpoints = BTreeSet::<(ProcessId, String)>::new();
for table in fd_tables {
for fd in &table.fds {
if fd.kind != KboxlikeFdTargetKind::Socket {
continue;
}
let Some(backing) = fd_system
.shadow_backing(fd.model_pid, fd.fd)
.ok()
.flatten()
.cloned()
else {
continue;
};
if !is_socketpair_backing(&backing) {
continue;
}
let backing_identity = shadow_backing_identity(&backing);
if !captured_endpoints.insert((fd.model_pid, backing_identity)) {
continue;
}
let Some(shutdown) = fd_system
.socket_shutdown(fd.model_pid, fd.fd)
.ok()
.flatten()
else {
continue;
};
if shutdown.is_empty() {
continue;
}
plans.push(KboxlikeFdSocketShutdownPlan {
host_pid: fd.host_pid,
model_pid: fd.model_pid,
model_thread_id: fd.model_thread_id,
fd: fd.fd,
target: fd.target.clone(),
backing,
shutdown,
});
}
}
plans.sort_by_key(|plan| (plan.model_pid, plan.model_thread_id, plan.fd));
plans
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn plan_fd_scm_rights_replay(
fd_tables: &[KboxlikeTraceeFdSnapshot],
fd_system: &KboxlikeFdSystem,
max_data_bytes: usize,
max_fds: usize,
) -> Vec<KboxlikeFdScmRightsPlan> {
if max_data_bytes == 0 || max_fds == 0 {
return Vec::new();
}
let mut plans = Vec::new();
let mut captured_receivers = BTreeSet::<(ProcessId, String)>::new();
for table in fd_tables {
for fd in &table.fds {
if fd.kind != KboxlikeFdTargetKind::Socket {
continue;
}
let Some(receiver_backing) = fd_system
.shadow_backing(fd.model_pid, fd.fd)
.ok()
.flatten()
.cloned()
else {
continue;
};
if !is_socketpair_backing(&receiver_backing) {
continue;
}
let receiver_identity = shadow_backing_identity(&receiver_backing);
if !captured_receivers.insert((fd.model_pid, receiver_identity)) {
continue;
}
let Some(injector_backing) = peer_stream_backing(&receiver_backing) else {
continue;
};
let Some(injector_fd) =
find_fd_with_backing(table, fd_system, fd.model_pid, &injector_backing)
else {
continue;
};
let Some((data, passed_fds, passed_backings, credentials)) = capture_scm_rights_message(
table,
fd_system,
fd.host_pid,
fd.model_pid,
fd.fd,
max_data_bytes,
max_fds,
) else {
continue;
};
if data.is_empty() || passed_fds.is_empty() {
continue;
}
let credential_identity = credentials
.as_ref()
.and_then(|credentials| tracee_identity_for_host_pid(fd_tables, credentials.pid));
plans.push(KboxlikeFdScmRightsPlan {
host_pid: fd.host_pid,
model_pid: fd.model_pid,
model_thread_id: fd.model_thread_id,
receiver_fd: fd.fd,
injector_fd,
target: fd.target.clone(),
receiver_backing,
injector_backing,
data,
passed_fds,
passed_backings,
credentials,
credential_identity,
});
}
}
plans.sort_by_key(|plan| (plan.model_pid, plan.model_thread_id, plan.receiver_fd));
plans
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn tracee_identity_for_host_pid(
fd_tables: &[KboxlikeTraceeFdSnapshot],
host_pid: i32,
) -> Option<KboxlikeTraceeIdentity> {
fd_tables.iter().find_map(|table| {
(table.host_pid == host_pid).then_some(KboxlikeTraceeIdentity {
host_pid: table.host_pid,
model_pid: table.model_pid,
model_thread_id: table.model_thread_id,
})
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn pipe_access_endpoint(flags: Option<u64>) -> Option<u8> {
match flags? & libc::O_ACCMODE as u64 {
mode if mode == libc::O_RDONLY as u64 => Some(0),
mode if mode == libc::O_WRONLY as u64 => Some(1),
_ => None,
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn pipe_shadow_group(target: &str) -> Option<String> {
let inode = target.strip_prefix("pipe:[")?.strip_suffix(']')?;
if inode.is_empty() || !inode.chars().all(|ch| ch.is_ascii_digit()) {
return None;
}
Some(format!("pipe{inode}"))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn socketpair_shadow_group<'a>(targets: impl Iterator<Item = &'a String>) -> Option<String> {
let mut inodes = Vec::new();
for target in targets {
let inode = target.strip_prefix("socket:[")?.strip_suffix(']')?;
if inode.is_empty() || !inode.chars().all(|ch| ch.is_ascii_digit()) {
return None;
}
inodes.push(inode.to_string());
}
if inodes.len() != 2 {
return None;
}
Some(format!("socketpair{}_{}", inodes[0], inodes[1]))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn peer_stream_backing(backing: &ShadowBacking) -> Option<ShadowBacking> {
let ShadowBacking::HostPassthrough { label } = backing else {
return None;
};
let (prefix, rest) = label.split_once(':')?;
if prefix != "pipe" && prefix != "socketpair" {
return None;
}
let (group, endpoint) = rest.rsplit_once(':')?;
let peer_endpoint = match endpoint {
"0" => "1",
"1" => "0",
_ => return None,
};
Some(ShadowBacking::HostPassthrough {
label: format!("{prefix}:{group}:{peer_endpoint}"),
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn is_socketpair_backing(backing: &ShadowBacking) -> bool {
matches!(
backing,
ShadowBacking::HostPassthrough { label } if label.starts_with("socketpair:")
)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn find_fd_with_backing(
table: &KboxlikeTraceeFdSnapshot,
fd_system: &KboxlikeFdSystem,
model_pid: ProcessId,
backing: &ShadowBacking,
) -> Option<i32> {
table
.fds
.iter()
.filter(|fd| fd.model_pid == model_pid)
.find(|fd| {
fd_system
.shadow_backing(model_pid, fd.fd)
.ok()
.flatten()
.is_some_and(|candidate| candidate == backing)
})
.map(|fd| fd.fd)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn capture_queued_fd_bytes(
host_pid: i32,
fd: i32,
kind: KboxlikeFdTargetKind,
max_bytes: usize,
) -> Option<Vec<u8>> {
let duplicated = duplicate_tracee_fd(host_pid, fd).ok()?;
let available = queued_fd_bytes_available(duplicated.as_raw_fd()).ok()?;
if available == 0 {
return None;
}
let len = available.min(max_bytes);
match kind {
KboxlikeFdTargetKind::Pipe => read_queued_bytes(duplicated.as_raw_fd(), len).ok(),
KboxlikeFdTargetKind::Socket => peek_socket_bytes(duplicated.as_raw_fd(), len).ok(),
_ => None,
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn capture_socket_int_options(host_pid: i32, fd: i32) -> Option<Vec<KboxlikeSocketIntOption>> {
let duplicated = duplicate_tracee_fd(host_pid, fd).ok()?;
let mut options = Vec::new();
for (level, name) in socket_int_options_to_capture() {
if let Ok(value) = get_socket_int_option(duplicated.as_raw_fd(), level, name) {
options.push(KboxlikeSocketIntOption { level, name, value });
}
}
Some(options)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn capture_scm_rights_message(
table: &KboxlikeTraceeFdSnapshot,
fd_system: &KboxlikeFdSystem,
host_pid: i32,
model_pid: ProcessId,
fd: i32,
max_data_bytes: usize,
max_fds: usize,
) -> Option<(
Vec<u8>,
Vec<i32>,
Vec<ShadowBacking>,
Option<KboxlikeUnixCredentials>,
)> {
let duplicated = duplicate_tracee_fd(host_pid, fd).ok()?;
let fd_bytes = max_fds.checked_mul(std::mem::size_of::<libc::c_int>())?;
let cmsg_space = unsafe {
libc::CMSG_SPACE(fd_bytes as libc::c_uint) as usize
+ libc::CMSG_SPACE(std::mem::size_of::<libc::ucred>() as libc::c_uint) as usize
};
let mut control = vec![0u8; cmsg_space];
let mut data = vec![0u8; max_data_bytes];
let mut iov = libc::iovec {
iov_base: data.as_mut_ptr() as *mut libc::c_void,
iov_len: data.len(),
};
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(
duplicated.as_raw_fd(),
&mut msg,
libc::MSG_PEEK | libc::MSG_DONTWAIT,
)
};
if read <= 0 {
return None;
}
data.truncate(read as usize);
let received = received_scm_rights_fds(&msg, max_fds);
if received.is_empty() {
return None;
}
let credentials = received_scm_credentials(&msg);
map_received_fds_to_backings(table, fd_system, model_pid, received)
.map(|(fds, backings)| (data, fds, backings, credentials))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn received_scm_rights_fds(msg: &libc::msghdr, max_fds: usize) -> Vec<OwnedFd> {
let mut fds = Vec::new();
let mut cmsg = unsafe { libc::CMSG_FIRSTHDR(msg) };
while !cmsg.is_null() && fds.len() < max_fds {
let header = unsafe { &*cmsg };
if header.cmsg_level == libc::SOL_SOCKET && header.cmsg_type == libc::SCM_RIGHTS {
let data_len = header.cmsg_len as usize - unsafe { libc::CMSG_LEN(0) as usize };
let fd_count = data_len / std::mem::size_of::<libc::c_int>();
let data = unsafe { libc::CMSG_DATA(cmsg) as *const libc::c_int };
for index in 0..fd_count.min(max_fds - fds.len()) {
let raw = unsafe { data.add(index).read_unaligned() };
if raw >= 0 {
fds.push(unsafe { OwnedFd::from_raw_fd(raw) });
}
}
}
cmsg = unsafe { libc::CMSG_NXTHDR(msg, cmsg) };
}
fds
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn received_scm_credentials(msg: &libc::msghdr) -> Option<KboxlikeUnixCredentials> {
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_CREDENTIALS {
let data_len = header.cmsg_len as usize - unsafe { libc::CMSG_LEN(0) as usize };
if data_len >= std::mem::size_of::<libc::ucred>() {
let creds =
unsafe { (libc::CMSG_DATA(cmsg) as *const libc::ucred).read_unaligned() };
return Some(KboxlikeUnixCredentials {
pid: creds.pid,
uid: creds.uid,
gid: creds.gid,
});
}
}
cmsg = unsafe { libc::CMSG_NXTHDR(msg, cmsg) };
}
None
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn map_received_fds_to_backings(
table: &KboxlikeTraceeFdSnapshot,
fd_system: &KboxlikeFdSystem,
model_pid: ProcessId,
received: Vec<OwnedFd>,
) -> Option<(Vec<i32>, Vec<ShadowBacking>)> {
let mut fds = Vec::new();
let mut backings = Vec::new();
for received_fd in received {
let target = fs::read_link(format!("/proc/self/fd/{}", received_fd.as_raw_fd()))
.ok()?
.to_string_lossy()
.into_owned();
let object_identity = read_proc_fdinfo(unsafe { libc::getpid() }, received_fd.as_raw_fd())
.and_then(|fdinfo| parse_proc_fdinfo_object_identity(&fdinfo));
let mut matches_by_backing = BTreeMap::<String, (i32, ShadowBacking)>::new();
for (fd, backing) in table
.fds
.iter()
.filter(|fd| fd.model_pid == model_pid && fd.target == target)
.filter(|fd| {
object_identity
.map(|identity| fd.object_identity == Some(identity))
.unwrap_or(true)
})
.filter_map(|fd| {
fd_system
.shadow_backing(model_pid, fd.fd)
.ok()
.flatten()
.cloned()
.map(|backing| (fd.fd, backing))
})
{
let identity = shadow_backing_identity(&backing);
matches_by_backing
.entry(identity)
.and_modify(|(existing_fd, _)| {
*existing_fd = (*existing_fd).min(fd);
})
.or_insert((fd, backing));
}
if matches_by_backing.len() != 1 {
return None;
}
let (_, (fd, backing)) = matches_by_backing
.pop_first()
.expect("checked unique backing");
fds.push(fd);
backings.push(backing);
}
Some((fds, backings))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn socket_int_options_to_capture() -> [(i32, i32); 1] {
[(libc::SOL_SOCKET, libc::SO_PASSCRED)]
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn get_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 capture socket option",
));
}
Ok(value)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn duplicate_tracee_fd(host_pid: i32, fd: i32) -> Result<OwnedFd, FdError> {
let pidfd = unsafe { libc::syscall(libc::SYS_pidfd_open, host_pid, 0) };
if pidfd >= 0 {
let pidfd = unsafe { OwnedFd::from_raw_fd(pidfd as i32) };
let duplicated = unsafe {
libc::syscall(
libc::SYS_pidfd_getfd,
pidfd.as_raw_fd(),
fd,
0 as libc::c_uint,
)
};
if duplicated >= 0 {
return Ok(unsafe { OwnedFd::from_raw_fd(duplicated as i32) });
}
}
fs::File::open(format!("/proc/{host_pid}/fd/{fd}"))
.map(OwnedFd::from)
.map_err(|_| FdError::TraceeInstallFailed("failed to duplicate tracee fd"))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn queued_fd_bytes_available(fd: i32) -> Result<usize, FdError> {
let mut available = 0 as libc::c_int;
let rc = unsafe { libc::ioctl(fd, libc::FIONREAD, &mut available) };
if rc < 0 {
return Err(FdError::TraceeInstallFailed(
"failed to query queued fd bytes",
));
}
usize::try_from(available).map_err(|_| FdError::TraceeInstallFailed("invalid queued fd bytes"))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn read_queued_bytes(fd: i32, len: usize) -> Result<Vec<u8>, FdError> {
let mut bytes = vec![0u8; len];
let mut read_total = 0usize;
while read_total < len {
let rc = unsafe {
libc::read(
fd,
bytes[read_total..].as_mut_ptr() as *mut libc::c_void,
len - read_total,
)
};
if rc < 0 {
return Err(FdError::TraceeInstallFailed(
"failed to read queued pipe bytes",
));
}
if rc == 0 {
bytes.truncate(read_total);
return Ok(bytes);
}
read_total += rc as usize;
}
Ok(bytes)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn peek_socket_bytes(fd: i32, len: usize) -> Result<Vec<u8>, FdError> {
let mut bytes = vec![0u8; len];
let rc = unsafe {
libc::recv(
fd,
bytes.as_mut_ptr() as *mut libc::c_void,
bytes.len(),
libc::MSG_PEEK | libc::MSG_DONTWAIT,
)
};
if rc < 0 {
return Err(FdError::TraceeInstallFailed(
"failed to peek queued socket bytes",
));
}
bytes.truncate(rc as usize);
Ok(bytes)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn apply_fd_shadow_backing_plans(
fd_system: &mut KboxlikeFdSystem,
plans: &[KboxlikeFdShadowBackingPlan],
) -> Result<usize, FdError> {
let mut first_descriptor_by_backing = BTreeMap::<String, (ProcessId, i32)>::new();
for plan in plans {
let identity = shadow_backing_identity(&plan.backing);
if let Some((source_pid, source_fd)) = first_descriptor_by_backing.get(&identity).copied() {
fd_system.alias_fd_from(
source_pid,
source_fd,
plan.model_pid,
plan.fd,
plan.cloexec,
)?;
} else {
fd_system.insert_shadow_fd_with_backing(
plan.model_pid,
plan.fd,
None,
ShadowObject {
kind: ShadowKind::HostPassthrough,
supervisor_fd: plan.fd,
},
plan.backing.clone(),
plan.cloexec,
)?;
first_descriptor_by_backing.insert(identity, (plan.model_pid, plan.fd));
}
}
Ok(plans.len())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn shadow_backing_identity(backing: &ShadowBacking) -> String {
match backing {
ShadowBacking::HostPassthrough { label } => format!("host:{label}"),
ShadowBacking::GuestPath { path, writable } => format!("guest:{writable}:{path}"),
ShadowBacking::HostPath { path, writable } => format!("hostpath:{writable}:{path}"),
ShadowBacking::Ephemeral => "ephemeral".to_string(),
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn capture_dump_candidate_memory(
snapshot: &KboxlikeStoppedTraceeSnapshot,
) -> KboxlikeMemoryDumpCapture {
let mut reader = ProcessVmTraceeMemoryReader;
capture_dump_candidate_memory_with(&snapshot.memory_maps, &mut reader)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn capture_dump_candidate_memory_with(
memory_maps: &[KboxlikeTraceeMapSnapshot],
reader: &mut impl TraceeMemoryReader,
) -> KboxlikeMemoryDumpCapture {
let mut capture = KboxlikeMemoryDumpCapture::default();
for tracee_maps in memory_maps {
for entry in tracee_maps
.maps
.iter()
.filter(|entry| entry.is_dump_candidate())
{
let Ok(len) = usize::try_from(entry.len()) else {
capture.failures.push(KboxlikeMemoryDumpFailure {
host_pid: tracee_maps.host_pid,
model_pid: tracee_maps.model_pid,
start: entry.start,
len: entry.len(),
bytes_read: 0,
reason: "mapping length overflows usize",
});
continue;
};
match reader.read_exact(tracee_maps.host_pid, entry.start, len) {
Ok(bytes) => capture.dumps.push(KboxlikeMemoryDump {
host_pid: tracee_maps.host_pid,
model_pid: tracee_maps.model_pid,
start: entry.start,
bytes,
}),
Err(err) => capture.failures.push(KboxlikeMemoryDumpFailure {
host_pid: tracee_maps.host_pid,
model_pid: tracee_maps.model_pid,
start: entry.start,
len: entry.len(),
bytes_read: err.bytes_read,
reason: err.reason,
}),
}
}
}
capture
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn capture_shared_object_memory(
plan: &KboxlikeMemoryRestorePlan,
) -> KboxlikeSharedMemoryDumpCapture {
let mut reader = ProcessVmTraceeMemoryReader;
capture_shared_object_memory_with(plan, &mut reader)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn capture_shared_object_memory_with(
plan: &KboxlikeMemoryRestorePlan,
reader: &mut impl TraceeMemoryReader,
) -> KboxlikeSharedMemoryDumpCapture {
let mut capture = KboxlikeSharedMemoryDumpCapture::default();
let mut seen_extents = BTreeSet::new();
for action in &plan.actions {
let KboxlikeMemoryRestoreAction::RecreateSharedObject {
host_pid,
model_pid,
start,
len,
kind,
path,
file_offset,
dev,
inode,
..
} = action
else {
continue;
};
let extent_key = (*kind, path.clone(), dev.clone(), *inode, *file_offset, *len);
if !seen_extents.insert(extent_key) {
capture.skipped_duplicate_extents += 1;
continue;
}
let Ok(len_usize) = usize::try_from(*len) else {
capture.failures.push(KboxlikeSharedMemoryDumpFailure {
host_pid: *host_pid,
model_pid: *model_pid,
start: *start,
len: *len,
kind: *kind,
path: path.clone(),
file_offset: *file_offset,
dev: dev.clone(),
inode: *inode,
bytes_read: 0,
reason: "shared mapping length overflows usize",
});
continue;
};
match reader.read_exact(*host_pid, *start, len_usize) {
Ok(bytes) => capture.dumps.push(KboxlikeSharedMemoryDump {
host_pid: *host_pid,
model_pid: *model_pid,
start: *start,
kind: *kind,
path: path.clone(),
file_offset: *file_offset,
dev: dev.clone(),
inode: *inode,
bytes,
}),
Err(err) => capture.failures.push(KboxlikeSharedMemoryDumpFailure {
host_pid: *host_pid,
model_pid: *model_pid,
start: *start,
len: *len,
kind: *kind,
path: path.clone(),
file_offset: *file_offset,
dev: dev.clone(),
inode: *inode,
bytes_read: err.bytes_read,
reason: err.reason,
}),
}
}
capture
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn classify_memory_restore_actions(
memory_maps: &[KboxlikeTraceeMapSnapshot],
) -> KboxlikeMemoryRestorePlan {
let actions = memory_maps
.iter()
.flat_map(|tracee_maps| {
tracee_maps.maps.iter().map(|entry| {
classify_memory_map_restore_action(
tracee_maps.host_pid,
tracee_maps.model_pid,
entry,
)
})
})
.collect();
KboxlikeMemoryRestorePlan { actions }
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn classify_memory_map_restore_action(
host_pid: i32,
model_pid: ProcessId,
entry: &KboxlikeMapEntry,
) -> KboxlikeMemoryRestoreAction {
if entry.path.as_deref() == Some("[vdso]") {
return KboxlikeMemoryRestoreAction::DumpAnonymousPrivate {
host_pid,
model_pid,
start: entry.start,
len: entry.len(),
prot: entry.perms.prot(),
label: entry.path.clone(),
};
}
if entry.is_kernel_synthetic() {
return KboxlikeMemoryRestoreAction::SkipKernelSynthetic {
host_pid,
model_pid,
start: entry.start,
len: entry.len(),
label: entry.path.clone().unwrap_or_default(),
};
}
if !entry.perms.read {
return KboxlikeMemoryRestoreAction::SkipInaccessible {
host_pid,
model_pid,
start: entry.start,
len: entry.len(),
prot: entry.perms.prot(),
};
}
let prot = entry.perms.prot();
if entry.perms.shared {
return KboxlikeMemoryRestoreAction::RecreateSharedObject {
host_pid,
model_pid,
start: entry.start,
len: entry.len(),
prot,
kind: shared_mapping_kind(entry),
path: entry.path.clone(),
file_offset: entry.file_offset,
dev: entry.dev.clone(),
inode: entry.inode,
};
}
let Some(path) = entry.file_path() else {
return KboxlikeMemoryRestoreAction::DumpAnonymousPrivate {
host_pid,
model_pid,
start: entry.start,
len: entry.len(),
prot,
label: entry.path.clone(),
};
};
if entry
.path
.as_deref()
.is_some_and(|path| path.ends_with(" (deleted)"))
{
return KboxlikeMemoryRestoreAction::DumpPrivateFileCow {
host_pid,
model_pid,
start: entry.start,
len: entry.len(),
prot,
path,
file_offset: entry.file_offset,
dev: entry.dev.clone(),
inode: entry.inode,
reason: "deleted file mapping cannot be reopened reliably",
};
}
if entry.perms.write {
return KboxlikeMemoryRestoreAction::DumpPrivateFileCow {
host_pid,
model_pid,
start: entry.start,
len: entry.len(),
prot,
path,
file_offset: entry.file_offset,
dev: entry.dev.clone(),
inode: entry.inode,
reason: "writable private file mapping may contain COW bytes",
};
}
if !entry.perms.exec {
return KboxlikeMemoryRestoreAction::DumpPrivateFileCow {
host_pid,
model_pid,
start: entry.start,
len: entry.len(),
prot,
path,
file_offset: entry.file_offset,
dev: entry.dev.clone(),
inode: entry.inode,
reason: "non-executable private file mapping may contain relocated COW bytes",
};
}
KboxlikeMemoryRestoreAction::RemapCleanFile {
host_pid,
model_pid,
start: entry.start,
len: entry.len(),
prot,
path,
file_offset: entry.file_offset,
dev: entry.dev.clone(),
inode: entry.inode,
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn shared_mapping_kind(entry: &KboxlikeMapEntry) -> KboxlikeSharedMappingKind {
let Some(path) = entry.path.as_deref() else {
return KboxlikeSharedMappingKind::Anonymous;
};
if path.contains("memfd:") {
return KboxlikeSharedMappingKind::Memfd;
}
if path.starts_with("/dev/") {
return KboxlikeSharedMappingKind::Device;
}
KboxlikeSharedMappingKind::File
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn materialize_memory_restore_actions(
plan: &KboxlikeMemoryRestorePlan,
dumps: &KboxlikeMemoryDumpCapture,
) -> Result<KboxlikeMaterializedMemoryRestore, KboxlikeRestoreMaterializeError> {
let dump_by_start = index_memory_dumps(&dumps.dumps)?;
let mut used_dump_starts = BTreeSet::new();
let mut actions = Vec::with_capacity(plan.actions.len());
for action in &plan.actions {
actions.push(materialize_memory_restore_action(
action,
&dump_by_start,
&mut used_dump_starts,
)?);
}
for dump in &dumps.dumps {
if !used_dump_starts.contains(&(dump.host_pid, dump.model_pid, dump.start)) {
return Err(KboxlikeRestoreMaterializeError::UnusedDump {
host_pid: dump.host_pid,
model_pid: dump.model_pid,
start: dump.start,
len: dump.bytes.len(),
});
}
}
Ok(KboxlikeMaterializedMemoryRestore { actions })
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn index_memory_dumps(
dumps: &[KboxlikeMemoryDump],
) -> Result<BTreeMap<(i32, ProcessId, u64), &KboxlikeMemoryDump>, KboxlikeRestoreMaterializeError> {
let mut dump_by_start = BTreeMap::new();
for dump in dumps {
let key = (dump.host_pid, dump.model_pid, dump.start);
if dump_by_start.insert(key, dump).is_some() {
return Err(KboxlikeRestoreMaterializeError::DuplicateDump {
host_pid: dump.host_pid,
model_pid: dump.model_pid,
start: dump.start,
});
}
}
Ok(dump_by_start)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn materialize_memory_restore_action(
action: &KboxlikeMemoryRestoreAction,
dumps: &BTreeMap<(i32, ProcessId, u64), &KboxlikeMemoryDump>,
used_dump_starts: &mut BTreeSet<(i32, ProcessId, u64)>,
) -> Result<KboxlikeMaterializedMemoryRestoreAction, KboxlikeRestoreMaterializeError> {
match action {
KboxlikeMemoryRestoreAction::DumpAnonymousPrivate {
host_pid,
model_pid,
start,
len,
prot,
label,
} => {
let mut bytes =
take_matching_dump(dumps, used_dump_starts, *host_pid, *model_pid, *start, *len)?;
if label.as_deref() == Some("[vdso]") {
patch_vdso_syscall_fallbacks(&mut bytes);
}
Ok(KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: *host_pid,
model_pid: *model_pid,
start: *start,
prot: *prot,
bytes,
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate {
label: label.clone(),
},
})
}
KboxlikeMemoryRestoreAction::DumpPrivateFileCow {
host_pid,
model_pid,
start,
len,
prot,
path,
file_offset,
dev,
inode,
reason,
} => Ok(KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: *host_pid,
model_pid: *model_pid,
start: *start,
prot: *prot,
bytes: take_matching_dump(
dumps,
used_dump_starts,
*host_pid,
*model_pid,
*start,
*len,
)?,
source: KboxlikeByteBackedRestoreSource::PrivateFileCow {
path: path.clone(),
file_offset: *file_offset,
dev: dev.clone(),
inode: *inode,
reason,
},
}),
KboxlikeMemoryRestoreAction::RemapCleanFile {
host_pid,
model_pid,
start,
len,
prot,
path,
file_offset,
dev,
inode,
} => Ok(KboxlikeMaterializedMemoryRestoreAction::RemapCleanFile {
host_pid: *host_pid,
model_pid: *model_pid,
start: *start,
len: *len,
prot: *prot,
path: path.clone(),
file_offset: *file_offset,
dev: dev.clone(),
inode: *inode,
}),
KboxlikeMemoryRestoreAction::RecreateSharedObject {
host_pid,
model_pid,
start,
len,
prot,
kind,
path,
file_offset,
dev,
inode,
} => Ok(
KboxlikeMaterializedMemoryRestoreAction::RecreateSharedObject {
host_pid: *host_pid,
model_pid: *model_pid,
start: *start,
len: *len,
prot: *prot,
kind: *kind,
path: path.clone(),
file_offset: *file_offset,
dev: dev.clone(),
inode: *inode,
},
),
KboxlikeMemoryRestoreAction::SkipKernelSynthetic {
host_pid,
model_pid,
start,
len,
label,
} => Ok(
KboxlikeMaterializedMemoryRestoreAction::SkipKernelSynthetic {
host_pid: *host_pid,
model_pid: *model_pid,
start: *start,
len: *len,
label: label.clone(),
},
),
KboxlikeMemoryRestoreAction::SkipInaccessible {
host_pid,
model_pid,
start,
len,
prot,
} => Ok(KboxlikeMaterializedMemoryRestoreAction::SkipInaccessible {
host_pid: *host_pid,
model_pid: *model_pid,
start: *start,
len: *len,
prot: *prot,
}),
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn take_matching_dump(
dumps: &BTreeMap<(i32, ProcessId, u64), &KboxlikeMemoryDump>,
used_dump_starts: &mut BTreeSet<(i32, ProcessId, u64)>,
host_pid: i32,
model_pid: ProcessId,
start: u64,
len: u64,
) -> Result<Vec<u8>, KboxlikeRestoreMaterializeError> {
let key = (host_pid, model_pid, start);
let dump = dumps
.get(&key)
.ok_or(KboxlikeRestoreMaterializeError::MissingDump {
host_pid,
model_pid,
start,
len,
})?;
if dump.bytes.len() as u64 != len {
return Err(KboxlikeRestoreMaterializeError::DumpLengthMismatch {
host_pid,
model_pid,
start,
expected: len,
actual: dump.bytes.len(),
});
}
if !used_dump_starts.insert(key) {
return Err(KboxlikeRestoreMaterializeError::ReusedDump {
host_pid,
model_pid,
start,
});
}
Ok(dump.bytes.clone())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn patch_vdso_syscall_fallbacks(bytes: &mut [u8]) -> usize {
if bytes.len() < 0x40
|| bytes.get(0..4) != Some(b"\x7fELF")
|| bytes.get(4).copied() != Some(2)
|| bytes.get(5).copied() != Some(1)
{
return 0;
}
let Some(e_shoff) = read_elf_u64(bytes, 0x28).and_then(usize_from_u64) else {
return 0;
};
let Some(e_shentsize) = read_elf_u16(bytes, 0x3a).map(usize::from) else {
return 0;
};
let Some(e_shnum) = read_elf_u16(bytes, 0x3c).map(usize::from) else {
return 0;
};
if e_shentsize < 64 {
return 0;
}
let mut patched_offsets = BTreeSet::new();
for section_index in 0..e_shnum {
let Some(section) = elf_section_offset(bytes, e_shoff, e_shentsize, e_shnum, section_index)
else {
continue;
};
if read_elf_u32(bytes, section + 4) != Some(11) {
continue;
}
let Some(strtab_index) = read_elf_u32(bytes, section + 40).and_then(usize_from_u32) else {
continue;
};
let Some(strtab) = elf_section_offset(bytes, e_shoff, e_shentsize, e_shnum, strtab_index)
else {
continue;
};
let Some(strtab_offset) = read_elf_u64(bytes, strtab + 24).and_then(usize_from_u64) else {
continue;
};
let Some(strtab_size) = read_elf_u64(bytes, strtab + 32).and_then(usize_from_u64) else {
continue;
};
let Some(sym_offset) = read_elf_u64(bytes, section + 24).and_then(usize_from_u64) else {
continue;
};
let Some(sym_size) = read_elf_u64(bytes, section + 32).and_then(usize_from_u64) else {
continue;
};
let Some(sym_entsize) = read_elf_u64(bytes, section + 56).and_then(usize_from_u64) else {
continue;
};
if sym_entsize < 24 {
continue;
}
let Some(sym_end) = sym_offset.checked_add(sym_size) else {
continue;
};
let mut sym = sym_offset;
while sym
.checked_add(24)
.is_some_and(|end| end <= sym_end && end <= bytes.len())
{
let Some(name_offset) = read_elf_u32(bytes, sym).and_then(usize_from_u32) else {
break;
};
let Some(name) = elf_string(bytes, strtab_offset, strtab_size, name_offset) else {
sym += sym_entsize;
continue;
};
let Some(syscall_number) = vdso_fallback_syscall_number(name) else {
sym += sym_entsize;
continue;
};
let Some(symbol_value) = read_elf_u64(bytes, sym + 8) else {
sym += sym_entsize;
continue;
};
if patched_offsets.insert(symbol_value) {
write_vdso_syscall_stub(bytes, symbol_value, syscall_number);
}
sym += sym_entsize;
}
}
patched_offsets.len()
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn vdso_fallback_syscall_number(name: &[u8]) -> Option<u32> {
match name {
b"__vdso_gettimeofday" | b"gettimeofday" => Some(96),
b"__vdso_time" | b"time" => Some(201),
b"__vdso_clock_gettime" | b"clock_gettime" => Some(228),
b"__vdso_clock_getres" | b"clock_getres" => Some(229),
b"__vdso_getcpu" | b"getcpu" => Some(309),
_ => None,
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn write_vdso_syscall_stub(bytes: &mut [u8], offset: u64, syscall_number: u32) -> bool {
let Some(offset) = usize_from_u64(offset) else {
return false;
};
let Some(end) = offset.checked_add(8) else {
return false;
};
if end > bytes.len() {
return false;
}
bytes[offset] = 0xb8;
bytes[offset + 1..offset + 5].copy_from_slice(&syscall_number.to_le_bytes());
bytes[offset + 5] = 0x0f;
bytes[offset + 6] = 0x05;
bytes[offset + 7] = 0xc3;
true
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn elf_section_offset(
bytes: &[u8],
e_shoff: usize,
e_shentsize: usize,
e_shnum: usize,
index: usize,
) -> Option<usize> {
if index >= e_shnum {
return None;
}
let offset = e_shoff.checked_add(index.checked_mul(e_shentsize)?)?;
offset.checked_add(64).filter(|end| *end <= bytes.len())?;
Some(offset)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn elf_string<'a>(
bytes: &'a [u8],
strtab_offset: usize,
strtab_size: usize,
name_offset: usize,
) -> Option<&'a [u8]> {
if name_offset >= strtab_size {
return None;
}
let start = strtab_offset.checked_add(name_offset)?;
let limit = strtab_offset.checked_add(strtab_size)?;
if start >= bytes.len() || limit > bytes.len() {
return None;
}
let rel_end = bytes[start..limit]
.iter()
.position(|byte| *byte == 0)
.unwrap_or(limit - start);
Some(&bytes[start..start + rel_end])
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn read_elf_u16(bytes: &[u8], offset: usize) -> Option<u16> {
let raw = bytes.get(offset..offset.checked_add(2)?)?;
Some(u16::from_le_bytes(raw.try_into().ok()?))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn read_elf_u32(bytes: &[u8], offset: usize) -> Option<u32> {
let raw = bytes.get(offset..offset.checked_add(4)?)?;
Some(u32::from_le_bytes(raw.try_into().ok()?))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn read_elf_u64(bytes: &[u8], offset: usize) -> Option<u64> {
let raw = bytes.get(offset..offset.checked_add(8)?)?;
Some(u64::from_le_bytes(raw.try_into().ok()?))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn usize_from_u32(value: u32) -> Option<usize> {
usize::try_from(value).ok()
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn usize_from_u64(value: u64) -> Option<usize> {
usize::try_from(value).ok()
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn process_vm_read_exact(
host_pid: i32,
addr: u64,
len: usize,
) -> Result<Vec<u8>, KboxlikeMemoryReadError> {
const CHUNK_LEN: usize = 1024 * 1024;
if host_pid <= 0 {
return Err(KboxlikeMemoryReadError {
bytes_read: 0,
reason: "invalid tracee pid",
});
}
if len == 0 {
return Ok(Vec::new());
}
let mut out = Vec::with_capacity(len);
while out.len() < len {
let offset = out.len();
let chunk_len = (len - offset).min(CHUNK_LEN);
let Some(remote_addr) = addr.checked_add(offset as u64) else {
return Err(KboxlikeMemoryReadError {
bytes_read: out.len(),
reason: "tracee memory address overflows",
});
};
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: remote_addr 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(KboxlikeMemoryReadError {
bytes_read: out.len(),
reason: "process_vm_readv failed",
});
}
if read == 0 {
return Err(KboxlikeMemoryReadError {
bytes_read: out.len(),
reason: "process_vm_readv returned zero",
});
}
let read = read as usize;
out.extend_from_slice(&chunk[..read]);
if read != chunk_len {
return Err(KboxlikeMemoryReadError {
bytes_read: out.len(),
reason: "process_vm_readv partial mapping read",
});
}
}
Ok(out)
}
#[cfg(all(test, target_os = "linux", target_arch = "x86_64"))]
mod tests {
use super::*;
use std::collections::BTreeMap;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
#[derive(Default)]
struct FakeRegs {
regs: BTreeMap<i32, libc::user_regs_struct>,
}
impl TraceeRegisterReader for FakeRegs {
fn thread_regs(&mut self, host_pid: i32) -> Result<libc::user_regs_struct, FdError> {
self.regs
.get(&host_pid)
.copied()
.ok_or(FdError::TraceeInstallFailed("missing fake regs"))
}
}
#[derive(Default)]
struct FakeMaps {
maps: BTreeMap<i32, Vec<KboxlikeMapEntry>>,
}
#[derive(Default)]
struct FakeMemory {
reads: BTreeMap<(i32, u64, usize), Result<Vec<u8>, KboxlikeMemoryReadError>>,
}
impl TraceeMapsReader for FakeMaps {
fn maps_for_pid(&mut self, host_pid: i32) -> Result<Vec<KboxlikeMapEntry>, FdError> {
self.maps
.get(&host_pid)
.cloned()
.ok_or(FdError::TraceeInstallFailed("missing fake maps"))
}
}
fn fake_regs(rip: u64) -> libc::user_regs_struct {
let mut regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
regs.rip = rip;
regs
}
fn put_u16(bytes: &mut [u8], offset: usize, value: u16) {
bytes[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
}
fn put_u32(bytes: &mut [u8], offset: usize, value: u32) {
bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
}
fn put_u64(bytes: &mut [u8], offset: usize, value: u64) {
bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
}
impl TraceeMemoryReader for FakeMemory {
fn read_exact(
&mut self,
host_pid: i32,
addr: u64,
len: usize,
) -> Result<Vec<u8>, KboxlikeMemoryReadError> {
self.reads
.get(&(host_pid, addr, len))
.cloned()
.unwrap_or_else(|| {
Err(KboxlikeMemoryReadError {
bytes_read: 0,
reason: "missing fake memory",
})
})
}
}
#[test]
fn capture_stopped_tracees_records_regs_and_dump_candidate_maps() {
let tracees = vec![
KboxlikeTraceeIdentity {
host_pid: 2001,
model_pid: 7,
model_thread_id: 1,
},
KboxlikeTraceeIdentity {
host_pid: 2002,
model_pid: 7,
model_thread_id: 2,
},
KboxlikeTraceeIdentity {
host_pid: 3001,
model_pid: 9,
model_thread_id: 3,
},
];
let mut regs = FakeRegs::default();
regs.regs.insert(2001, fake_regs(0x1000));
regs.regs.insert(2002, fake_regs(0x2000));
regs.regs.insert(3001, fake_regs(0x3000));
let mut maps = FakeMaps::default();
maps.maps.insert(
2001,
super::super::memory::parse_proc_maps(
"1000-2000 r--p 00000000 00:00 0\n\
3000-4000 ---p 00000000 00:00 0\n\
7000-8000 r--p 00000000 00:00 0 [vvar]\n",
),
);
maps.maps.insert(
2002,
super::super::memory::parse_proc_maps("9000-b000 rw-p 00000000 00:00 0 [heap]\n"),
);
maps.maps.insert(3001, Vec::new());
let snapshot = capture_stopped_tracees_with(tracees, &mut regs, &mut maps).unwrap();
assert_eq!(snapshot.threads.len(), 3);
assert_eq!(snapshot.threads[0].host_pid, 2001);
assert_eq!(snapshot.threads[0].model_pid, 7);
assert_eq!(snapshot.threads[0].model_thread_id, 1);
assert_eq!(snapshot.threads[0].regs.rip, 0x1000);
assert_eq!(snapshot.threads[2].host_pid, 3001);
assert_eq!(snapshot.threads[2].model_pid, 9);
assert_eq!(snapshot.threads[2].model_thread_id, 3);
assert_eq!(snapshot.thread_kernel_states.len(), 3);
assert_eq!(snapshot.thread_kernel_states[0].host_pid, 2001);
assert_eq!(snapshot.thread_kernel_states[0].clear_child_tid, None);
assert_eq!(snapshot.thread_kernel_states[1].robust_list_head, None);
assert_eq!(snapshot.memory_maps[0].dump_candidate_bytes, 0x1000);
assert_eq!(snapshot.memory_maps[1].dump_candidate_bytes, 0x2000);
assert_eq!(snapshot.memory_maps[2].dump_candidate_bytes, 0);
assert_eq!(snapshot.fd_tables.len(), 3);
assert!(snapshot.fd_tables.iter().all(|table| table.fds.is_empty()));
}
#[test]
fn fd_target_classifier_recognizes_chromium_kernel_objects() {
assert_eq!(classify_fd_target("pipe:[123]"), KboxlikeFdTargetKind::Pipe);
assert_eq!(
classify_fd_target("socket:[456]"),
KboxlikeFdTargetKind::Socket
);
assert_eq!(
classify_fd_target("anon_inode:[eventfd]"),
KboxlikeFdTargetKind::EventFd
);
assert_eq!(
classify_fd_target("anon_inode:[eventpoll]"),
KboxlikeFdTargetKind::EventPoll
);
assert_eq!(
classify_fd_target("anon_inode:[timerfd]"),
KboxlikeFdTargetKind::TimerFd
);
assert_eq!(
classify_fd_target("/memfd:kboxlike (deleted)"),
KboxlikeFdTargetKind::Memfd
);
assert_eq!(
classify_fd_target("/dev/shm/chrome"),
KboxlikeFdTargetKind::Device
);
assert_eq!(
classify_fd_target("/usr/bin/chromium"),
KboxlikeFdTargetKind::RegularPath
);
}
fn fake_fd(fd: i32, target: &str, flags: u64) -> KboxlikeFdSnapshot {
KboxlikeFdSnapshot {
host_pid: 2001,
model_pid: 7,
model_thread_id: 1,
fd,
target: target.to_string(),
kind: classify_fd_target(target),
object_identity: None,
flags: Some(flags),
cloexec: flags & libc::O_CLOEXEC as u64 != 0,
eventfd_count: None,
eventfd_semaphore: None,
timerfd: None,
epoll_interests: Vec::new(),
}
}
#[test]
fn fdinfo_parser_reads_epoll_interest_records() {
let fdinfo = "\
pos:\t0
flags:\t02000002
mnt_id:\t16
ino:\t1066
tfd: 5 events: 19 data: 7fff00000005 pos:0 ino:42a sdev:f
tfd: 3 events: 19 data: 7fff00000003 pos:0 ino:308bbad sdev:e
";
assert_eq!(parse_proc_fdinfo_flags(fdinfo), Some(0o2000002));
assert_eq!(
parse_proc_fdinfo_object_identity(fdinfo),
Some(KboxlikeFdObjectIdentity {
mnt_id: Some(16),
ino: Some(1066),
eventfd_id: None,
})
);
assert_eq!(
parse_proc_fdinfo_epoll_interests(fdinfo),
vec![
KboxlikeEpollInterestSnapshot {
target_fd: 5,
events: 0x19,
data: 0x7fff00000005,
},
KboxlikeEpollInterestSnapshot {
target_fd: 3,
events: 0x19,
data: 0x7fff00000003,
},
]
);
}
#[test]
fn eventfd_shadow_planner_uses_fdinfo_counter_and_flags() {
let mut eventfd = fake_fd(
8,
"anon_inode:[eventfd]",
(libc::O_CLOEXEC | libc::O_NONBLOCK) as u64,
);
eventfd.eventfd_count = Some(7);
eventfd.eventfd_semaphore = Some(true);
let fd_tables = vec![KboxlikeTraceeFdSnapshot {
host_pid: 2001,
model_pid: 7,
model_thread_id: 1,
fds: vec![eventfd],
}];
assert_eq!(
plan_eventfd_shadow_backings(&fd_tables),
vec![KboxlikeFdShadowBackingPlan {
host_pid: 2001,
model_pid: 7,
model_thread_id: 1,
fd: 8,
target: "anon_inode:[eventfd]".to_string(),
cloexec: true,
backing: ShadowBacking::HostPassthrough {
label: format!(
"eventfd:init=7,flags={}",
libc::EFD_NONBLOCK | libc::EFD_SEMAPHORE
),
},
}]
);
}
#[test]
fn fdinfo_parser_reads_timerfd_state() {
let fdinfo = "\
pos:\t0
flags:\t02004002
mnt_id:\t16
ino:\t1066
clockid: 1
ticks: 2
settime flags: 00
it_value: (60, 987620682)
it_interval: (2, 123456789)
";
assert_eq!(parse_proc_fdinfo_flags(fdinfo), Some(0o2004002));
assert_eq!(
parse_proc_fdinfo_object_identity(fdinfo),
Some(KboxlikeFdObjectIdentity {
mnt_id: Some(16),
ino: Some(1066),
eventfd_id: None,
})
);
assert_eq!(
parse_proc_fdinfo_timerfd(fdinfo),
Some(KboxlikeTimerFdSnapshot {
clockid: 1,
ticks: 2,
settime_flags: 0,
value_sec: 60,
value_nsec: 987_620_682,
interval_sec: 2,
interval_nsec: 123_456_789,
})
);
}
#[test]
fn timerfd_shadow_planner_uses_fdinfo_timer_state_and_flags() {
let mut timerfd = fake_fd(
10,
"anon_inode:[timerfd]",
(libc::O_CLOEXEC | libc::O_NONBLOCK) as u64,
);
timerfd.timerfd = Some(KboxlikeTimerFdSnapshot {
clockid: libc::CLOCK_MONOTONIC,
ticks: 0,
settime_flags: 0,
value_sec: 60,
value_nsec: 987_620_682,
interval_sec: 2,
interval_nsec: 123_456_789,
});
let fd_tables = vec![KboxlikeTraceeFdSnapshot {
host_pid: 2001,
model_pid: 7,
model_thread_id: 1,
fds: vec![timerfd],
}];
assert_eq!(
plan_timerfd_shadow_backings(&fd_tables),
vec![KboxlikeFdShadowBackingPlan {
host_pid: 2001,
model_pid: 7,
model_thread_id: 1,
fd: 10,
target: "anon_inode:[timerfd]".to_string(),
cloexec: true,
backing: ShadowBacking::HostPassthrough {
label: format!(
"timerfd:clockid={},flags={},value_sec=60,value_nsec=987620682,interval_sec=2,interval_nsec=123456789",
libc::CLOCK_MONOTONIC,
libc::TFD_NONBLOCK
),
},
}]
);
}
#[test]
fn epoll_shadow_planner_links_interests_to_watched_fd_backings() {
let mut fd_system = KboxlikeFdSystem::new();
let model_pid = fd_system.spawn_init();
for fd in [3, 4, 8, 9] {
fd_system
.insert_plain_fd(model_pid, fd, Some(fd as i64), false)
.unwrap();
}
let mut pipe_read = fake_fd(3, "pipe:[12345]", libc::O_CLOEXEC as u64);
let mut pipe_write = fake_fd(4, "pipe:[12345]", (libc::O_CLOEXEC | libc::O_WRONLY) as u64);
let mut eventfd = fake_fd(
8,
"anon_inode:[eventfd]",
(libc::O_CLOEXEC | libc::O_NONBLOCK) as u64,
);
let mut epoll = fake_fd(9, "anon_inode:[eventpoll]", libc::O_CLOEXEC as u64);
for fd in [&mut pipe_read, &mut pipe_write, &mut eventfd, &mut epoll] {
fd.model_pid = model_pid;
}
eventfd.eventfd_count = Some(7);
eventfd.eventfd_semaphore = Some(false);
epoll.epoll_interests = vec![
KboxlikeEpollInterestSnapshot {
target_fd: 3,
events: 0x19,
data: 0x7fff00000003,
},
KboxlikeEpollInterestSnapshot {
target_fd: 8,
events: 0x19,
data: 0x7fff00000008,
},
];
let fd_tables = vec![KboxlikeTraceeFdSnapshot {
host_pid: 2001,
model_pid,
model_thread_id: 1,
fds: vec![pipe_read, pipe_write, eventfd, epoll],
}];
let mut watched_fd_plans = plan_pipe_shadow_backings(&fd_tables);
watched_fd_plans.extend(plan_eventfd_shadow_backings(&fd_tables));
apply_fd_shadow_backing_plans(&mut fd_system, &watched_fd_plans).unwrap();
let epoll_plans = plan_epoll_shadow_backings(&fd_tables, &fd_system);
assert_eq!(epoll_plans.len(), 1);
assert_eq!(
epoll_plans[0].fd_plan.backing,
ShadowBacking::HostPassthrough {
label: "epoll:epoll2001_9".to_string(),
}
);
assert_eq!(epoll_plans[0].interests.len(), 2);
assert_eq!(
epoll_plans[0].interests[0].target_backing,
Some(ShadowBacking::HostPassthrough {
label: "pipe:pipe12345:0".to_string(),
})
);
assert_eq!(
epoll_plans[0].interests[1].target_backing,
Some(ShadowBacking::HostPassthrough {
label: format!("eventfd:init=7,flags={}", libc::EFD_NONBLOCK),
})
);
apply_fd_shadow_backing_plans(
&mut fd_system,
&epoll_plans
.iter()
.map(|plan| plan.fd_plan.clone())
.collect::<Vec<_>>(),
)
.unwrap();
assert!(matches!(
fd_system.route_fd_operation(model_pid, 9, super::super::fd::FdOperation::Generic),
Ok(super::super::fd::FdRoute::HostPassthrough { shadow })
if shadow.kind == ShadowKind::HostPassthrough
));
}
#[test]
fn pipe_shadow_planner_pairs_unique_read_and_write_ends() {
let fd_tables = vec![KboxlikeTraceeFdSnapshot {
host_pid: 2001,
model_pid: 7,
model_thread_id: 1,
fds: vec![
fake_fd(3, "pipe:[12345]", libc::O_CLOEXEC as u64),
fake_fd(4, "pipe:[12345]", (libc::O_CLOEXEC | libc::O_WRONLY) as u64),
fake_fd(5, "socket:[999]", libc::O_CLOEXEC as u64),
],
}];
let plans = plan_pipe_shadow_backings(&fd_tables);
assert_eq!(
plans,
vec![
KboxlikeFdShadowBackingPlan {
host_pid: 2001,
model_pid: 7,
model_thread_id: 1,
fd: 3,
target: "pipe:[12345]".to_string(),
cloexec: true,
backing: ShadowBacking::HostPassthrough {
label: "pipe:pipe12345:0".to_string(),
},
},
KboxlikeFdShadowBackingPlan {
host_pid: 2001,
model_pid: 7,
model_thread_id: 1,
fd: 4,
target: "pipe:[12345]".to_string(),
cloexec: true,
backing: ShadowBacking::HostPassthrough {
label: "pipe:pipe12345:1".to_string(),
},
},
]
);
}
#[test]
fn pipe_shadow_planner_keeps_duplicate_endpoint_identity() {
let fd_tables = vec![KboxlikeTraceeFdSnapshot {
host_pid: 2001,
model_pid: 7,
model_thread_id: 1,
fds: vec![
fake_fd(3, "pipe:[12345]", libc::O_CLOEXEC as u64),
fake_fd(4, "pipe:[12345]", libc::O_CLOEXEC as u64),
fake_fd(5, "pipe:[12345]", (libc::O_CLOEXEC | libc::O_WRONLY) as u64),
],
}];
let plans = plan_pipe_shadow_backings(&fd_tables);
assert_eq!(plans.len(), 3);
assert_eq!(
plans
.iter()
.map(|plan| (&plan.fd, &plan.backing))
.collect::<Vec<_>>(),
vec![
(
&3,
&ShadowBacking::HostPassthrough {
label: "pipe:pipe12345:0".to_string()
}
),
(
&4,
&ShadowBacking::HostPassthrough {
label: "pipe:pipe12345:0".to_string()
}
),
(
&5,
&ShadowBacking::HostPassthrough {
label: "pipe:pipe12345:1".to_string()
}
),
]
);
}
#[test]
fn socketpair_shadow_planner_pairs_two_socket_endpoints() {
let fd_tables = vec![KboxlikeTraceeFdSnapshot {
host_pid: 2001,
model_pid: 7,
model_thread_id: 1,
fds: vec![
fake_fd(6, "socket:[111]", libc::O_CLOEXEC as u64),
fake_fd(
7,
"socket:[222]",
(libc::O_CLOEXEC | libc::O_NONBLOCK) as u64,
),
fake_fd(8, "anon_inode:[eventfd]", libc::O_CLOEXEC as u64),
],
}];
let plans = plan_socketpair_shadow_backings(&fd_tables);
assert_eq!(
plans,
vec![
KboxlikeFdShadowBackingPlan {
host_pid: 2001,
model_pid: 7,
model_thread_id: 1,
fd: 6,
target: "socket:[111]".to_string(),
cloexec: true,
backing: ShadowBacking::HostPassthrough {
label: "socketpair:socketpair111_222:0".to_string(),
},
},
KboxlikeFdShadowBackingPlan {
host_pid: 2001,
model_pid: 7,
model_thread_id: 1,
fd: 7,
target: "socket:[222]".to_string(),
cloexec: true,
backing: ShadowBacking::HostPassthrough {
label: "socketpair:socketpair111_222:1".to_string(),
},
},
]
);
}
#[test]
fn socketpair_shadow_planner_preserves_duplicate_endpoint_identity() {
let fd_tables = vec![KboxlikeTraceeFdSnapshot {
host_pid: 2001,
model_pid: 7,
model_thread_id: 1,
fds: vec![
fake_fd(6, "socket:[111]", libc::O_CLOEXEC as u64),
fake_fd(8, "socket:[111]", libc::O_CLOEXEC as u64),
fake_fd(7, "socket:[222]", libc::O_CLOEXEC as u64),
],
}];
let plans = plan_socketpair_shadow_backings(&fd_tables);
assert_eq!(plans.len(), 3);
assert_eq!(
plans
.iter()
.map(|plan| (&plan.fd, &plan.backing))
.collect::<Vec<_>>(),
vec![
(
&6,
&ShadowBacking::HostPassthrough {
label: "socketpair:socketpair111_222:0".to_string()
}
),
(
&7,
&ShadowBacking::HostPassthrough {
label: "socketpair:socketpair111_222:1".to_string()
}
),
(
&8,
&ShadowBacking::HostPassthrough {
label: "socketpair:socketpair111_222:0".to_string()
}
),
]
);
}
#[test]
fn socketpair_shadow_planner_skips_ambiguous_socket_sets() {
let fd_tables = vec![KboxlikeTraceeFdSnapshot {
host_pid: 2001,
model_pid: 7,
model_thread_id: 1,
fds: vec![
fake_fd(6, "socket:[111]", libc::O_CLOEXEC as u64),
fake_fd(7, "socket:[222]", libc::O_CLOEXEC as u64),
fake_fd(8, "socket:[333]", libc::O_CLOEXEC as u64),
],
}];
assert!(plan_socketpair_shadow_backings(&fd_tables).is_empty());
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn scm_rights_mapping_accepts_duplicate_fd_aliases_with_same_backing() {
let raw_received = unsafe { libc::eventfd(7, libc::EFD_CLOEXEC) };
assert!(raw_received >= 0);
let received = unsafe { OwnedFd::from_raw_fd(raw_received) };
let target = fs::read_link(format!("/proc/self/fd/{}", received.as_raw_fd()))
.unwrap()
.to_string_lossy()
.into_owned();
let object_identity = read_proc_fdinfo(unsafe { libc::getpid() }, received.as_raw_fd())
.and_then(|fdinfo| parse_proc_fdinfo_object_identity(&fdinfo));
let mut fd_system = KboxlikeFdSystem::new();
let model_pid = fd_system.spawn_init();
let backing = ShadowBacking::HostPassthrough {
label: "eventfd:init=7,flags=0".to_string(),
};
fd_system
.insert_shadow_fd_with_backing(
model_pid,
8,
None,
ShadowObject {
kind: ShadowKind::HostPassthrough,
supervisor_fd: 8,
},
backing.clone(),
false,
)
.unwrap();
fd_system.dup_to(model_pid, 8, 9, false).unwrap();
let mut fd8 = fake_fd(8, &target, libc::O_CLOEXEC as u64);
fd8.model_pid = model_pid;
fd8.object_identity = object_identity;
let mut fd9 = fake_fd(9, &target, libc::O_CLOEXEC as u64);
fd9.model_pid = model_pid;
fd9.object_identity = object_identity;
let table = KboxlikeTraceeFdSnapshot {
host_pid: 2001,
model_pid,
model_thread_id: 1,
fds: vec![fd8, fd9],
};
let mapped =
map_received_fds_to_backings(&table, &fd_system, model_pid, vec![received]).unwrap();
assert_eq!(mapped, (vec![8], vec![backing]));
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn scm_rights_mapping_uses_fdinfo_identity_for_same_target_different_backings() {
let raw_received = unsafe { libc::eventfd(7, libc::EFD_CLOEXEC) };
assert!(raw_received >= 0);
let received = unsafe { OwnedFd::from_raw_fd(raw_received) };
let target = fs::read_link(format!("/proc/self/fd/{}", received.as_raw_fd()))
.unwrap()
.to_string_lossy()
.into_owned();
let object_identity = read_proc_fdinfo(unsafe { libc::getpid() }, received.as_raw_fd())
.and_then(|fdinfo| parse_proc_fdinfo_object_identity(&fdinfo))
.unwrap();
let mut fd_system = KboxlikeFdSystem::new();
let model_pid = fd_system.spawn_init();
let expected_backing = ShadowBacking::HostPassthrough {
label: "eventfd:init=7,flags=0".to_string(),
};
for (fd, label) in [
(8, "eventfd:init=7,flags=0"),
(9, "eventfd:init=99,flags=0"),
] {
fd_system
.insert_shadow_fd_with_backing(
model_pid,
fd,
None,
ShadowObject {
kind: ShadowKind::HostPassthrough,
supervisor_fd: fd,
},
ShadowBacking::HostPassthrough {
label: label.to_string(),
},
false,
)
.unwrap();
}
let mut fd8 = fake_fd(8, &target, libc::O_CLOEXEC as u64);
fd8.model_pid = model_pid;
fd8.object_identity = Some(object_identity);
let mut fd9 = fake_fd(9, &target, libc::O_CLOEXEC as u64);
fd9.model_pid = model_pid;
fd9.object_identity = Some(KboxlikeFdObjectIdentity {
eventfd_id: object_identity.eventfd_id.map(|id| id + 1),
..object_identity
});
let table = KboxlikeTraceeFdSnapshot {
host_pid: 2001,
model_pid,
model_thread_id: 1,
fds: vec![fd8, fd9],
};
let mapped =
map_received_fds_to_backings(&table, &fd_system, model_pid, vec![received]).unwrap();
assert_eq!(mapped, (vec![8], vec![expected_backing]));
}
#[test]
fn applies_fd_shadow_backing_plans_to_fd_model() {
let mut fd_system = KboxlikeFdSystem::new();
let model_pid = fd_system.spawn_init();
fd_system
.insert_plain_fd(model_pid, 3, Some(3), false)
.unwrap();
fd_system
.insert_plain_fd(model_pid, 4, Some(4), false)
.unwrap();
let plans = vec![
KboxlikeFdShadowBackingPlan {
host_pid: 2001,
model_pid,
model_thread_id: 1,
fd: 3,
target: "pipe:[12345]".to_string(),
cloexec: false,
backing: ShadowBacking::HostPassthrough {
label: "pipe:pipe12345:0".to_string(),
},
},
KboxlikeFdShadowBackingPlan {
host_pid: 2001,
model_pid,
model_thread_id: 1,
fd: 4,
target: "pipe:[12345]".to_string(),
cloexec: true,
backing: ShadowBacking::HostPassthrough {
label: "pipe:pipe12345:1".to_string(),
},
},
];
assert_eq!(
apply_fd_shadow_backing_plans(&mut fd_system, &plans).unwrap(),
2
);
assert_eq!(
fd_system.shadow_backing(model_pid, 3).unwrap(),
Some(&ShadowBacking::HostPassthrough {
label: "pipe:pipe12345:0".to_string(),
})
);
assert_eq!(
fd_system.shadow_backing(model_pid, 4).unwrap(),
Some(&ShadowBacking::HostPassthrough {
label: "pipe:pipe12345:1".to_string(),
})
);
assert!(!fd_system.descriptor(model_pid, 3).unwrap().cloexec);
assert!(fd_system.descriptor(model_pid, 4).unwrap().cloexec);
assert!(matches!(
fd_system.route_fd_operation(model_pid, 3, super::super::fd::FdOperation::Read),
Ok(super::super::fd::FdRoute::HostPassthrough { shadow })
if shadow.kind == ShadowKind::HostPassthrough
));
}
#[test]
fn applies_duplicate_pipe_endpoint_plans_as_shared_open_file() {
let mut fd_system = KboxlikeFdSystem::new();
let browser = fd_system.spawn_init();
let helper = fd_system.fork(browser).unwrap();
for (pid, fd) in [(browser, 3), (helper, 4), (browser, 5)] {
fd_system
.insert_plain_fd(pid, fd, Some(fd as i64), false)
.unwrap();
}
let plans = vec![
KboxlikeFdShadowBackingPlan {
host_pid: 2001,
model_pid: browser,
model_thread_id: 1,
fd: 3,
target: "pipe:[12345]".to_string(),
cloexec: false,
backing: ShadowBacking::HostPassthrough {
label: "pipe:pipe12345:0".to_string(),
},
},
KboxlikeFdShadowBackingPlan {
host_pid: 2002,
model_pid: helper,
model_thread_id: 2,
fd: 4,
target: "pipe:[12345]".to_string(),
cloexec: true,
backing: ShadowBacking::HostPassthrough {
label: "pipe:pipe12345:0".to_string(),
},
},
KboxlikeFdShadowBackingPlan {
host_pid: 2001,
model_pid: browser,
model_thread_id: 1,
fd: 5,
target: "pipe:[12345]".to_string(),
cloexec: false,
backing: ShadowBacking::HostPassthrough {
label: "pipe:pipe12345:1".to_string(),
},
},
];
assert_eq!(
apply_fd_shadow_backing_plans(&mut fd_system, &plans).unwrap(),
3
);
let first_read = fd_system.descriptor(browser, 3).unwrap();
let duplicate_read = fd_system.descriptor(helper, 4).unwrap();
let write_end = fd_system.descriptor(browser, 5).unwrap();
assert_eq!(first_read.open_file, duplicate_read.open_file);
assert_ne!(first_read.open_file, write_end.open_file);
assert_eq!(first_read.ref_count, 2);
assert!(duplicate_read.cloexec);
assert_eq!(
fd_system.shadow_backing(helper, 4).unwrap(),
Some(&ShadowBacking::HostPassthrough {
label: "pipe:pipe12345:0".to_string(),
})
);
}
#[test]
fn capture_stopped_tracees_records_tracked_thread_kernel_state() {
let tracees = vec![
KboxlikeTraceeIdentity {
host_pid: 2001,
model_pid: 7,
model_thread_id: 1,
},
KboxlikeTraceeIdentity {
host_pid: 2002,
model_pid: 7,
model_thread_id: 2,
},
];
let kernel_states = vec![KboxlikeThreadKernelStateSnapshot {
host_pid: 2002,
model_pid: 7,
model_thread_id: 2,
clear_child_tid: Some(0x7000),
robust_list_head: Some(0x8000),
robust_list_len: 24,
}];
let mut regs = FakeRegs::default();
regs.regs.insert(2001, fake_regs(0x1000));
regs.regs.insert(2002, fake_regs(0x2000));
let mut maps = FakeMaps::default();
maps.maps.insert(2001, Vec::new());
maps.maps.insert(2002, Vec::new());
let snapshot = capture_stopped_tracees_with_kernel_states(
tracees,
kernel_states,
&mut regs,
&mut maps,
)
.unwrap();
assert_eq!(snapshot.thread_kernel_states.len(), 2);
assert_eq!(snapshot.thread_kernel_states[0].host_pid, 2001);
assert_eq!(snapshot.thread_kernel_states[0].clear_child_tid, None);
assert_eq!(snapshot.thread_kernel_states[1].host_pid, 2002);
assert_eq!(
snapshot.thread_kernel_states[1].clear_child_tid,
Some(0x7000)
);
assert_eq!(
snapshot.thread_kernel_states[1].robust_list_head,
Some(0x8000)
);
assert_eq!(snapshot.thread_kernel_states[1].robust_list_len, 24);
}
#[test]
fn capture_dump_candidate_memory_reads_only_candidate_mappings() {
let memory_maps = vec![KboxlikeTraceeMapSnapshot {
host_pid: 2001,
model_pid: 7,
model_thread_id: 7,
maps: super::super::memory::parse_proc_maps(
"1000-1004 r--p 00000000 00:00 0\n\
2000-2004 ---p 00000000 00:00 0\n\
3000-3004 r--p 00000000 00:00 0 [vvar]\n\
4000-4002 rw-p 00000000 00:00 0 [heap]\n\
5000-5003 r-xp 00000000 00:00 0 [vdso]\n",
),
dump_candidate_bytes: 9,
}];
let mut memory = FakeMemory::default();
memory.reads.insert((2001, 0x1000, 4), Ok(b"abcd".to_vec()));
memory.reads.insert((2001, 0x4000, 2), Ok(b"ef".to_vec()));
memory.reads.insert((2001, 0x5000, 3), Ok(b"ghi".to_vec()));
let capture = capture_dump_candidate_memory_with(&memory_maps, &mut memory);
assert!(capture.failures.is_empty());
assert_eq!(
capture.dumps,
vec![
KboxlikeMemoryDump {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
bytes: b"abcd".to_vec(),
},
KboxlikeMemoryDump {
host_pid: 2001,
model_pid: 7,
start: 0x4000,
bytes: b"ef".to_vec(),
},
KboxlikeMemoryDump {
host_pid: 2001,
model_pid: 7,
start: 0x5000,
bytes: b"ghi".to_vec(),
},
]
);
assert_eq!(capture.captured_bytes(), 9);
assert_eq!(capture.failed_bytes(), 0);
}
#[test]
fn vdso_patcher_replaces_exported_entry_with_syscall_stub() {
let mut bytes = vec![0x90; 0x220];
bytes[0..4].copy_from_slice(b"\x7fELF");
bytes[4] = 2;
bytes[5] = 1;
put_u64(&mut bytes, 0x28, 0x40);
put_u16(&mut bytes, 0x3a, 64);
put_u16(&mut bytes, 0x3c, 3);
let dynsym_section = 0x40 + 64;
put_u32(&mut bytes, dynsym_section + 4, 11);
put_u64(&mut bytes, dynsym_section + 24, 0x100);
put_u64(&mut bytes, dynsym_section + 32, 24);
put_u32(&mut bytes, dynsym_section + 40, 2);
put_u64(&mut bytes, dynsym_section + 56, 24);
let strtab_section = 0x40 + 128;
let names = b"\0__vdso_gettimeofday\0";
put_u32(&mut bytes, strtab_section + 4, 3);
put_u64(&mut bytes, strtab_section + 24, 0x180);
put_u64(&mut bytes, strtab_section + 32, names.len() as u64);
bytes[0x180..0x180 + names.len()].copy_from_slice(names);
put_u32(&mut bytes, 0x100, 1);
put_u64(&mut bytes, 0x108, 0x200);
let patched = patch_vdso_syscall_fallbacks(&mut bytes);
assert_eq!(patched, 1);
assert_eq!(&bytes[0x200..0x208], &[0xb8, 96, 0, 0, 0, 0x0f, 0x05, 0xc3]);
}
#[test]
fn capture_dump_candidate_memory_records_read_failures_without_stopping() {
let memory_maps = vec![KboxlikeTraceeMapSnapshot {
host_pid: 2001,
model_pid: 7,
model_thread_id: 7,
maps: super::super::memory::parse_proc_maps(
"1000-1004 r--p 00000000 00:00 0\n\
2000-2004 rw-p 00000000 00:00 0 [heap]\n",
),
dump_candidate_bytes: 8,
}];
let mut memory = FakeMemory::default();
memory.reads.insert((2001, 0x1000, 4), Ok(b"abcd".to_vec()));
memory.reads.insert(
(2001, 0x2000, 4),
Err(KboxlikeMemoryReadError {
bytes_read: 2,
reason: "process_vm_readv partial mapping read",
}),
);
let capture = capture_dump_candidate_memory_with(&memory_maps, &mut memory);
assert_eq!(capture.dumps.len(), 1);
assert_eq!(capture.captured_bytes(), 4);
assert_eq!(capture.failed_bytes(), 4);
assert_eq!(
capture.failures,
vec![KboxlikeMemoryDumpFailure {
host_pid: 2001,
model_pid: 7,
start: 0x2000,
len: 4,
bytes_read: 2,
reason: "process_vm_readv partial mapping read",
}]
);
}
#[test]
fn capture_shared_object_memory_reads_one_copy_per_backing_extent() {
let plan = KboxlikeMemoryRestorePlan {
actions: vec![
KboxlikeMemoryRestoreAction::RecreateSharedObject {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
len: 4,
prot: libc::PROT_READ | libc::PROT_WRITE,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:shared".to_owned()),
file_offset: 0,
dev: "00:01".to_owned(),
inode: 99,
},
KboxlikeMemoryRestoreAction::RecreateSharedObject {
host_pid: 2002,
model_pid: 8,
start: 0x5000,
len: 4,
prot: libc::PROT_READ | libc::PROT_WRITE,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:shared".to_owned()),
file_offset: 0,
dev: "00:01".to_owned(),
inode: 99,
},
KboxlikeMemoryRestoreAction::RecreateSharedObject {
host_pid: 2001,
model_pid: 7,
start: 0x2000,
len: 2,
prot: libc::PROT_READ,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:shared".to_owned()),
file_offset: 4,
dev: "00:01".to_owned(),
inode: 99,
},
],
};
let mut memory = FakeMemory::default();
memory.reads.insert((2001, 0x1000, 4), Ok(b"abcd".to_vec()));
memory.reads.insert((2001, 0x2000, 2), Ok(b"ef".to_vec()));
let capture = capture_shared_object_memory_with(&plan, &mut memory);
assert!(capture.failures.is_empty());
assert_eq!(capture.skipped_duplicate_extents, 1);
assert_eq!(capture.captured_bytes(), 6);
assert_eq!(
capture.dumps,
vec![
KboxlikeSharedMemoryDump {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:shared".to_owned()),
file_offset: 0,
dev: "00:01".to_owned(),
inode: 99,
bytes: b"abcd".to_vec(),
},
KboxlikeSharedMemoryDump {
host_pid: 2001,
model_pid: 7,
start: 0x2000,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:shared".to_owned()),
file_offset: 4,
dev: "00:01".to_owned(),
inode: 99,
bytes: b"ef".to_vec(),
},
]
);
}
#[test]
fn capture_shared_object_memory_records_read_failures() {
let plan = KboxlikeMemoryRestorePlan {
actions: vec![KboxlikeMemoryRestoreAction::RecreateSharedObject {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
len: 4,
prot: libc::PROT_READ,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:shared".to_owned()),
file_offset: 0,
dev: "00:01".to_owned(),
inode: 99,
}],
};
let mut memory = FakeMemory::default();
memory.reads.insert(
(2001, 0x1000, 4),
Err(KboxlikeMemoryReadError {
bytes_read: 2,
reason: "process_vm_readv partial mapping read",
}),
);
let capture = capture_shared_object_memory_with(&plan, &mut memory);
assert!(capture.dumps.is_empty());
assert_eq!(capture.failed_bytes(), 4);
assert_eq!(
capture.failures,
vec![KboxlikeSharedMemoryDumpFailure {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
len: 4,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:shared".to_owned()),
file_offset: 0,
dev: "00:01".to_owned(),
inode: 99,
bytes_read: 2,
reason: "process_vm_readv partial mapping read",
}]
);
}
#[test]
fn restore_classifier_splits_clean_files_cow_bytes_and_skips() {
let memory_maps = vec![KboxlikeTraceeMapSnapshot {
host_pid: 2001,
model_pid: 7,
model_thread_id: 7,
maps: super::super::memory::parse_proc_maps(
"1000-2000 r-xp 00000000 08:02 11 /usr/bin/chrome\n\
2000-3000 rw-p 00001000 08:02 11 /usr/bin/chrome\n\
3000-4000 rw-p 00000000 00:00 0 [heap]\n\
4000-5000 ---p 00000000 00:00 0\n\
5000-6000 r--p 00000000 00:00 0 [vvar]\n\
6000-7000 r--p 00000000 00:00 0 [vvar_vclock]\n\
7000-8000 r-xp 00000000 00:00 0 [vdso]\n\
8000-9000 --xp 00000000 00:00 0 [vsyscall]\n\
9000-a000 r--p 00002000 08:02 11 /usr/bin/chrome\n",
),
dump_candidate_bytes: 0x4000,
}];
let plan = classify_memory_restore_actions(&memory_maps);
assert_eq!(
plan.actions,
vec![
KboxlikeMemoryRestoreAction::RemapCleanFile {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
len: 0x1000,
prot: libc::PROT_READ | libc::PROT_EXEC,
path: PathBuf::from("/usr/bin/chrome"),
file_offset: 0,
dev: "08:02".to_owned(),
inode: 11,
},
KboxlikeMemoryRestoreAction::DumpPrivateFileCow {
host_pid: 2001,
model_pid: 7,
start: 0x2000,
len: 0x1000,
prot: libc::PROT_READ | libc::PROT_WRITE,
path: PathBuf::from("/usr/bin/chrome"),
file_offset: 0x1000,
dev: "08:02".to_owned(),
inode: 11,
reason: "writable private file mapping may contain COW bytes",
},
KboxlikeMemoryRestoreAction::DumpAnonymousPrivate {
host_pid: 2001,
model_pid: 7,
start: 0x3000,
len: 0x1000,
prot: libc::PROT_READ | libc::PROT_WRITE,
label: Some("[heap]".to_owned()),
},
KboxlikeMemoryRestoreAction::SkipInaccessible {
host_pid: 2001,
model_pid: 7,
start: 0x4000,
len: 0x1000,
prot: libc::PROT_NONE,
},
KboxlikeMemoryRestoreAction::SkipKernelSynthetic {
host_pid: 2001,
model_pid: 7,
start: 0x5000,
len: 0x1000,
label: "[vvar]".to_owned(),
},
KboxlikeMemoryRestoreAction::SkipKernelSynthetic {
host_pid: 2001,
model_pid: 7,
start: 0x6000,
len: 0x1000,
label: "[vvar_vclock]".to_owned(),
},
KboxlikeMemoryRestoreAction::DumpAnonymousPrivate {
host_pid: 2001,
model_pid: 7,
start: 0x7000,
len: 0x1000,
prot: libc::PROT_READ | libc::PROT_EXEC,
label: Some("[vdso]".to_owned()),
},
KboxlikeMemoryRestoreAction::SkipKernelSynthetic {
host_pid: 2001,
model_pid: 7,
start: 0x8000,
len: 0x1000,
label: "[vsyscall]".to_owned(),
},
KboxlikeMemoryRestoreAction::DumpPrivateFileCow {
host_pid: 2001,
model_pid: 7,
start: 0x9000,
len: 0x1000,
prot: libc::PROT_READ,
path: PathBuf::from("/usr/bin/chrome"),
file_offset: 0x2000,
dev: "08:02".to_owned(),
inode: 11,
reason: "non-executable private file mapping may contain relocated COW bytes",
},
]
);
}
#[test]
fn restore_classifier_keeps_shared_objects_out_of_plain_byte_dump() {
let memory_maps = vec![KboxlikeTraceeMapSnapshot {
host_pid: 2001,
model_pid: 7,
model_thread_id: 7,
maps: super::super::memory::parse_proc_maps(
"1000-2000 rw-s 00000000 00:01 99 /memfd:chromium-shared (deleted)\n\
2000-3000 r--s 00000000 00:05 10 /dev/shm/chrome\n\
3000-4000 rw-s 00000000 00:00 0\n",
),
dump_candidate_bytes: 0x3000,
}];
let plan = classify_memory_restore_actions(&memory_maps);
assert_eq!(
plan.actions,
vec![
KboxlikeMemoryRestoreAction::RecreateSharedObject {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
len: 0x1000,
prot: libc::PROT_READ | libc::PROT_WRITE,
kind: KboxlikeSharedMappingKind::Memfd,
path: Some("/memfd:chromium-shared (deleted)".to_owned()),
file_offset: 0,
dev: "00:01".to_owned(),
inode: 99,
},
KboxlikeMemoryRestoreAction::RecreateSharedObject {
host_pid: 2001,
model_pid: 7,
start: 0x2000,
len: 0x1000,
prot: libc::PROT_READ,
kind: KboxlikeSharedMappingKind::Device,
path: Some("/dev/shm/chrome".to_owned()),
file_offset: 0,
dev: "00:05".to_owned(),
inode: 10,
},
KboxlikeMemoryRestoreAction::RecreateSharedObject {
host_pid: 2001,
model_pid: 7,
start: 0x3000,
len: 0x1000,
prot: libc::PROT_READ | libc::PROT_WRITE,
kind: KboxlikeSharedMappingKind::Anonymous,
path: None,
file_offset: 0,
dev: "00:00".to_owned(),
inode: 0,
},
]
);
}
#[test]
fn restore_classifier_dumps_deleted_private_file_mappings() {
let memory_maps = vec![KboxlikeTraceeMapSnapshot {
host_pid: 2001,
model_pid: 7,
model_thread_id: 7,
maps: super::super::memory::parse_proc_maps(
"1000-2000 r--p 00002000 00:01 99 /tmp/chrome-cache (deleted)\n",
),
dump_candidate_bytes: 0x1000,
}];
let plan = classify_memory_restore_actions(&memory_maps);
assert_eq!(
plan.actions,
vec![KboxlikeMemoryRestoreAction::DumpPrivateFileCow {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
len: 0x1000,
prot: libc::PROT_READ,
path: PathBuf::from("/tmp/chrome-cache"),
file_offset: 0x2000,
dev: "00:01".to_owned(),
inode: 99,
reason: "deleted file mapping cannot be reopened reliably",
}]
);
}
#[test]
fn materializer_binds_byte_actions_to_process_specific_dumps() {
let plan = KboxlikeMemoryRestorePlan {
actions: vec![
KboxlikeMemoryRestoreAction::DumpAnonymousPrivate {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
len: 4,
prot: libc::PROT_READ | libc::PROT_WRITE,
label: Some("[heap]".to_owned()),
},
KboxlikeMemoryRestoreAction::RemapCleanFile {
host_pid: 2001,
model_pid: 7,
start: 0x2000,
len: 0x1000,
prot: libc::PROT_READ | libc::PROT_EXEC,
path: PathBuf::from("/usr/bin/chrome"),
file_offset: 0,
dev: "08:02".to_owned(),
inode: 11,
},
KboxlikeMemoryRestoreAction::DumpAnonymousPrivate {
host_pid: 2002,
model_pid: 8,
start: 0x1000,
len: 2,
prot: libc::PROT_READ | libc::PROT_WRITE,
label: Some("[heap]".to_owned()),
},
],
};
let dumps = KboxlikeMemoryDumpCapture {
dumps: vec![
KboxlikeMemoryDump {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
bytes: b"abcd".to_vec(),
},
KboxlikeMemoryDump {
host_pid: 2002,
model_pid: 8,
start: 0x1000,
bytes: b"ef".to_vec(),
},
],
failures: Vec::new(),
};
let materialized = materialize_memory_restore_actions(&plan, &dumps).unwrap();
assert_eq!(
materialized.actions,
vec![
KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
prot: libc::PROT_READ | libc::PROT_WRITE,
bytes: b"abcd".to_vec(),
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate {
label: Some("[heap]".to_owned()),
},
},
KboxlikeMaterializedMemoryRestoreAction::RemapCleanFile {
host_pid: 2001,
model_pid: 7,
start: 0x2000,
len: 0x1000,
prot: libc::PROT_READ | libc::PROT_EXEC,
path: PathBuf::from("/usr/bin/chrome"),
file_offset: 0,
dev: "08:02".to_owned(),
inode: 11,
},
KboxlikeMaterializedMemoryRestoreAction::ByteBacked {
host_pid: 2002,
model_pid: 8,
start: 0x1000,
prot: libc::PROT_READ | libc::PROT_WRITE,
bytes: b"ef".to_vec(),
source: KboxlikeByteBackedRestoreSource::AnonymousPrivate {
label: Some("[heap]".to_owned()),
},
},
]
);
}
#[test]
fn materializer_rejects_missing_or_short_dumps() {
let plan = KboxlikeMemoryRestorePlan {
actions: vec![KboxlikeMemoryRestoreAction::DumpAnonymousPrivate {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
len: 4,
prot: libc::PROT_READ,
label: None,
}],
};
assert_eq!(
materialize_memory_restore_actions(&plan, &KboxlikeMemoryDumpCapture::default())
.unwrap_err(),
KboxlikeRestoreMaterializeError::MissingDump {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
len: 4,
}
);
let dumps = KboxlikeMemoryDumpCapture {
dumps: vec![KboxlikeMemoryDump {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
bytes: b"abc".to_vec(),
}],
failures: Vec::new(),
};
assert_eq!(
materialize_memory_restore_actions(&plan, &dumps).unwrap_err(),
KboxlikeRestoreMaterializeError::DumpLengthMismatch {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
expected: 4,
actual: 3,
}
);
}
#[test]
fn materializer_rejects_duplicate_reused_or_unused_dumps() {
let plan = KboxlikeMemoryRestorePlan {
actions: vec![
KboxlikeMemoryRestoreAction::DumpAnonymousPrivate {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
len: 4,
prot: libc::PROT_READ,
label: None,
},
KboxlikeMemoryRestoreAction::DumpAnonymousPrivate {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
len: 4,
prot: libc::PROT_READ,
label: None,
},
],
};
let dumps = KboxlikeMemoryDumpCapture {
dumps: vec![KboxlikeMemoryDump {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
bytes: b"abcd".to_vec(),
}],
failures: Vec::new(),
};
assert_eq!(
materialize_memory_restore_actions(&plan, &dumps).unwrap_err(),
KboxlikeRestoreMaterializeError::ReusedDump {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
}
);
let duplicate_dumps = KboxlikeMemoryDumpCapture {
dumps: vec![
KboxlikeMemoryDump {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
bytes: b"abcd".to_vec(),
},
KboxlikeMemoryDump {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
bytes: b"abcd".to_vec(),
},
],
failures: Vec::new(),
};
assert_eq!(
materialize_memory_restore_actions(
&KboxlikeMemoryRestorePlan {
actions: Vec::new()
},
&duplicate_dumps
)
.unwrap_err(),
KboxlikeRestoreMaterializeError::DuplicateDump {
host_pid: 2001,
model_pid: 7,
start: 0x1000,
}
);
let unused_dumps = KboxlikeMemoryDumpCapture {
dumps: vec![KboxlikeMemoryDump {
host_pid: 2001,
model_pid: 7,
start: 0x3000,
bytes: b"x".to_vec(),
}],
failures: Vec::new(),
};
assert_eq!(
materialize_memory_restore_actions(
&KboxlikeMemoryRestorePlan {
actions: Vec::new()
},
&unused_dumps
)
.unwrap_err(),
KboxlikeRestoreMaterializeError::UnusedDump {
host_pid: 2001,
model_pid: 7,
start: 0x3000,
len: 1,
}
);
}
#[test]
fn process_vm_memory_reader_rejects_invalid_tracee_pid() {
assert_eq!(
process_vm_read_exact(0, 0x1000, 4).unwrap_err(),
KboxlikeMemoryReadError {
bytes_read: 0,
reason: "invalid tracee pid",
}
);
}
}