pub mod audit_reader;
pub mod cgroup;
pub mod debug_guard;
pub mod landlock;
pub mod limits;
pub mod mounts;
pub mod namespaces;
pub mod net_relay;
pub mod observe_seccomp;
pub mod proctrack;
pub mod seccomp_netblock;
pub mod visibility;
use std::ffi::CString;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use anyhow::{anyhow, bail, Result};
use super::handle::{KillStrategy, SandboxHandle, SpawnOptions, StdioMode};
use super::Spawned;
use crate::config::NetMode;
use crate::policy::{Policy, Tier};
const SETUP_TIMEOUT_MS: i32 = 30_000;
#[derive(Debug, Clone)]
pub struct Probe {
pub kernel: String,
pub landlock_abi: Option<u32>,
pub userns_available: bool,
pub full_tier_available: bool,
pub seccomp_filter_available: bool,
pub seccomp_notify_available: bool,
pub audit_feed_readable: bool,
}
pub fn probe() -> Probe {
let kernel = kernel_release();
let userns_available = namespaces::probe_unprivileged_userns();
Probe {
kernel,
landlock_abi: landlock::abi_version(),
userns_available,
full_tier_available: userns_available && namespaces::probe_full_tier(),
seccomp_filter_available: seccomp_netblock::probe_available(),
seccomp_notify_available: observe_seccomp::probe_available(),
audit_feed_readable: audit_reader::open_audit_feed().is_ok(),
}
}
fn kernel_release() -> String {
let mut uts = std::mem::MaybeUninit::<libc::utsname>::uninit();
if unsafe { libc::uname(uts.as_mut_ptr()) } != 0 {
return "unknown".into();
}
let u = unsafe { uts.assume_init() };
let bytes: Vec<u8> = u
.release
.iter()
.take_while(|&&c| c != 0)
.map(|&c| c as u8)
.collect();
String::from_utf8_lossy(&bytes).to_string()
}
pub fn pick_tier(probe: &Probe) -> Result<Tier> {
match std::env::var("VETTO_FORCE_TIER").as_deref() {
Ok("seccomp") if probe.seccomp_filter_available => return Ok(Tier::Seccomp),
Ok("fs-only") if probe.seccomp_filter_available => return Ok(Tier::FsOnly),
Ok("full") if probe.full_tier_available => return Ok(Tier::Full),
_ => {}
}
if probe.landlock_abi.is_none() {
if probe.seccomp_filter_available {
return Ok(Tier::Seccomp);
}
bail!(
"Landlock and seccomp filters are unavailable on this kernel; \
refusing to run the agent unsandboxed (fail-closed)"
);
}
if probe.full_tier_available {
return Ok(Tier::Full);
}
if probe.seccomp_filter_available {
return Ok(Tier::FsOnly);
}
bail!(
"no enforcement tier possible: unprivileged user namespaces are disabled AND \
seccomp filters are unavailable; refusing to run unsandboxed (fail-closed)"
);
}
pub struct LinuxSandbox {
pub probe: Probe,
pub tier: Tier,
pub net: NetMode,
pub observe_seccomp: bool,
}
impl LinuxSandbox {
pub fn spawn(self, policy: &Policy, opts: SpawnOptions) -> Result<Spawned> {
if (self.tier == Tier::FsOnly || self.tier == Tier::Seccomp) && self.net.uses_relay() {
bail!(
"--net relay modes require Tier FULL (unprivileged user namespaces), \
which is unavailable on this machine; refusing to run (fail-closed)"
);
}
let relay_port = match self.net {
NetMode::Allowlist(_) | NetMode::Strict(_) | NetMode::Ask => {
Some(net_relay::RELAY_PORT_BASE)
}
NetMode::Off => None,
};
match self.tier {
Tier::Full => spawn_full(policy, opts, self.observe_seccomp, relay_port),
Tier::FsOnly => spawn_fs_only(policy, opts, self.observe_seccomp),
Tier::Seccomp => spawn_seccomp_only(policy, opts, self.observe_seccomp),
}
}
}
fn errno_val() -> i32 {
std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
}
fn pipe2_cloexec() -> Result<(OwnedFd, OwnedFd)> {
let mut fds = [0 as libc::c_int; 2];
if unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC) } != 0 {
bail!("pipe2: {}", std::io::Error::last_os_error());
}
Ok((unsafe { OwnedFd::from_raw_fd(fds[0]) }, unsafe {
OwnedFd::from_raw_fd(fds[1])
}))
}
fn socketpair_cloexec() -> Result<(OwnedFd, OwnedFd)> {
let mut fds = [0 as libc::c_int; 2];
if unsafe {
libc::socketpair(
libc::AF_UNIX,
libc::SOCK_STREAM | libc::SOCK_CLOEXEC,
0,
fds.as_mut_ptr(),
)
} != 0
{
bail!("socketpair: {}", std::io::Error::last_os_error());
}
Ok((unsafe { OwnedFd::from_raw_fd(fds[0]) }, unsafe {
OwnedFd::from_raw_fd(fds[1])
}))
}
fn close_range(first: u32, last: u32) {
if first > last {
return;
}
const SYS_CLOSE_RANGE: libc::c_long = 436;
unsafe { libc::syscall(SYS_CLOSE_RANGE, first, last, 0u32) };
}
fn close_all_except(keep: &[RawFd]) {
let mut keep: Vec<u32> = keep.iter().map(|&f| f as u32).collect();
keep.sort_unstable();
keep.dedup();
let mut cur = 3u32;
for &fd in &keep {
if fd > cur {
close_range(cur, fd - 1);
}
cur = fd + 1;
}
close_range(cur, u32::MAX);
}
fn child_write_all(fd: RawFd, mut buf: &[u8]) {
while !buf.is_empty() {
let n = unsafe { libc::write(fd, buf.as_ptr().cast(), buf.len()) };
if n > 0 {
buf = &buf[n as usize..];
} else if n < 0 && errno_val() == libc::EINTR {
continue;
} else {
return;
}
}
}
fn child_fail(err_w: RawFd, code: i32, msg: &str) -> ! {
let mut m = String::with_capacity(msg.len() + 4);
m.push('E');
m.push(':');
m.push_str(msg);
m.push('\n');
child_write_all(err_w, m.as_bytes());
unsafe { libc::_exit(code) }
}
fn child_exit(code: i32) -> ! {
unsafe { libc::_exit(code) }
}
fn child_pdeathsig(parent_pid: libc::pid_t) {
if unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL, 0, 0, 0) } != 0 {
child_exit(113);
}
if unsafe { libc::getppid() } != parent_pid {
child_exit(113);
}
}
fn drop_agent_capabilities() -> Result<(), String> {
const SECURE_NOROOT_AND_NO_SETUID_FIXUP_LOCKED: libc::c_ulong = 0x0f;
if unsafe {
libc::prctl(
libc::PR_SET_SECUREBITS,
SECURE_NOROOT_AND_NO_SETUID_FIXUP_LOCKED,
0,
0,
0,
)
} != 0
{
return Err(format!(
"lock securebits: {}",
std::io::Error::last_os_error()
));
}
for capability in 0..64 {
let result = unsafe { libc::prctl(libc::PR_CAPBSET_DROP, capability, 0, 0, 0) };
if result != 0 && std::io::Error::last_os_error().raw_os_error() != Some(libc::EINVAL) {
return Err(format!(
"drop capability {capability} from bounding set: {}",
std::io::Error::last_os_error()
));
}
}
const PR_CAP_AMBIENT_CLEAR_ALL: libc::c_ulong = 4;
if unsafe { libc::prctl(libc::PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0) } != 0 {
return Err(format!(
"clear ambient capabilities: {}",
std::io::Error::last_os_error()
));
}
#[repr(C)]
struct CapHeader {
version: u32,
pid: i32,
}
#[repr(C)]
#[derive(Clone, Copy)]
struct CapData {
effective: u32,
permitted: u32,
inheritable: u32,
}
const LINUX_CAPABILITY_VERSION_3: u32 = 0x2008_0522;
let mut header = CapHeader {
version: LINUX_CAPABILITY_VERSION_3,
pid: 0,
};
let data = [
CapData {
effective: 0,
permitted: 0,
inheritable: 0,
},
CapData {
effective: 0,
permitted: 0,
inheritable: 0,
},
];
if unsafe {
libc::syscall(
libc::SYS_capset,
&mut header as *mut CapHeader,
data.as_ptr(),
)
} != 0
{
return Err(format!(
"clear capability sets: {}",
std::io::Error::last_os_error()
));
}
if unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 1, 0, 0, 0) } != 0 {
return Err(format!(
"restore parent observability: {}",
std::io::Error::last_os_error()
));
}
Ok(())
}
fn decode_status(status: i32) -> i32 {
if libc::WIFEXITED(status) {
libc::WEXITSTATUS(status)
} else if libc::WIFSIGNALED(status) {
-libc::WTERMSIG(status)
} else {
-1
}
}
fn exit_byte(code: i32) -> i32 {
if code < 0 {
128 - code } else {
code
}
}
enum ByteRead {
Byte(u8),
Eof,
Timeout,
}
fn read_byte(fd: RawFd, timeout_ms: i32) -> ByteRead {
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms as u64);
loop {
let now = std::time::Instant::now();
if now >= deadline {
return ByteRead::Timeout;
}
let remain = (deadline - now).as_millis() as i32 + 1;
let mut pfd = libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
};
let pr = unsafe { libc::poll(&mut pfd, 1, remain) };
if pr <= 0 {
if pr < 0 && errno_val() == libc::EINTR {
continue;
}
if std::time::Instant::now() >= deadline {
return ByteRead::Timeout;
}
continue;
}
let mut b = [0u8; 1];
let n = unsafe { libc::read(fd, b.as_mut_ptr().cast(), 1) };
return if n == 1 {
ByteRead::Byte(b[0])
} else if n == 0 {
ByteRead::Eof
} else if errno_val() == libc::EINTR {
continue;
} else {
ByteRead::Eof
};
}
}
fn read_exact_timeout(fd: RawFd, buf: &mut [u8], timeout_ms: i32) -> std::io::Result<()> {
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms as u64);
let mut filled = 0usize;
while filled < buf.len() {
let now = std::time::Instant::now();
if now >= deadline {
return Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"read timeout",
));
}
let remain = (deadline - now).as_millis() as i32 + 1;
let mut pfd = libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
};
if unsafe { libc::poll(&mut pfd, 1, remain) } <= 0 {
continue;
}
let n = unsafe { libc::read(fd, buf[filled..].as_mut_ptr().cast(), buf.len() - filled) };
if n > 0 {
filled += n as usize;
} else if n == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"eof",
));
} else if errno_val() != libc::EINTR {
return Err(std::io::Error::last_os_error());
}
}
Ok(())
}
fn drain_err_reason(err_r: RawFd) -> String {
let mut out = Vec::new();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while std::time::Instant::now() < deadline {
let mut pfd = libc::pollfd {
fd: err_r,
events: libc::POLLIN,
revents: 0,
};
if unsafe { libc::poll(&mut pfd, 1, 200) } <= 0 {
break;
}
let mut chunk = [0u8; 512];
let n = unsafe { libc::read(err_r, chunk.as_mut_ptr().cast(), chunk.len()) };
if n <= 0 {
break;
}
out.extend_from_slice(&chunk[..n as usize]);
if out.len() > 8192 {
break;
}
}
String::from_utf8_lossy(&out).trim().to_string()
}
fn reap_child(pid: libc::pid_t) -> i32 {
loop {
let mut status = 0i32;
let r = unsafe { libc::waitpid(pid, &mut status, 0) };
if r == pid {
return decode_status(status);
}
if r < 0 && errno_val() != libc::EINTR {
return -1;
}
}
}
fn kill_and_reap(pid: libc::pid_t) -> i32 {
unsafe { libc::kill(pid, libc::SIGKILL) };
reap_child(pid)
}
fn err_from_dead_child(pid: libc::pid_t, err_r: RawFd) -> anyhow::Error {
let reason = drain_err_reason(err_r);
let code = reap_child(pid);
if reason.is_empty() {
anyhow!("sandbox child died during setup (exit {code})")
} else {
anyhow!(
"sandbox setup failed (child exit {code}): {}",
reason.trim_start_matches("E:")
)
}
}
fn child_stdio_setup(stdio: &StdioMode) -> Result<(), String> {
match *stdio {
StdioMode::Pty { slave_fd } => {
if unsafe { libc::ioctl(slave_fd, libc::TIOCSCTTY, 0i32) } != 0 {
return Err(format!("TIOCSCTTY: {}", errno_val()));
}
dup2_all(slave_fd)
}
StdioMode::Captured { stdout_w, stderr_w } => {
let devnull = unsafe {
libc::open(
b"/dev/null\0".as_ptr().cast(),
libc::O_RDONLY | libc::O_CLOEXEC,
)
};
if devnull < 0 {
return Err(format!("open /dev/null: {}", errno_val()));
}
dup2_all(devnull)?;
dup2_to(stdout_w, 1)?;
dup2_to(stderr_w, 2)?;
unsafe { libc::close(devnull) };
Ok(())
}
StdioMode::Inherit => Ok(()),
}
}
fn dup2_to(fd: RawFd, target: RawFd) -> Result<(), String> {
loop {
if unsafe { libc::dup2(fd, target) } >= 0 {
return Ok(());
}
if errno_val() != libc::EINTR {
return Err(format!(
"dup2({fd}, {target}): {}",
std::io::Error::last_os_error()
));
}
}
}
fn dup2_all(fd: RawFd) -> Result<(), String> {
dup2_to(fd, 0)?;
dup2_to(fd, 1)?;
dup2_to(fd, 2)
}
fn append_stdio_fds(keep: &mut Vec<RawFd>, stdio: StdioMode) {
match stdio {
StdioMode::Pty { slave_fd } => keep.push(slave_fd),
StdioMode::Captured { stdout_w, stderr_w } => {
keep.push(stdout_w);
keep.push(stderr_w);
}
StdioMode::Inherit => {}
}
}
fn close_stdio_fds(stdio: StdioMode) {
let mut fds = [None, None];
match stdio {
StdioMode::Pty { slave_fd } => fds[0] = Some(slave_fd),
StdioMode::Captured { stdout_w, stderr_w } => {
fds[0] = Some(stdout_w);
fds[1] = Some(stderr_w);
}
StdioMode::Inherit => {}
}
for fd in fds.into_iter().flatten().filter(|fd| *fd > 2) {
unsafe { libc::close(fd) };
}
}
fn child_exec(policy: &Policy, opts: &SpawnOptions) -> ! {
let mut argv = Vec::with_capacity(opts.agent_cmd.len() + 1);
for a in &opts.agent_cmd {
match CString::new(a.as_str()) {
Ok(c) => argv.push(c),
Err(_) => child_exit(127),
}
}
let mut env: std::collections::BTreeMap<std::ffi::OsString, std::ffi::OsString> =
std::env::vars_os()
.filter(|(key, _)| policy.environment.allows(key))
.collect();
crate::cred_broker::filter_proxy_secrets(&mut env, &policy.secret_proxies);
for (k, v) in &opts.env_extra {
env.insert(
std::ffi::OsString::from(k.as_str()),
std::ffi::OsString::from(v.as_str()),
);
}
let mut envp = Vec::with_capacity(env.len());
for (k, v) in &env {
let mut entry = k.as_encoded_bytes().to_vec();
entry.push(b'=');
entry.extend_from_slice(v.as_encoded_bytes());
if let Ok(c) = CString::new(entry) {
envp.push(c);
}
}
let prog = match argv.first() {
Some(p) => p.clone(),
None => child_exit(127),
};
let mut argv_ptr: Vec<*const libc::c_char> = argv.iter().map(|a| a.as_ptr()).collect();
argv_ptr.push(std::ptr::null());
let mut envp_ptr: Vec<*const libc::c_char> = envp.iter().map(|e| e.as_ptr()).collect();
envp_ptr.push(std::ptr::null());
if !opts.cwd.as_os_str().is_empty() {
if let Err(e) = std::env::set_current_dir(&opts.cwd) {
let _ = e;
child_exit(117);
}
}
if let Err(error) = limits::apply_before_exec(&policy.limits) {
let message = format!("[vetto-child] resource limits failed: {error}\n");
unsafe { libc::write(2, message.as_ptr().cast(), message.len()) };
child_exit(126);
}
if let Err(error) = limits::apply_io_priority(policy.io_priority.as_deref()) {
let message = format!("[vetto-child] io priority failed: {error}\n");
unsafe { libc::write(2, message.as_ptr().cast(), message.len()) };
child_exit(126);
}
let r = unsafe { libc::execve(prog.as_ptr(), argv_ptr.as_ptr(), envp_ptr.as_ptr()) };
let msg = format!(
"[vetto-child] execve failed r={r} errno={}\n",
std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
);
unsafe { libc::write(2, msg.as_ptr().cast(), msg.len()) };
child_exit(127)
}
fn child_relay(relay_end: RawFd, s_pid: libc::pid_t, port: u16) -> ! {
child_pdeathsig(s_pid);
close_all_except(&[relay_end]);
let devnull = unsafe {
libc::open(
b"/dev/null\0".as_ptr().cast(),
libc::O_RDWR | libc::O_CLOEXEC,
)
};
if devnull < 0 || dup2_all(devnull).is_err() {
child_exit(111);
}
unsafe { libc::close(devnull) };
net_relay::serve_relay(relay_end, port)
}
fn child_b(
alive_r: RawFd,
err_w: RawFd,
notif_child: Option<RawFd>,
observe: bool,
policy: &Policy,
opts: &SpawnOptions,
socket_policy: seccomp_netblock::SocketPolicy,
) -> ! {
if unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL, 0, 0, 0) } != 0 {
child_exit(113);
}
match mounts::mount_restricted_proc() {
Ok(mounts::ProcVisibility::HidePid) => {}
Ok(mounts::ProcVisibility::Fallback) => {
let message = b"[vetto-child] /proc hidepid unsupported; private proc fallback\n";
unsafe { libc::write(2, message.as_ptr().cast(), message.len()) };
}
Err(error) => child_fail(err_w, 115, &format!("restricted /proc: {error}")),
}
if let Err(error) = landlock::apply_policy_with_net_ports(
&policy.allow_write,
&policy.allow_read,
false,
&policy.net_bind_ports,
&policy.net_connect_ports,
) {
child_fail(err_w, 120, &format!("{error}"));
}
let mut keep = vec![0, 1, 2, alive_r, err_w];
if let Some(fd) = notif_child {
keep.push(fd);
}
append_stdio_fds(&mut keep, opts.stdio);
close_all_except(&keep);
let b_pid = unsafe { libc::getpid() };
let c_pid = unsafe { libc::fork() };
if c_pid < 0 {
child_fail(err_w, 118, "fork agent failed");
}
if c_pid == 0 {
child_pdeathsig(b_pid);
let mut keep = vec![0, 1, 2, err_w];
if let Some(fd) = notif_child {
keep.push(fd);
}
append_stdio_fds(&mut keep, opts.stdio);
close_all_except(&keep);
if matches!(opts.stdio, StdioMode::Pty { .. }) && unsafe { libc::setsid() } < 0 {
child_fail(err_w, 125, "setsid failed");
}
if let Err(msg) = child_stdio_setup(&opts.stdio) {
child_fail(err_w, 124, &format!("stdio: {msg}"));
}
if let Err(error) = drop_agent_capabilities() {
child_fail(err_w, 126, &format!("drop capabilities: {error}"));
}
if observe {
if let Some(nc) = notif_child {
let mut ok = false;
if let Ok(listener) = observe_seccomp::install_tap() {
child_write_all(nc, b"T");
if net_relay::send_fd(nc, listener).is_ok() {
ok = true;
}
unsafe { libc::close(listener) };
}
if !ok {
child_write_all(nc, b"N");
}
unsafe { libc::close(nc) };
}
}
if let Err(e) = seccomp_netblock::install_for_profile(socket_policy, policy.seccomp_profile)
{
child_fail(err_w, 126, &format!("seccomp hardening: {e}"));
}
child_write_all(err_w, b"R");
close_all_except(&[0, 1, 2]);
child_exec(policy, opts)
}
if let Some(fd) = notif_child {
unsafe { libc::close(fd) };
}
close_stdio_fds(opts.stdio);
let mut c_code: i32 = 0;
loop {
loop {
let mut status = 0i32;
let r = unsafe { libc::waitpid(-1, &mut status, libc::WNOHANG) };
if r == c_pid {
c_code = decode_status(status);
unsafe { libc::kill(-1, libc::SIGKILL) };
} else if r > 0 {
continue; } else if r < 0 && errno_val() == libc::ECHILD {
child_exit(exit_byte(c_code));
} else {
break; }
}
let mut pfd = libc::pollfd {
fd: alive_r,
events: libc::POLLIN,
revents: 0,
};
let pr = unsafe { libc::poll(&mut pfd, 1, 50) };
if pr > 0 && (pfd.revents & (libc::POLLIN | libc::POLLHUP | libc::POLLERR)) != 0 {
let mut b = [0u8; 1];
let n = unsafe { libc::read(alive_r, b.as_mut_ptr().cast(), 1) };
if n == 0 {
unsafe { libc::kill(-1, libc::SIGKILL) };
loop {
let mut status = 0i32;
let r = unsafe { libc::waitpid(-1, &mut status, 0) };
if r < 0 {
break;
}
}
child_exit(exit_byte(c_code));
}
}
}
}
struct FullChildArgs<'a> {
parent_pid: libc::pid_t,
err_w: RawFd,
map_w: RawFd,
ack_r: RawFd,
alive_r: RawFd,
relay_end: Option<RawFd>,
notif_child: Option<RawFd>,
relay_port: Option<u16>,
observe: bool,
policy: &'a Policy,
opts: &'a SpawnOptions,
}
unsafe fn child_full(a: FullChildArgs<'_>) -> ! {
let FullChildArgs {
parent_pid,
err_w,
map_w,
ack_r,
alive_r,
relay_end,
notif_child,
relay_port,
observe,
policy,
opts,
} = a;
let mut keep: Vec<RawFd> = vec![0, 1, 2, err_w, map_w, ack_r, alive_r];
if let Some(fd) = relay_end {
keep.push(fd);
}
if let Some(fd) = notif_child {
keep.push(fd);
}
if let StdioMode::Pty { slave_fd } = opts.stdio {
keep.push(slave_fd);
}
if let StdioMode::Captured { stdout_w, stderr_w } = opts.stdio {
keep.push(stdout_w);
keep.push(stderr_w);
}
close_all_except(&keep);
child_pdeathsig(parent_pid);
if let Err(e) = namespaces::unshare(namespaces::CLONE_NEWUSER) {
child_fail(err_w, 114, &format!("unshare user: {e}"));
}
let my_pid = unsafe { libc::getpid() };
child_write_all(map_w, &my_pid.to_le_bytes());
let mut ack = [0u8; 1];
let n = unsafe { libc::read(ack_r, ack.as_mut_ptr().cast(), 1) };
if n != 1 || ack[0] != 0 {
child_exit(119);
}
unsafe {
libc::close(map_w);
libc::close(ack_r);
}
if let Err(e) = namespaces::unshare(namespaces::CLONE_NEWNS) {
child_fail(err_w, 115, &format!("unshare mount: {e}"));
}
if let Err(e) = mounts::make_root_private() {
child_fail(err_w, 115, &format!("make root private: {e}"));
}
if let Err(e) = mounts::isolate_dev_shm() {
child_fail(err_w, 115, &format!("isolate /dev/shm: {e}"));
}
if policy.tmpfs_tmp {
if let Err(e) = mounts::isolate_tmp() {
child_fail(err_w, 115, &format!("isolate /tmp: {e}"));
}
}
if let Err(e) = mounts::remount_sys_readonly() {
child_fail(err_w, 115, &format!("remount /sys read-only: {e}"));
}
let _ = mounts::mask_sensitive_proc_paths();
let _ = mounts::mount_ro_caches(&policy.ro_mounts);
if let Err(e) = mounts::mask_restricted_devices(policy.dev_allow.as_deref()) {
child_fail(err_w, 115, &format!("mask restricted devices: {e}"));
}
if let Err(e) = namespaces::unshare(namespaces::CLONE_NEWIPC) {
child_fail(err_w, 115, &format!("unshare ipc: {e}"));
}
if let Err(e) = namespaces::unshare(namespaces::CLONE_NEWNET) {
child_fail(err_w, 115, &format!("unshare net: {e}"));
}
if let Some(port) = relay_port {
let relay_fd = relay_end.expect("relay fd wired for allowlist mode");
if let Err(e) = mounts::blackhole_resolv_conf() {
child_fail(err_w, 121, &format!("blackhole resolv.conf: {e}"));
}
let s_pid = unsafe { libc::getpid() };
let r = unsafe { libc::fork() };
if r < 0 {
child_fail(err_w, 116, "fork relay failed");
}
if r == 0 {
child_relay(relay_fd, s_pid, port);
}
unsafe { libc::close(relay_fd) };
}
if let Err(e) = namespaces::unshare(namespaces::CLONE_NEWPID) {
child_fail(err_w, 115, &format!("unshare pidns: {e}"));
}
for entry in &policy.deny_resolved {
match mounts::mask_path(&entry.path, entry.is_dir) {
Ok(true) => {}
Ok(false) => {} Err(e) => child_fail(
err_w,
121,
&format!("mask overlay {}: {e}", entry.path.display()),
),
}
}
let socket_policy = match relay_port {
Some(_) => seccomp_netblock::SocketPolicy::UnixAndIp,
None => seccomp_netblock::SocketPolicy::UnixOnly,
};
let b = unsafe { libc::fork() };
if b < 0 {
child_fail(err_w, 122, "fork supervisor failed");
}
if b == 0 {
child_b(
alive_r,
err_w,
notif_child,
observe,
policy,
opts,
socket_policy,
);
}
close_all_except(&[]);
let mut status = 0i32;
let code = loop {
let r = unsafe { libc::waitpid(b, &mut status, 0) };
if r == b {
break decode_status(status);
}
if r < 0 && errno_val() != libc::EINTR {
break -1;
}
};
child_exit(exit_byte(code))
}
fn spawn_full(
policy: &Policy,
opts: SpawnOptions,
observe: bool,
relay_port: Option<u16>,
) -> Result<Spawned> {
let parent_pid = unsafe { libc::getpid() };
let (err_r, err_w) = pipe2_cloexec()?;
let (map_r, map_w) = pipe2_cloexec()?;
let (ack_r, ack_w) = pipe2_cloexec()?;
let (alive_r, alive_w) = pipe2_cloexec()?;
let relay_pair = if relay_port.is_some() {
Some(socketpair_cloexec()?)
} else {
None
};
let (broker_end, relay_end) = match relay_pair {
Some((a, b)) => (Some(a), Some(b)),
None => (None, None),
};
let (notif_parent, notif_child) = if observe {
let (a, b) = socketpair_cloexec()?;
(Some(a), Some(b))
} else {
(None, None)
};
let args = FullChildArgs {
parent_pid,
err_w: err_w.as_raw_fd(),
map_w: map_w.as_raw_fd(),
ack_r: ack_r.as_raw_fd(),
alive_r: alive_r.as_raw_fd(),
relay_end: relay_end.as_ref().map(|f| f.as_raw_fd()),
notif_child: notif_child.as_ref().map(|f| f.as_raw_fd()),
relay_port,
observe,
policy,
opts: &opts,
};
let pid = unsafe { libc::fork() };
if pid < 0 {
bail!("fork: {}", std::io::Error::last_os_error());
}
if pid == 0 {
unsafe { child_full(args) }
}
drop(err_w);
drop(map_w);
drop(ack_r);
drop(alive_r);
drop(relay_end);
drop(notif_child);
let mut pid_buf = [0u8; 4];
if let Err(e) = read_exact_timeout(map_r.as_raw_fd(), &mut pid_buf, SETUP_TIMEOUT_MS) {
let code = kill_and_reap(pid);
let reason = drain_err_reason(err_r.as_raw_fd());
return Err(anyhow!(
"userns handshake failed ({e}, child exit {code}): {}",
reason.trim_start_matches("E:")
));
}
let child_pid = u32::from_le_bytes(pid_buf) as libc::pid_t;
let maps_ok = namespaces::write_id_maps(child_pid).is_ok();
let ack: &[u8] = if maps_ok { &[0] } else { &[1] };
let _ = unsafe { libc::write(ack_w.as_raw_fd(), ack.as_ptr().cast(), ack.len()) };
if !maps_ok {
let code = reap_child(pid);
return Err(anyhow!(
"writing uid_map/gid_map failed (child exit {code}); \
unprivileged userns may be restricted here"
));
}
drop(map_r);
drop(ack_w);
match read_byte(err_r.as_raw_fd(), SETUP_TIMEOUT_MS) {
ByteRead::Byte(b'R') => {}
ByteRead::Byte(b'E') | ByteRead::Eof => {
return Err(err_from_dead_child(pid, err_r.as_raw_fd()));
}
ByteRead::Byte(other) => {
let code = reap_child(pid);
return Err(anyhow!(
"unexpected setup byte {other:#x} from sandbox child (exit {code})"
));
}
ByteRead::Timeout => {
let code = kill_and_reap(pid);
return Err(anyhow!("sandbox setup timed out (child exit {code})"));
}
}
let notif_listener = match notif_parent {
Some(np) => match read_byte(np.as_raw_fd(), SETUP_TIMEOUT_MS) {
ByteRead::Byte(b'T') => net_relay::recv_fd(np.as_raw_fd()).ok(),
_ => None,
},
None => None,
};
let cgroup_handle =
match cgroup::setup_cgroup(policy.cgroup.as_ref(), policy.cpu_max.as_deref()) {
Ok(Some(cg)) => {
let _ = cg.add_process(pid as u32);
Some(cg)
}
_ => None,
};
Ok(Spawned {
handle: SandboxHandle {
root_pid: pid as u32,
strategy: Some(KillStrategy::PidNsPipe(alive_w)),
_cgroup: cgroup_handle,
},
broker_ctrl_fd: broker_end,
relay_port,
notif_listener,
})
}
struct FsChildArgs<'a> {
parent_pid: libc::pid_t,
err_w: RawFd,
notif_child: Option<RawFd>,
observe: bool,
net_off: bool,
policy: &'a Policy,
opts: &'a SpawnOptions,
}
unsafe fn child_fs_only(a: FsChildArgs<'_>) -> ! {
let FsChildArgs {
parent_pid,
err_w,
notif_child,
observe,
net_off,
policy,
opts,
} = a;
let mut keep: Vec<RawFd> = vec![0, 1, 2, err_w];
if let Some(fd) = notif_child {
keep.push(fd);
}
if let StdioMode::Pty { slave_fd } = opts.stdio {
keep.push(slave_fd);
}
if let StdioMode::Captured { stdout_w, stderr_w } = opts.stdio {
keep.push(stdout_w);
keep.push(stderr_w);
}
close_all_except(&keep);
child_pdeathsig(parent_pid);
match opts.stdio {
StdioMode::Pty { .. } => {
if unsafe { libc::setsid() } < 0 {
child_fail(
err_w,
125,
&format!("setsid: {}", std::io::Error::last_os_error()),
);
}
}
StdioMode::Captured { .. } | StdioMode::Inherit => {
if unsafe { libc::setpgid(0, 0) } < 0 {
child_fail(
err_w,
125,
&format!("setpgid: {}", std::io::Error::last_os_error()),
);
}
}
}
if net_off {
if let Err(e) = seccomp_netblock::install_for_profile(
seccomp_netblock::SocketPolicy::UnixOnly,
policy.seccomp_profile,
) {
child_fail(err_w, 123, &format!("network block: {e}"));
}
}
if let Err(e) = landlock::apply_policy_with_net_ports(
&policy.allow_write,
&policy.allow_read,
true,
&policy.net_bind_ports,
&policy.net_connect_ports,
) {
child_fail(err_w, 120, &format!("{e}"));
}
if observe {
if let Some(nc) = notif_child {
let mut ok = false;
if let Ok(listener) = observe_seccomp::install_tap() {
child_write_all(nc, b"T");
if net_relay::send_fd(nc, listener).is_ok() {
ok = true;
}
unsafe { libc::close(listener) };
}
if !ok {
child_write_all(nc, b"N");
}
unsafe { libc::close(nc) };
}
}
if let Err(msg) = child_stdio_setup(&opts.stdio) {
child_fail(err_w, 124, &format!("stdio: {msg}"));
}
if !opts.cwd.as_os_str().is_empty() {
if let Err(e) = std::env::set_current_dir(&opts.cwd) {
child_fail(err_w, 117, &format!("chdir {}: {e}", opts.cwd.display()));
}
}
child_write_all(err_w, b"R");
close_all_except(&[0, 1, 2]);
child_exec(policy, opts)
}
fn spawn_fs_only(policy: &Policy, opts: SpawnOptions, observe: bool) -> Result<Spawned> {
let parent_pid = unsafe { libc::getpid() };
let net_off = true;
if let Err(error) = crate::multi::isolation::set_subreaper() {
tracing::warn!(
"fs-only: PR_SET_CHILD_SUBREAPER failed ({error}); setsid-detached \
grandchildren may survive teardown"
);
}
let (err_r, err_w) = pipe2_cloexec()?;
let (notif_parent, notif_child) = if observe {
let (a, b) = socketpair_cloexec()?;
(Some(a), Some(b))
} else {
(None, None)
};
let args = FsChildArgs {
parent_pid,
err_w: err_w.as_raw_fd(),
notif_child: notif_child.as_ref().map(|f| f.as_raw_fd()),
observe,
net_off,
policy,
opts: &opts,
};
let pid = unsafe { libc::fork() };
if pid < 0 {
bail!("fork: {}", std::io::Error::last_os_error());
}
if pid == 0 {
unsafe { child_fs_only(args) }
}
drop(err_w);
drop(notif_child);
let cgroup_handle =
match cgroup::setup_cgroup(policy.cgroup.as_ref(), policy.cpu_max.as_deref()) {
Ok(Some(cg)) => {
let _ = cg.add_process(pid as u32);
Some(cg)
}
_ => None,
};
match read_byte(err_r.as_raw_fd(), SETUP_TIMEOUT_MS) {
ByteRead::Byte(b'R') => {}
ByteRead::Byte(b'E') | ByteRead::Eof => {
return Err(err_from_dead_child(pid, err_r.as_raw_fd()));
}
ByteRead::Byte(other) => {
let code = reap_child(pid);
return Err(anyhow!(
"unexpected setup byte {other:#x} from sandbox child (exit {code})"
));
}
ByteRead::Timeout => {
let code = kill_and_reap(pid);
return Err(anyhow!("sandbox setup timed out (child exit {code})"));
}
}
let notif_listener = match notif_parent {
Some(np) => match read_byte(np.as_raw_fd(), SETUP_TIMEOUT_MS) {
ByteRead::Byte(b'T') => net_relay::recv_fd(np.as_raw_fd()).ok(),
_ => None,
},
None => None,
};
proctrack::arm_exit_sweep(pid, pid);
Ok(Spawned {
handle: SandboxHandle {
root_pid: pid as u32,
strategy: Some(KillStrategy::ProcessGroup {
pid,
pgid: pid,
sweep: true,
}),
_cgroup: cgroup_handle,
},
broker_ctrl_fd: None,
relay_port: None,
notif_listener,
})
}
unsafe fn child_seccomp_only(a: FsChildArgs<'_>) -> ! {
let FsChildArgs {
parent_pid,
err_w,
notif_child,
observe,
net_off: _,
policy,
opts,
} = a;
let mut keep: Vec<RawFd> = vec![0, 1, 2, err_w];
if let Some(fd) = notif_child {
keep.push(fd);
}
if let StdioMode::Pty { slave_fd } = opts.stdio {
keep.push(slave_fd);
}
if let StdioMode::Captured { stdout_w, stderr_w } = opts.stdio {
keep.push(stdout_w);
keep.push(stderr_w);
}
close_all_except(&keep);
child_pdeathsig(parent_pid);
match opts.stdio {
StdioMode::Pty { .. } => {
if unsafe { libc::setsid() } < 0 {
child_fail(
err_w,
125,
&format!("setsid: {}", std::io::Error::last_os_error()),
);
}
}
StdioMode::Captured { .. } | StdioMode::Inherit => {
if unsafe { libc::setpgid(0, 0) } < 0 {
child_fail(
err_w,
125,
&format!("setpgid: {}", std::io::Error::last_os_error()),
);
}
}
}
if let Err(e) = seccomp_netblock::install_for_profile(
seccomp_netblock::SocketPolicy::UnixOnly,
policy.seccomp_profile,
) {
child_fail(err_w, 123, &format!("seccomp filter: {e}"));
}
if observe {
if let Some(nc) = notif_child {
let mut ok = false;
if let Ok(listener) = observe_seccomp::install_tap() {
child_write_all(nc, b"T");
if net_relay::send_fd(nc, listener).is_ok() {
ok = true;
}
unsafe { libc::close(listener) };
}
if !ok {
child_write_all(nc, b"N");
}
unsafe { libc::close(nc) };
}
}
if let Err(msg) = child_stdio_setup(&opts.stdio) {
child_fail(err_w, 124, &format!("stdio: {msg}"));
}
if !opts.cwd.as_os_str().is_empty() {
if let Err(e) = std::env::set_current_dir(&opts.cwd) {
child_fail(err_w, 117, &format!("chdir {}: {e}", opts.cwd.display()));
}
}
child_write_all(err_w, b"R");
close_all_except(&[0, 1, 2]);
child_exec(policy, opts)
}
fn spawn_seccomp_only(policy: &Policy, opts: SpawnOptions, observe: bool) -> Result<Spawned> {
let parent_pid = unsafe { libc::getpid() };
if let Err(error) = crate::multi::isolation::set_subreaper() {
tracing::warn!(
"seccomp: PR_SET_CHILD_SUBREAPER failed ({error}); setsid-detached \
grandchildren may survive teardown"
);
}
let (err_r, err_w) = pipe2_cloexec()?;
let (notif_parent, notif_child) = if observe {
let (a, b) = socketpair_cloexec()?;
(Some(a), Some(b))
} else {
(None, None)
};
let args = FsChildArgs {
parent_pid,
err_w: err_w.as_raw_fd(),
notif_child: notif_child.as_ref().map(|f| f.as_raw_fd()),
observe,
net_off: true,
policy,
opts: &opts,
};
let pid = unsafe { libc::fork() };
if pid < 0 {
bail!("fork: {}", std::io::Error::last_os_error());
}
if pid == 0 {
unsafe { child_seccomp_only(args) }
}
drop(err_w);
drop(notif_child);
let cgroup_handle =
match cgroup::setup_cgroup(policy.cgroup.as_ref(), policy.cpu_max.as_deref()) {
Ok(Some(cg)) => {
let _ = cg.add_process(pid as u32);
Some(cg)
}
_ => None,
};
match read_byte(err_r.as_raw_fd(), SETUP_TIMEOUT_MS) {
ByteRead::Byte(b'R') => {}
ByteRead::Byte(b'E') | ByteRead::Eof => {
return Err(err_from_dead_child(pid, err_r.as_raw_fd()));
}
ByteRead::Byte(other) => {
let code = reap_child(pid);
return Err(anyhow!(
"unexpected setup byte {other:#x} from sandbox child (exit {code})"
));
}
ByteRead::Timeout => {
let code = kill_and_reap(pid);
return Err(anyhow!("sandbox setup timed out (child exit {code})"));
}
}
let notif_listener = match notif_parent {
Some(np) => match read_byte(np.as_raw_fd(), SETUP_TIMEOUT_MS) {
ByteRead::Byte(b'T') => net_relay::recv_fd(np.as_raw_fd()).ok(),
_ => None,
},
None => None,
};
proctrack::arm_exit_sweep(pid, pid);
Ok(Spawned {
handle: SandboxHandle {
root_pid: pid as u32,
strategy: Some(KillStrategy::ProcessGroup {
pid,
pgid: pid,
sweep: true,
}),
_cgroup: cgroup_handle,
},
broker_ctrl_fd: None,
relay_port: None,
notif_listener,
})
}