use std::collections::HashSet;
use std::fs;
use std::io;
fn read_task_state(tid: i32) -> Option<char> {
let status = fs::read_to_string(format!("/proc/{}/status", tid)).ok()?;
let line = status.lines().find(|l| l.starts_with("State:"))?;
line.split_whitespace().nth(1).and_then(|s| s.chars().next())
}
fn seize_and_interrupt(tid: i32) -> io::Result<bool> {
if read_task_state(tid) == Some('D') {
return Ok(false);
}
let ret = unsafe {
libc::ptrace(libc::PTRACE_SEIZE as libc::c_uint, tid, 0, 0)
};
if ret < 0 {
let err = io::Error::last_os_error();
if err.raw_os_error() == Some(libc::ESRCH) {
return Ok(false); }
return Err(err);
}
let ret = unsafe {
libc::ptrace(libc::PTRACE_INTERRUPT as libc::c_uint, tid, 0, 0)
};
if ret < 0 {
let err = io::Error::last_os_error();
let _ = unsafe { libc::ptrace(libc::PTRACE_DETACH, tid, 0, 0) };
if err.raw_os_error() == Some(libc::ESRCH) {
return Ok(false);
}
return Err(err);
}
let mut status: i32 = 0;
let _ = unsafe { libc::waitpid(tid, &mut status, libc::__WALL) };
Ok(true)
}
fn detach(tid: i32) {
let _ = unsafe { libc::ptrace(libc::PTRACE_DETACH, tid, 0, 0) };
}
fn list_threads_of_tgid(tgid: i32) -> io::Result<Vec<i32>> {
let dir = fs::read_dir(format!("/proc/{}/task", tgid))?;
let mut tids = Vec::new();
for entry in dir {
let entry = match entry {
Ok(e) => e,
Err(_) => continue,
};
let name = entry.file_name();
let name_str = match name.to_str() {
Some(s) => s,
None => continue,
};
if let Ok(tid) = name_str.parse::<i32>() {
tids.push(tid);
}
}
Ok(tids)
}
fn read_tgid_of_tid(tid: i32) -> io::Result<i32> {
let status = fs::read_to_string(format!("/proc/{}/status", tid))?;
for line in status.lines() {
if let Some(rest) = line.strip_prefix("Tgid:") {
return rest.trim().parse().map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("parse Tgid: {}", e),
)
});
}
}
Err(io::Error::new(
io::ErrorKind::InvalidData,
"no Tgid: line in /proc/<tid>/status",
))
}
#[derive(Debug, Default)]
pub(crate) struct SandboxFreeze {
pub sibling_tids: Vec<i32>,
pub peer_tids: Vec<i32>,
}
pub(crate) fn freeze_sandbox_for_execve(
processes: &crate::seccomp::state::ProcessIndex,
caller_tid: i32,
) -> io::Result<SandboxFreeze> {
let caller_tgid = read_tgid_of_tid(caller_tid)?;
let mut tgids: HashSet<i32> = processes.pids_snapshot();
tgids.insert(caller_tgid);
let mut sibling_tids: Vec<i32> = Vec::new();
let mut peer_tids: Vec<i32> = Vec::new();
for tgid in &tgids {
let tids = match list_threads_of_tgid(*tgid) {
Ok(t) => t,
Err(_) => continue,
};
for tid in tids {
if tid == caller_tid {
continue;
}
match seize_and_interrupt(tid) {
Ok(true) => {
if *tgid == caller_tgid {
sibling_tids.push(tid);
} else {
peer_tids.push(tid);
}
}
Ok(false) => continue, Err(e) => {
for t in &sibling_tids {
detach(*t);
}
for t in &peer_tids {
detach(*t);
}
return Err(e);
}
}
}
}
Ok(SandboxFreeze {
sibling_tids,
peer_tids,
})
}
pub(crate) fn detach_peers(peer_tids: &[i32]) {
for tid in peer_tids {
detach(*tid);
}
}
pub(crate) fn detach_all(freeze: &SandboxFreeze) {
for tid in &freeze.sibling_tids {
detach(*tid);
}
for tid in &freeze.peer_tids {
detach(*tid);
}
}
pub(crate) fn requires_freeze_on_continue(syscall_nr: i64) -> bool {
syscall_nr == libc::SYS_execve || syscall_nr == libc::SYS_execveat
}
#[cfg(test)]
mod tests {
use super::*;
use crate::seccomp::state::ProcessIndex;
#[test]
fn list_threads_of_tgid_includes_self() {
let our_tid = unsafe { libc::syscall(libc::SYS_gettid) } as i32;
let tids = list_threads_of_tgid(our_tid).unwrap();
assert!(tids.contains(&our_tid));
}
#[test]
fn requires_freeze_only_for_exec() {
assert!(requires_freeze_on_continue(libc::SYS_execve));
assert!(requires_freeze_on_continue(libc::SYS_execveat));
assert!(!requires_freeze_on_continue(libc::SYS_openat));
assert!(!requires_freeze_on_continue(libc::SYS_connect));
}
#[test]
fn freeze_sandbox_includes_peer_process() {
use std::process::{Command, Stdio};
let mut caller = Command::new("/bin/sleep")
.arg("60")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn caller sleep");
let caller_tid = caller.id() as i32;
let mut peer = Command::new("/bin/sleep")
.arg("60")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn peer sleep");
let peer_pid = peer.id() as i32;
std::thread::sleep(std::time::Duration::from_millis(50));
let processes = ProcessIndex::new();
processes
.register(peer_pid)
.expect("register peer in ProcessIndex");
let outcome = freeze_sandbox_for_execve(&processes, caller_tid)
.expect("freeze_sandbox_for_execve");
assert!(
outcome.peer_tids.contains(&peer_pid),
"peer pid {} should be in peer_tids: {:?}",
peer_pid,
outcome.peer_tids
);
let status = std::fs::read_to_string(format!("/proc/{}/status", peer_pid))
.expect("read peer status");
let state_line = status
.lines()
.find(|l| l.starts_with("State:"))
.expect("State: line");
assert!(
state_line.contains("t (tracing stop)") || state_line.contains("T (stopped)"),
"peer should be ptrace-stopped, got: {}",
state_line
);
detach_peers(&outcome.peer_tids);
let _ = peer.kill();
let _ = peer.wait();
let _ = caller.kill();
let _ = caller.wait();
}
}