use std::ffi::{CStr, CString};
use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use crate::resolved::ResolvedSandbox;
use crate::sandbox::Sandbox;
use crate::seccomp::bpf;
#[cfg(test)]
use crate::arch;
#[cfg(test)]
use crate::sys::structs::{
AF_INET, AF_INET6, CLONE_NS_FLAGS, DEFAULT_BLOCKLIST_SYSCALLS, PR_SET_DUMPABLE,
SIOCGIFCONF, SIOCETHTOOL, SOCK_DGRAM, SOCK_RAW, SOCK_TYPE_MASK, TIOCLINUX, TIOCSTI,
};
pub struct PipePair {
pub notif_r: OwnedFd,
pub notif_w: OwnedFd,
pub ready_r: OwnedFd,
pub ready_w: OwnedFd,
}
impl PipePair {
pub fn new() -> io::Result<Self> {
let mut notif_fds = [0i32; 2];
let mut ready_fds = [0i32; 2];
let ret = unsafe { libc::pipe2(notif_fds.as_mut_ptr(), libc::O_CLOEXEC) };
if ret < 0 {
return Err(io::Error::last_os_error());
}
let ret = unsafe { libc::pipe2(ready_fds.as_mut_ptr(), libc::O_CLOEXEC) };
if ret < 0 {
unsafe {
libc::close(notif_fds[0]);
libc::close(notif_fds[1]);
}
return Err(io::Error::last_os_error());
}
Ok(PipePair {
notif_r: unsafe { OwnedFd::from_raw_fd(notif_fds[0]) },
notif_w: unsafe { OwnedFd::from_raw_fd(notif_fds[1]) },
ready_r: unsafe { OwnedFd::from_raw_fd(ready_fds[0]) },
ready_w: unsafe { OwnedFd::from_raw_fd(ready_fds[1]) },
})
}
}
pub(crate) fn write_u32_fd(fd: RawFd, val: u32) -> io::Result<()> {
let buf = val.to_le_bytes();
let mut written = 0usize;
while written < 4 {
let ret = unsafe {
libc::write(
fd,
buf[written..].as_ptr() as *const libc::c_void,
4 - written,
)
};
if ret < 0 {
return Err(io::Error::last_os_error());
}
written += ret as usize;
}
Ok(())
}
pub(crate) fn read_u32_fd(fd: RawFd) -> io::Result<u32> {
let mut buf = [0u8; 4];
let mut total = 0usize;
while total < 4 {
let ret = unsafe {
libc::read(
fd,
buf[total..].as_mut_ptr() as *mut libc::c_void,
4 - total,
)
};
if ret < 0 {
return Err(io::Error::last_os_error());
}
if ret == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"pipe closed before 4 bytes read",
));
}
total += ret as usize;
}
Ok(u32::from_le_bytes(buf))
}
#[cfg(test)]
use crate::seccomp::syscall::syscall_name_to_nr;
pub(crate) use crate::seccomp_plan::{arg_filters_resolved, notif_syscalls_resolved};
pub fn notif_syscalls(policy: &Sandbox, sandbox_name: Option<&str>) -> Vec<u32> {
crate::seccomp_plan::notif_syscalls(policy, sandbox_name)
}
pub fn no_supervisor_blocklist_syscall_numbers(policy: &Sandbox) -> Vec<u32> {
crate::seccomp_plan::no_supervisor_blocklist_syscall_numbers(policy)
}
pub fn blocklist_syscall_numbers(policy: &Sandbox) -> Vec<u32> {
crate::seccomp_plan::blocklist_syscall_numbers(policy)
}
pub fn arg_filters(policy: &Sandbox) -> Vec<crate::sys::structs::SockFilter> {
crate::seccomp_plan::arg_filters(policy)
}
fn close_fds_above(min_fd: RawFd, keep: &[RawFd]) {
let fds_to_close: Vec<RawFd> = {
let dir = match std::fs::read_dir("/proc/self/fd") {
Ok(d) => d,
Err(_) => return,
};
dir.flatten()
.filter_map(|entry| {
entry.file_name().into_string().ok()
.and_then(|name| name.parse::<RawFd>().ok())
})
.filter(|&fd| fd > min_fd && !keep.contains(&fd))
.collect()
};
for fd in fds_to_close {
unsafe { libc::close(fd) };
}
}
fn write_id_maps(real_uid: u32, real_gid: u32, target_uid: u32, target_gid: u32) {
let _ = std::fs::write("/proc/self/uid_map", format!("{} {} 1\n", target_uid, real_uid));
let _ = std::fs::write("/proc/self/setgroups", "deny\n");
let _ = std::fs::write("/proc/self/gid_map", format!("{} {} 1\n", target_gid, real_gid));
}
pub(crate) enum ChildEntry<'a> {
Exec(&'a [CString]),
InProcess { name: &'a CStr, run: fn() },
}
pub(crate) struct ChildSpawnArgs<'a> {
pub sandbox: &'a Sandbox,
pub entry: ChildEntry<'a>,
pub pipes: &'a PipePair,
pub no_supervisor: bool,
pub keep_fds: &'a [RawFd],
pub sandbox_name: Option<&'a str>,
pub extra_syscalls: &'a [u32],
pub parent_pid: libc::pid_t,
}
fn set_proc_name(name: &CStr) {
unsafe { libc::prctl(libc::PR_SET_NAME, name.as_ptr() as libc::c_ulong, 0, 0, 0) };
}
pub(crate) fn confine_child(args: ChildSpawnArgs<'_>) -> ! {
let ChildSpawnArgs {
sandbox,
entry,
pipes,
no_supervisor,
keep_fds,
sandbox_name,
extra_syscalls,
parent_pid,
} = args;
macro_rules! fail {
($msg:expr) => {{
let err = std::io::Error::last_os_error();
let _ = write!(std::io::stderr(), "sandlock child: {}: {}\n", $msg, err);
unsafe { libc::_exit(127) };
}};
}
use std::io::Write;
if unsafe { libc::setpgid(0, 0) } != 0 {
fail!("setpgid");
}
if unsafe { libc::isatty(0) } == 1 {
unsafe {
libc::signal(libc::SIGTTOU, libc::SIG_IGN);
libc::tcsetpgrp(0, libc::getpgrp());
libc::signal(libc::SIGTTOU, libc::SIG_DFL);
}
}
if unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) } != 0 {
fail!("prctl(PR_SET_PDEATHSIG)");
}
if unsafe { libc::getppid() } != parent_pid {
fail!("parent died before confinement");
}
if sandbox.no_randomize_memory {
const ADDR_NO_RANDOMIZE: libc::c_ulong = 0x0040000;
let current = unsafe { libc::personality(0xffffffff) };
if current == -1 {
fail!("personality(query)");
}
if unsafe { libc::personality(current as libc::c_ulong | ADDR_NO_RANDOMIZE) } == -1 {
fail!("personality(ADDR_NO_RANDOMIZE)");
}
}
if let Some(ref cores) = sandbox.cpu_cores {
if !cores.is_empty() {
let mut set = unsafe { std::mem::zeroed::<libc::cpu_set_t>() };
unsafe { libc::CPU_ZERO(&mut set) };
for &core in cores {
unsafe { libc::CPU_SET(core as usize, &mut set) };
}
if unsafe {
libc::sched_setaffinity(
0,
std::mem::size_of::<libc::cpu_set_t>(),
&set,
)
} != 0
{
fail!("sched_setaffinity");
}
}
}
if sandbox.no_huge_pages {
if unsafe { libc::prctl(libc::PR_SET_THP_DISABLE, 1, 0, 0, 0) } != 0 {
fail!("prctl(PR_SET_THP_DISABLE)");
}
}
if sandbox.no_coredump {
let rlim = libc::rlimit { rlim_cur: 0, rlim_max: 0 };
if unsafe { libc::setrlimit(libc::RLIMIT_CORE, &rlim) } != 0 {
fail!("setrlimit(RLIMIT_CORE, 0)");
}
}
let real_uid = unsafe { libc::getuid() };
let real_gid = unsafe { libc::getgid() };
if let Some(run_as) = sandbox.user {
if run_as.uid != real_uid || run_as.gid != real_gid {
if unsafe { libc::unshare(libc::CLONE_NEWUSER) } != 0 {
fail!("unshare(CLONE_NEWUSER)");
}
write_id_maps(real_uid, real_gid, run_as.uid, run_as.gid);
}
}
let effective_cwd = if let Some(ref cwd) = sandbox.cwd {
if let Some(ref chroot_root) = sandbox.chroot {
Some(chroot_root.join(cwd.strip_prefix("/").unwrap_or(cwd)))
} else {
Some(cwd.clone())
}
} else if let Some(ref chroot_root) = sandbox.chroot {
Some(chroot_root.to_path_buf())
} else if let Some(ref workdir) = sandbox.workdir {
Some(workdir.clone())
} else {
None
};
if let Some(ref cwd) = effective_cwd {
let c_path = match CString::new(cwd.as_os_str().as_encoded_bytes()) {
Ok(c) => c,
Err(_) => fail!("invalid cwd path"),
};
if unsafe { libc::chdir(c_path.as_ptr()) } != 0 {
fail!("chdir");
}
}
if unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) } != 0 {
fail!("prctl(PR_SET_NO_NEW_PRIVS)");
}
if let Err(e) = crate::landlock::confine(sandbox) {
fail!(format!("landlock: {}", e));
}
let handler_syscalls: Vec<i64> = extra_syscalls.iter().map(|&nr| nr as i64).collect();
let resolved = ResolvedSandbox::from_sandbox(sandbox, sandbox_name, &handler_syscalls);
let args = arg_filters_resolved(&resolved);
let mut keep_fd: i32 = -1;
if no_supervisor {
let deny = no_supervisor_blocklist_syscall_numbers(sandbox);
let filter = match bpf::assemble_filter(&[], &deny, &args) {
Ok(f) => f,
Err(e) => fail!(format!("seccomp assemble: {}", e)),
};
if let Err(e) = bpf::install_deny_filter(&filter) {
fail!(format!("seccomp deny filter: {}", e));
}
if let Err(e) = write_u32_fd(pipes.notif_w.as_raw_fd(), 0) {
fail!(format!("write no-supervisor signal: {}", e));
}
} else {
let deny = blocklist_syscall_numbers(sandbox);
let mut notif = notif_syscalls_resolved(&resolved);
if !extra_syscalls.is_empty() {
notif.extend_from_slice(extra_syscalls);
}
notif.sort_unstable();
notif.dedup();
let filter = match bpf::assemble_filter(¬if, &deny, &args) {
Ok(f) => f,
Err(e) => fail!(format!("seccomp assemble: {}", e)),
};
let notif_fd = match bpf::install_filter(&filter) {
Ok(fd) => fd,
Err(e) => {
if e.raw_os_error() == Some(libc::EBUSY) {
let _ = write!(
std::io::stderr(),
"sandlock child: seccomp install: {} (an outer sandbox already owns the \
seccomp listener; pass --no-supervisor or Sandbox::no_supervisor(true) \
on this sandbox to nest)\n",
e,
);
unsafe { libc::_exit(127) };
}
fail!(format!("seccomp install: {}", e));
}
};
keep_fd = notif_fd.as_raw_fd();
if let Err(e) = write_u32_fd(pipes.notif_w.as_raw_fd(), keep_fd as u32) {
fail!(format!("write notif fd: {}", e));
}
std::mem::forget(notif_fd);
}
match read_u32_fd(pipes.ready_r.as_raw_fd()) {
Ok(_) => {}
Err(e) => fail!(format!("read ready signal: {}", e)),
}
let mut fds_to_keep: Vec<RawFd> = keep_fds.to_vec();
if keep_fd >= 0 {
fds_to_keep.push(keep_fd);
}
close_fds_above(2, &fds_to_keep);
if sandbox.clean_env {
for (key, _) in std::env::vars_os() {
std::env::remove_var(&key);
}
}
for name in &sandbox.inject_env_strip {
std::env::remove_var(name);
}
for (key, value) in &sandbox.env {
std::env::set_var(key, value);
}
if let Some(ref devices) = sandbox.gpu_devices {
if !devices.is_empty() {
let vis = devices.iter().map(|d| d.to_string()).collect::<Vec<_>>().join(",");
std::env::set_var("CUDA_VISIBLE_DEVICES", &vis);
std::env::set_var("ROCR_VISIBLE_DEVICES", &vis);
}
}
let cmd: &[CString] = match entry {
ChildEntry::InProcess { name, run } => {
set_proc_name(name);
run();
unsafe { libc::_exit(0) };
}
ChildEntry::Exec(cmd) => cmd,
};
debug_assert!(!cmd.is_empty(), "cmd must not be empty");
let argv_ptrs: Vec<*const libc::c_char> = cmd
.iter()
.map(|s| s.as_ptr())
.chain(std::iter::once(std::ptr::null()))
.collect();
if sandbox.chroot.is_some() {
let mut exec_path = vec![0u8; libc::PATH_MAX as usize];
let orig = cmd[0].as_bytes_with_nul();
exec_path[..orig.len()].copy_from_slice(orig);
unsafe {
libc::execvp(
exec_path.as_ptr() as *const libc::c_char,
argv_ptrs.as_ptr(),
)
};
} else {
unsafe { libc::execvp(argv_ptrs[0], argv_ptrs.as_ptr()) };
}
fail!(format!("execvp '{}'", cmd[0].to_string_lossy()));
}
#[cfg(test)]
mod tests;