blob: 40cd5414114bb821f8c28d3158e2739e4b321cfd [file]
//! The Linux backend: namespaces, natively and unprivileged.
//!
//! The child unshares user, mount, network, uts and ipc namespaces, maps itself to the
//! caller's uid, builds a new root on a tmpfs from the plan's bind mounts, pivots into it,
//! and execs the relay. The relay (`amem relay`) then unshares a pid namespace and
//! spawns the harness, mounting `/proc` from inside it, so the harness is pid 1 of its
//! own world and sees no other process.
//!
//! No network interface exists inside except loopback. The only way out is the unix
//! socket bind-mounted from the host, which the relay forwards to. The last step before
//! the exec sets `no_new_privs` and installs a seccomp filter that refuses
//! what a network namespace does not cover: vsock and io_uring.
mod seccomp;
use crate::{Error, Mount, Plan, Resources};
use nix::mount::{mount, MsFlags};
use nix::sched::{unshare, CloneFlags};
use nix::unistd::{getgid, getuid, pivot_root, sethostname};
use std::ffi::OsString;
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::Command;
fn confine(e: impl std::fmt::Display) -> Error {
Error::Confine(e.to_string())
}
fn write_file(path: &str, text: &str) -> Result<(), Error> {
std::fs::write(path, text).map_err(|e| confine(format!("{path}: {e}")))
}
/// Become root in a fresh user namespace, mapped to the caller outside.
fn enter_user_namespace(uid: u32, gid: u32) -> Result<(), Error> {
unshare(
CloneFlags::CLONE_NEWUSER
| CloneFlags::CLONE_NEWNS
| CloneFlags::CLONE_NEWNET
| CloneFlags::CLONE_NEWUTS
| CloneFlags::CLONE_NEWIPC,
)
.map_err(|e| confine(format!("unshare: {e}")))?;
write_file("/proc/self/setgroups", "deny")?;
write_file("/proc/self/uid_map", &format!("{uid} {uid} 1\n"))?;
write_file("/proc/self/gid_map", &format!("{gid} {gid} 1\n"))?;
Ok(())
}
/// Why this host will not let an unprivileged process confine anything, in a sentence
/// that names the fix.
fn restricted_why() -> String {
let what = "this host will not let an unprivileged process enter a user namespace";
let apparmor =
std::fs::read_to_string("/proc/sys/kernel/apparmor_restrict_unprivileged_userns")
.is_ok_and(|v| v.trim() == "1");
if apparmor {
format!(
"{what}: kernel.apparmor_restrict_unprivileged_userns is 1, so AppArmor moves a \
binary that has no profile of its own into one without CAP_SYS_ADMIN. Give amem a \
profile that allows it: packaging/apparmor/amem"
)
} else {
format!("{what}; --unconfined runs the harness without a sandbox")
}
}
/// Why the sandbox's seccomp filter cannot be installed. A container runtime's profile
/// can refuse `seccomp(2)` itself.
const NO_SECCOMP: &str = "the sandbox cannot install its seccomp filter here: it needs a \
kernel with seccomp filters, and a seccomp profile that allows \
seccomp(2)";
/// Why a read-only grant cannot be made read-only below its submounts. A kernel before
/// Linux 5.12 has no `mount_setattr`, and a container runtime's seccomp profile that does
/// not allow the call answers it with `ENOSYS` or `EPERM`.
const NO_MOUNT_SETATTR: &str = "mount_setattr is not available here: it needs Linux 5.12 or \
later, and a seccomp profile that allows it";
/// Whether `mount_setattr` answers here, asked with an empty path and a size below the
/// first `mount_attr`: a kernel that has the call refuses that size with `EINVAL` before
/// it checks anything else, so any other answer is a filter in the way.
fn has_mount_setattr() -> bool {
// SAFETY: an empty NUL-terminated path, no mount_attr, and a size of zero; the kernel
// refuses the call before it reads either pointer.
let r = unsafe {
libc::syscall(
libc::SYS_mount_setattr,
-1,
c"".as_ptr(),
0,
std::ptr::null::<libc::mount_attr>(),
0,
)
};
r == 0 || std::io::Error::last_os_error().raw_os_error() == Some(libc::EINVAL)
}
/// Whether this host lets an unprivileged process build the sandbox at all: can it
/// create a user namespace and map itself into it, make a read-only grant read-only
/// below its submounts, and install the seccomp filter?
///
/// Probed in a child, because the answer is a namespace the asking process would
/// otherwise be stuck in, and by calling [`enter_user_namespace`] itself rather than a
/// copy of it: a probe that unshares a different set of namespaces, or writes the maps
/// in a different order, can disagree with the launch it is meant to predict, and a
/// check that says a working host is broken is worse than no check. The first cut of
/// this did exactly that.
///
/// A sentence rather than a flag, since what to do differs: a kernel without user
/// namespaces is one thing, Ubuntu's AppArmor moving an unprofiled binary into a profile
/// without CAP_SYS_ADMIN is another, and the second is fixed by installing a profile
/// rather than by giving up on confinement.
///
/// The child also asks for `mount_setattr`, without which every launch fails in
/// [`read_only_below`].
///
/// The child also installs the launch's seccomp filter, compiled before the fork as a
/// launch compiles it.
///
/// # Errors
///
/// Why not, in a sentence `amem doctor` and the launch line can carry.
pub fn availability() -> Result<(), String> {
let filter = seccomp::program().map_err(|e| e.to_string())?;
// SAFETY: fork in a process that only waits for the child; the child makes the same
// syscalls a launch does and _exits without running atexit handlers, which belong to
// the parent.
let pid = unsafe { libc::fork() };
if pid < 0 {
return Err(format!("fork: {}", std::io::Error::last_os_error()));
}
if pid == 0 {
let code = if enter_user_namespace(getuid().as_raw(), getgid().as_raw()).is_err() {
1
} else if !has_mount_setattr() {
2
} else if seccomp::install(&filter).is_err() {
3
} else {
0
};
// SAFETY: exiting the probe child without running the parent's atexit handlers.
unsafe { libc::_exit(code) };
}
let mut status: libc::c_int = 0;
// SAFETY: waitpid on our own child with a valid status pointer.
if unsafe { libc::waitpid(pid, &mut status, 0) } < 0 {
return Err(format!("waitpid: {}", std::io::Error::last_os_error()));
}
if !libc::WIFEXITED(status) {
return Err("the user-namespace probe did not exit normally".to_string());
}
match libc::WEXITSTATUS(status) {
0 => Ok(()),
2 => Err(NO_MOUNT_SETATTR.to_string()),
3 => Err(NO_SECCOMP.to_string()),
_ => Err(restricted_why()),
}
}
fn bind(m: &Mount, root: &Path) -> Result<(), Error> {
let target = root.join(m.target.strip_prefix("/").unwrap_or(&m.target));
if m.source.is_dir() {
std::fs::create_dir_all(&target)
.map_err(|e| confine(format!("{}: {e}", target.display())))?;
} else {
if let Some(p) = target.parent() {
std::fs::create_dir_all(p).map_err(|e| confine(format!("{}: {e}", p.display())))?;
}
if !target.exists() {
std::fs::write(&target, b"")
.map_err(|e| confine(format!("{}: {e}", target.display())))?;
}
}
mount(
Some(&m.source),
&target,
None::<&str>,
MsFlags::MS_BIND | MsFlags::MS_REC,
None::<&str>,
)
.map_err(|e| {
confine(format!(
"bind {} -> {}: {e}",
m.source.display(),
target.display()
))
})?;
if m.read_only {
mount(
None::<&str>,
&target,
None::<&str>,
MsFlags::MS_BIND
| MsFlags::MS_REMOUNT
| MsFlags::MS_RDONLY
| MsFlags::MS_NOSUID
| kept_flags(&target),
None::<&str>,
)
.map_err(|e| confine(format!("remount ro {}: {e}", target.display())))?;
read_only_below(&target)?;
}
Ok(())
}
/// Make every mount under `target` read-only and nosuid as well. A bind with `MS_REC`
/// copies the source's submounts, `/usr/lib/wsl/lib` under `/usr` on WSL2 say, and the
/// remount reaches the top mount only. `mount_setattr(2)` with `AT_RECURSIVE` sets the
/// flags on the whole tree at once, a submount another mount hides included.
///
/// Where `mount_setattr` is not available, [`NO_MOUNT_SETATTR`], the launch fails rather
/// than leave a submount writable under a read-only grant.
fn read_only_below(target: &Path) -> Result<(), Error> {
use std::os::unix::ffi::OsStrExt;
let path = std::ffi::CString::new(target.as_os_str().as_bytes())
.map_err(|e| confine(format!("{}: {e}", target.display())))?;
let attr = libc::mount_attr {
attr_set: libc::MOUNT_ATTR_RDONLY | libc::MOUNT_ATTR_NOSUID,
attr_clr: 0,
propagation: 0,
userns_fd: 0,
};
// SAFETY: a NUL-terminated path and a mount_attr passed with its own size; the kernel
// only reads both.
let r = unsafe {
libc::syscall(
libc::SYS_mount_setattr,
libc::AT_FDCWD,
path.as_ptr(),
libc::AT_RECURSIVE,
&attr,
std::mem::size_of::<libc::mount_attr>(),
)
};
if r == 0 {
return Ok(());
}
let e = std::io::Error::last_os_error();
if e.raw_os_error() == Some(libc::ENOSYS) {
return Err(confine(format!(
"cannot make the mounts under {} read-only: {NO_MOUNT_SETATTR}",
target.display()
)));
}
Err(confine(format!(
"mount_setattr ro {}: {e}",
target.display()
)))
}
/// The restricting flags a mount already carries, to carry into its read-only remount.
/// A mount made outside the user namespace, a FUSE mount the launcher serves say, has
/// its `nodev`, `noatime` and the like locked by the kernel: a remount that leaves one
/// out asks to lift it and is refused, so they are read back and kept.
fn kept_flags(target: &Path) -> MsFlags {
use nix::sys::statvfs::{statvfs, FsFlags};
let Ok(st) = statvfs(target) else {
return MsFlags::empty();
};
let have = st.flags();
let pairs = [
(FsFlags::ST_NOSUID, MsFlags::MS_NOSUID),
(FsFlags::ST_NODEV, MsFlags::MS_NODEV),
(FsFlags::ST_NOEXEC, MsFlags::MS_NOEXEC),
(FsFlags::ST_NOATIME, MsFlags::MS_NOATIME),
(FsFlags::ST_NODIRATIME, MsFlags::MS_NODIRATIME),
(FsFlags::ST_RELATIME, MsFlags::MS_RELATIME),
];
pairs
.into_iter()
.filter(|(st, _)| have.contains(*st))
.fold(MsFlags::empty(), |acc, (_, ms)| acc | ms)
}
fn tmpfs(path: &Path, opts: &str) -> Result<(), Error> {
std::fs::create_dir_all(path).map_err(|e| confine(format!("{}: {e}", path.display())))?;
mount(
Some("tmpfs"),
path,
Some("tmpfs"),
MsFlags::MS_NOSUID | MsFlags::MS_NODEV,
Some(opts),
)
.map_err(|e| confine(format!("tmpfs {}: {e}", path.display())))
}
fn dev_nodes(root: &Path) -> Result<(), Error> {
let dev = root.join("dev");
tmpfs(&dev, "mode=755,size=1m")?;
for n in ["null", "zero", "full", "random", "urandom", "tty"] {
let src = PathBuf::from("/dev").join(n);
if src.exists() {
bind(
&Mount {
source: src,
target: PathBuf::from("/dev").join(n),
read_only: false,
},
root,
)?;
}
}
// pseudo-terminals, so an interactive harness can open the one it was given
let pts = PathBuf::from("/dev/pts");
if pts.exists() {
bind(
&Mount {
source: pts,
target: PathBuf::from("/dev/pts"),
read_only: false,
},
root,
)?;
std::os::unix::fs::symlink("pts/ptmx", dev.join("ptmx")).ok();
}
tmpfs(&dev.join("shm"), "mode=1777,size=64m")?;
for (link, to) in [
("fd", "/proc/self/fd"),
("stdin", "/proc/self/fd/0"),
("stdout", "/proc/self/fd/1"),
("stderr", "/proc/self/fd/2"),
] {
std::os::unix::fs::symlink(to, dev.join(link)).ok();
}
Ok(())
}
/// Bring the loopback interface up. A fresh network namespace has `lo` down, and the
/// relay listens on it.
fn loopback_up() -> Result<(), Error> {
// SAFETY: a datagram socket and two ioctls on a zeroed, correctly sized ifreq whose
// name is "lo"; the socket is closed before returning.
unsafe {
let fd = libc::socket(libc::AF_INET, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0);
if fd < 0 {
return Err(confine(format!(
"socket: {}",
std::io::Error::last_os_error()
)));
}
let mut req: libc::ifreq = std::mem::zeroed();
for (i, b) in b"lo".iter().enumerate() {
req.ifr_name[i] = *b as libc::c_char;
}
let r = libc::ioctl(fd, libc::SIOCGIFFLAGS, &mut req);
if r < 0 {
libc::close(fd);
return Err(confine(format!(
"SIOCGIFFLAGS lo: {}",
std::io::Error::last_os_error()
)));
}
req.ifr_ifru.ifru_flags |= (libc::IFF_UP | libc::IFF_RUNNING) as libc::c_short;
let r = libc::ioctl(fd, libc::SIOCSIFFLAGS, &req);
libc::close(fd);
if r < 0 {
return Err(confine(format!(
"SIOCSIFFLAGS lo: {}",
std::io::Error::last_os_error()
)));
}
}
Ok(())
}
/// Build the root and pivot into it. Runs in the child, after `enter_user_namespace`.
fn build_root(plan: &Plan, root: &Path) -> Result<(), Error> {
// make every mount private so nothing propagates back to the host
mount(
None::<&str>,
"/",
None::<&str>,
MsFlags::MS_REC | MsFlags::MS_PRIVATE,
None::<&str>,
)
.map_err(|e| confine(format!("make / private: {e}")))?;
tmpfs(root, "mode=755")?;
// /tmp and /dev first, so a plan mount under them (a working tree in /tmp, say) lands
// on top rather than being hidden by them
tmpfs(&root.join("tmp"), "mode=1777")?;
dev_nodes(root)?;
for m in &plan.mounts {
bind(m, root)?;
}
// merged-usr: /bin -> usr/bin and so on, recreated so `#!/bin/sh` resolves inside;
// the plan took them from the rootfs it runs in
for (name, target) in &plan.links {
let inside = root.join(name);
if !inside.exists() {
std::os::unix::fs::symlink(target, &inside)
.map_err(|e| confine(format!("symlink {}: {e}", inside.display())))?;
}
}
std::fs::create_dir_all(root.join("proc")).map_err(confine)?;
// The procfs for the harness's pid namespace, mounted here rather than over /proc
// after the pivot. `mount -t proc` from a non-initial user namespace is refused
// unless a fully visible procfs already exists in this mount namespace:
// fs/proc/root.c, mount_too_revealing(). The host's is still visible at this point
// and satisfies that; after pivot_root detaches the old root, nothing does, and the
// mount fails with EPERM on kernels that enforce the check (seen on 6.17).
//
// Binding the host's procfs instead is not an option: the kernel refuses to clone
// it into the new namespace at all, recursively or not, with EINVAL.
//
// This is why the pid namespace is entered before the root is built: the mount
// shows the pid namespace it was made in, so it has to come after the fork.
mount(
Some("proc"),
&root.join("proc"),
Some("proc"),
MsFlags::MS_NOSUID | MsFlags::MS_NODEV | MsFlags::MS_NOEXEC,
None::<&str>,
)
.map_err(|e| confine(format!("mount proc: {e}")))?;
std::fs::create_dir_all(root.join("run")).map_err(confine)?;
let old = root.join(".old-root");
std::fs::create_dir_all(&old).map_err(confine)?;
pivot_root(root, &old).map_err(|e| confine(format!("pivot_root: {e}")))?;
std::env::set_current_dir("/").map_err(confine)?;
nix::mount::umount2("/.old-root", nix::mount::MntFlags::MNT_DETACH)
.map_err(|e| confine(format!("umount old root: {e}")))?;
std::fs::remove_dir("/.old-root").ok();
std::env::set_current_dir(&plan.cwd)
.map_err(|e| confine(format!("chdir {}: {e}", plan.cwd.display())))?;
Ok(())
}
/// Run the plan: fork, confine the child, exec the relay. Returns the exit code and what
/// the confined tree cost the host.
///
/// # Errors
///
/// When the sandbox cannot be built or the relay cannot be started.
pub fn run(plan: &Plan) -> Result<(i32, Resources), Error> {
let (code, _, r) = measured(plan, false)?;
Ok((code, r))
}
/// As [`run`], with the harness's standard output captured; standard error is inherited.
///
/// # Errors
///
/// When the sandbox cannot be built or the relay cannot be started.
pub fn run_captured(plan: &Plan) -> Result<(i32, Vec<u8>, Resources), Error> {
measured(plan, true)
}
fn measured(plan: &Plan, capture: bool) -> Result<(i32, Vec<u8>, Resources), Error> {
let scratch = crate::scratch::Scratch::make(&std::env::temp_dir())?;
let (mut cmd, _) = command(plan)?;
let ran = crate::measure::run(&mut cmd, capture)
.map_err(|e| confine(format!("spawn {}: {e}", plan.argv[0])));
drop(scratch);
ran
}
/// The command that builds the sandbox in `pre_exec` and execs the relay, and the
/// scratch directory it pivots into, which the caller holds as a [`crate::scratch::Scratch`].
fn command(plan: &Plan) -> Result<(Command, PathBuf), Error> {
let uid = getuid().as_raw();
let gid = getgid().as_raw();
// made and locked by the caller's Scratch; not made again here, where it would
// not be locked
let root = crate::scratch::dir(&std::env::temp_dir());
let launcher = nix::unistd::getpid();
let plan2 = plan.clone();
let root2 = root.clone();
// compiled here, since compiling allocates and installing it does not
let filter = seccomp::program()?;
let open_files = open_files_limit();
let mut cmd = Command::new(&plan.argv[0]);
cmd.args(&plan.argv[1..]);
cmd.env_clear();
for (k, v) in &plan.env {
cmd.env(k, v);
}
cmd.current_dir(&plan.cwd);
// SAFETY: everything in pre_exec is syscalls and small allocations in a fresh
// single-threaded child; nothing touches the parent's state.
unsafe {
cmd.pre_exec(move || {
let go = || -> Result<(), Error> {
enter_user_namespace(uid, gid)?;
sethostname(&plan2.hostname).map_err(|e| confine(format!("hostname: {e}")))?;
loopback_up()?;
// the run ends with the launcher, however the launcher ends
die_with_parent(launcher)?;
// the intermediate, as pid 1 can still tell it after the fork: its
// parent is outside its namespace, and getppid reads 0 there
let intermediate = pidfd_of_self();
// before build_root: it mounts this namespace's procfs, which must be
// made after the fork that enters the namespace and before pivot_root
// takes the host's procfs out of view
become_pid_one()?;
// pid 1: the namespace, and the harness in it, ends with the intermediate
die_with_intermediate(intermediate)?;
build_root(&plan2, &root2)?;
Ok(())
};
go().map_err(|e| {
// std carries only the raw errno back to the parent, so say what
// failed here, where stderr is still the terminal
eprintln!("amem: sandbox: {e}");
std::io::Error::other(e.to_string())
})?;
cloexec_from_three(open_files)
.inspect_err(|_| say(b"cannot mark inherited descriptors close-on-exec"))?;
// last, so the setup above may make calls the filter refuses
seccomp::install(&filter).inspect_err(|_| say(NO_SECCOMP.as_bytes()))
});
}
Ok((cmd, root))
}
/// Say what failed between fork and exec, on stderr, without allocating.
fn say(what: &[u8]) {
for part in [b"amem: sandbox: ".as_slice(), what, b"\n"] {
// SAFETY: a write of a buffer with its own length to stderr.
unsafe { libc::write(2, part.as_ptr().cast(), part.len()) };
}
}
/// The soft limit on open files, the highest descriptor number this process can hold.
fn open_files_limit() -> libc::c_int {
let mut limit = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
// SAFETY: getrlimit with a valid pointer to write into.
if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) } != 0 {
return 1024;
}
libc::c_int::try_from(limit.rlim_cur).unwrap_or(libc::c_int::MAX)
}
/// Mark every descriptor from 3 up close-on-exec, so the harness inherits only its
/// standard streams. A socket the launcher left open would otherwise work inside: a
/// vsock socket made outside the filter reaches the host. The launcher passes nothing
/// into the sandbox as a descriptor; the egress socket is bind-mounted as a path.
///
/// `close_range` with `CLOSE_RANGE_CLOEXEC` (Linux 5.11) does it in one call. Before
/// that, `fcntl` sets `FD_CLOEXEC` on each number below `limit`, the open-files limit
/// read before the fork. Allocates nothing.
fn cloexec_from_three(limit: libc::c_int) -> std::io::Result<()> {
// SAFETY: close_range with integer arguments only.
let r = unsafe {
libc::syscall(
libc::SYS_close_range,
3,
libc::c_uint::MAX,
libc::CLOSE_RANGE_CLOEXEC,
)
};
if r == 0 {
return Ok(());
}
let e = std::io::Error::last_os_error();
// ENOSYS before Linux 5.9, EINVAL for the flag before 5.11
if !matches!(e.raw_os_error(), Some(libc::ENOSYS | libc::EINVAL)) {
return Err(e);
}
for fd in 3..limit {
// SAFETY: fcntl on a descriptor number; one that is not open fails with EBADF
// and has nothing to mark.
unsafe { libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC) };
}
Ok(())
}
/// Enter a pid namespace and become its pid 1. Runs between fork and exec, while the
/// process still holds the user namespace's capabilities (exec of a non-root uid drops
/// them, which is why the relay cannot do this itself).
///
/// `unshare(CLONE_NEWPID)` only moves the caller's *children* into the new namespace, so
/// this forks once more: the parent waits for the child and exits with its status; the
/// child, now pid 1, goes on to build the root and exec the relay.
///
/// Called before [`build_root`], which mounts the procfs for this namespace while the
/// host's is still visible. A procfs mount shows the pid namespace it was made in, so it
/// has to happen after this fork, and the visibility the kernel demands of it is gone
/// once `pivot_root` has detached the old root -- which leaves only the window between
/// the two.
fn become_pid_one() -> Result<(), Error> {
unshare(CloneFlags::CLONE_NEWPID).map_err(|e| confine(format!("unshare pid: {e}")))?;
// SAFETY: fork between fork and exec in an already single-threaded child; the parent
// branch only closes descriptors, waits and exits, the child branch only mounts and
// returns to exec.
let pid = unsafe { libc::fork() };
if pid < 0 {
return Err(confine(format!(
"fork: {}",
std::io::Error::last_os_error()
)));
}
if pid > 0 {
// The intermediate never execs, so its copies of the launcher's descriptors
// stay open until it exits: among them std's exec-error channel, whose end of
// file is what lets `spawn` return. The child, pid 1, keeps its own copies and
// reports through that channel as before.
close_from_three();
let mut status: libc::c_int = 0;
// SAFETY: waitpid on our own child with a valid status pointer.
let r = unsafe { libc::waitpid(pid, &mut status, 0) };
// the code as crate::measure::exit_code gives it, said without allocating
let code = if r < 0 {
1
} else if libc::WIFSIGNALED(status) {
let signal = libc::WTERMSIG(status);
if crate::measure::is_said(signal) {
say_the_relay_died(signal, libc::WCOREDUMP(status));
}
128 + signal
} else if libc::WIFEXITED(status) {
libc::WEXITSTATUS(status)
} else {
1
};
// SAFETY: exiting the intermediate process without running atexit handlers,
// which belong to the parent that forked us.
unsafe { libc::_exit(code) };
}
Ok(())
}
/// Say on standard error that the relay died of `signal`, as [`crate::measure::exit_code`]
/// says it, in one `write` from a buffer on the stack. This runs after a fork from a
/// launcher with other threads, where allocating or taking stderr's lock can hang on a
/// lock one of them held at the fork.
fn say_the_relay_died(signal: i32, core_dumped: bool) {
fn put(buf: &mut [u8], at: usize, bytes: &[u8]) -> usize {
let end = (at + bytes.len()).min(buf.len());
buf[at..end].copy_from_slice(&bytes[..end - at]);
end
}
let mut digits = [0u8; 10];
let mut first = digits.len();
let mut rest = signal.unsigned_abs();
loop {
first -= 1;
digits[first] = b'0' + u8::try_from(rest % 10).unwrap_or(0);
rest /= 10;
if rest == 0 || first == 0 {
break;
}
}
let number = &digits[first..];
let mut buf = [0u8; 96];
let mut n = put(&mut buf, 0, b"amem: the relay was killed by ");
if let Some(name) = crate::measure::signal_name(signal) {
n = put(&mut buf, n, name.as_bytes());
n = put(&mut buf, n, b" (signal ");
n = put(&mut buf, n, number);
n = put(&mut buf, n, b")");
} else {
n = put(&mut buf, n, b"signal ");
n = put(&mut buf, n, number);
}
if core_dumped {
n = put(&mut buf, n, b", core dumped");
}
n = put(&mut buf, n, b"\n");
// SAFETY: write to fd 2 from a live buffer of at least n bytes; a failure is ignored,
// as there is nowhere else to say it.
unsafe { libc::write(2, buf.as_ptr().cast(), n) };
}
/// Close every descriptor from 3 up. Runs between fork and exec, so it allocates nothing:
/// `close_range` (Linux 5.9) in one call, or else `close` on each number below the
/// open-files limit.
fn close_from_three() {
// SAFETY: close_range with integer arguments only.
let r = unsafe { libc::syscall(libc::SYS_close_range, 3, libc::c_uint::MAX, 0) };
if r == 0 {
return;
}
for fd in 3..open_files_limit() {
// SAFETY: close of a descriptor number; one that is not open fails with EBADF.
unsafe { libc::close(fd) };
}
}
/// Have the kernel SIGKILL this process when the thread that forked it ends, and fail if
/// `parent` is no longer its parent: gone before the request took hold. Runs between fork
/// and exec; the setting outlives the exec. Allocates only to say why it failed.
fn die_with_parent(parent: nix::unistd::Pid) -> Result<(), Error> {
nix::sys::prctl::set_pdeathsig(nix::sys::signal::Signal::SIGKILL)
.map_err(|e| confine(format!("parent-death signal: {e}")))?;
if nix::unistd::getppid() == parent {
Ok(())
} else {
Err(confine("the launcher has gone"))
}
}
/// A pidfd for this process, close-on-exec, or -1 where the kernel has none (before 5.3).
fn pidfd_of_self() -> libc::c_int {
// SAFETY: pidfd_open with this process's pid and no flags; it returns a new
// descriptor or -1.
let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, libc::getpid(), 0) };
libc::c_int::try_from(fd).unwrap_or(-1)
}
/// As [`die_with_parent`] for pid 1, whose parent is the intermediate behind `pidfd`:
/// fail if the intermediate has already exited, which makes the pidfd readable. Without
/// a pidfd the check is not made, and an intermediate that dies between its fork and
/// the request leaves pid 1 to run until the harness ends.
fn die_with_intermediate(pidfd: libc::c_int) -> Result<(), Error> {
nix::sys::prctl::set_pdeathsig(nix::sys::signal::Signal::SIGKILL)
.map_err(|e| confine(format!("parent-death signal: {e}")))?;
if pidfd < 0 {
return Ok(());
}
let mut poll = libc::pollfd {
fd: pidfd,
events: libc::POLLIN,
revents: 0,
};
// SAFETY: poll on one valid pollfd, without waiting; then close of the pidfd this
// process owns.
let gone = unsafe {
let ready = libc::poll(&raw mut poll, 1, 0);
libc::close(pidfd);
ready > 0
};
if gone {
Err(confine("the intermediate has gone"))
} else {
Ok(())
}
}
/// What `amem relay` does inside: spawn the harness and wait for it. The relay is
/// already pid 1 of the harness's namespace, courtesy of [`run`].
///
/// # Errors
///
/// When the spawn fails.
pub fn spawn_in_pid_namespace(argv: &[OsString]) -> Result<i32, Error> {
let status = Command::new(&argv[0])
.args(&argv[1..])
.status()
.map_err(|e| confine(format!("spawn {}: {e}", argv[0].to_string_lossy())))?;
Ok(crate::measure::exit_code(status, &argv[0]))
}