Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion crates/sandlock-core/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ fn main() {
// restore-stub: a core component of the restore engine (the supervisor execs
// it to reconstruct a checkpoint), freestanding, no libc, no PIE. It lives
// next to the checkpoint code that owns it; its binary is built into OUT_DIR
// and its path is handed to the crate via the RESTORE_STUB_PATH env var.
// and its path is handed to the crate via the RESTORE_STUB_PATH env var,
// which embeds it: an installed sandlock has no build tree to exec from.
//
// The fixed load address must match `checkpoint::restore_blob::STUB_BASE`:
// the stub reconstructs the checkpoint's layout around itself, so its own
Expand Down Expand Up @@ -99,9 +100,54 @@ fn main() {
}
println!("cargo:warning={fail_msg}");
}
// The crate embeds the stub with include_bytes!, so an arch without restore
// still needs a file there; the runtime treats an empty stub as unavailable.
if !stub_bin.exists() {
std::fs::write(&stub_bin, b"").unwrap();
}
// Emit the path every run (rustc-env is not cached across build-script runs),
// whether or not the binary was just (re)built.
println!("cargo:rustc-env=RESTORE_STUB_PATH={}", stub_bin.display());

// shebang-trampoline: runs a chroot script's #! interpreter (see the
// source). Freestanding, so any compiler for the target arch will do.
let tramp_src = manifest_dir.join("src/chroot/shebang-trampoline.c");
let tramp_bin = out_dir.join("shebang-trampoline");
let arch = target.split('-').next().unwrap_or_default();
let arch = if is_riscv64 { "riscv64" } else { arch };
let mut tramp_ccs = vec![format!("{arch}-linux-gnu-gcc"), format!("{arch}-unknown-linux-gnu-gcc")];
if host.starts_with(arch) {
tramp_ccs.insert(0, "cc".to_string());
}
let tramp_ccs: Vec<&str> = tramp_ccs.iter().map(String::as_str).collect();
if !build_static(
&tramp_src,
&tramp_bin,
&tramp_ccs,
&[
"-static",
"-nostdlib",
"-no-pie",
"-O2",
"-ffreestanding",
"-fno-tree-loop-distribute-patterns",
"-fno-stack-protector",
],
) {
let msg = format!(
"failed to compile shebang-trampoline for {arch}: no working C compiler \
(tried {}); #! scripts cannot run under chroot",
tramp_ccs.join(", "),
);
if matches!(arch, "x86_64" | "aarch64" | "riscv64") {
panic!("{msg}");
}
println!("cargo:warning={msg}");
}
if !tramp_bin.exists() {
std::fs::write(&tramp_bin, b"").unwrap();
}
println!("cargo:rustc-env=SHEBANG_TRAMPOLINE_PATH={}", tramp_bin.display());
}

/// Compile `src` to `bin` with the first working compiler in `ccs`, skipping the
Expand Down
1 change: 1 addition & 0 deletions crates/sandlock-core/src/checkpoint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub(crate) mod resume;
pub(crate) const CTRL_FD: i32 = 3; // control-blob memfd
pub(crate) const READY_FD: i32 = 4; // eventfd: stub -> supervisor ("layout done")
pub(crate) const GO_FD: i32 = 5; // eventfd: supervisor -> stub ("pages written")
pub(crate) const STUB_FD: i32 = 6; // sealed memfd holding the stub image itself

pub(crate) use capture::capture;

Expand Down
4 changes: 3 additions & 1 deletion crates/sandlock-core/src/checkpoint/restore-stub.c
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
#define CTRL_FD 3
#define READY_FD 4
#define GO_FD 5
#define STUB_FD 6

/* The window this stub is linked into, mirroring restore_blob::STUB_BASE and
* STUB_SPAN and the -Wl,-Ttext-segment= flag in build.rs. Used only to refuse a
Expand Down Expand Up @@ -441,10 +442,11 @@ static void _start_c(u64 *sp) {
}

/* 8. Reopen the fd table. The control fds go first: a restored fd number
* may well be 3, 4 or 5, and nothing needs them from here on. */
* may well be 3 to 6, and nothing needs them from here on. */
SC1(SYS_close, CTRL_FD);
SC1(SYS_close, READY_FD);
SC1(SYS_close, GO_FD);
SC1(SYS_close, STUB_FD);
for (i = 0; i < h->n_fds; i++) {
struct blob_fd *f = &fds[i];
i64 fd = SC4(SYS_openat, AT_FDCWD, strings + f->path_off, f->flags, 0);
Expand Down
120 changes: 77 additions & 43 deletions crates/sandlock-core/src/checkpoint/resume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@

use std::io;
use std::os::fd::{FromRawFd, OwnedFd, RawFd};
use std::path::PathBuf;

use crate::checkpoint::{CTRL_FD, GO_FD, READY_FD};
use crate::checkpoint::{CTRL_FD, GO_FD, READY_FD, STUB_FD};
use crate::error::{SandboxRuntimeError, SandlockError};

/// How long to wait for the stub to finish laying out the address space.
Expand All @@ -36,14 +35,15 @@ fn child_err(msg: String) -> SandlockError {
SandlockError::Runtime(SandboxRuntimeError::Child(msg))
}

/// Path to the freestanding restore-stub binary, compiled by `build.rs`.
pub(crate) fn stub_path() -> PathBuf {
PathBuf::from(env!("RESTORE_STUB_PATH"))
}
/// The freestanding restore-stub, compiled by `build.rs`. Embedded rather than
/// exec'd from its build path, which does not exist once sandlock is installed.
/// Empty when this arch has no restore engine or no C compiler was found.
pub(crate) const STUB_ELF: &[u8] = include_bytes!(env!("RESTORE_STUB_PATH"));

/// The fds the stub inherits, held open in the supervisor for the handshake.
/// Their numbers in the child are fixed by the [`CTRL_FD`]/[`READY_FD`]/
/// [`GO_FD`] convention; `Sandbox`'s `extra_fds` mechanism does the `dup2`.
/// [`GO_FD`]/[`STUB_FD`] convention; `Sandbox`'s `extra_fds` mechanism does the
/// `dup2`.
///
/// READY is an eventfd the stub signals once the address space is laid out. GO
/// is a pipe rather than an eventfd because it carries data back: the count of
Expand All @@ -57,21 +57,24 @@ pub(crate) struct StubChannel {
go_r: OwnedFd,
/// Write end, kept here.
go_w: OwnedFd,
stub: OwnedFd,
}

impl StubChannel {
/// Build the control-blob memfd, the READY eventfd and the GO pipe, each
/// relocated clear of the fixed child-side numbers (see [`relocate_above`]).
/// Build the control-blob memfd, the READY eventfd, the GO pipe and the
/// stub memfd, each relocated clear of the fixed child-side numbers (see
/// [`relocate_above`]).
pub(crate) fn new(blob: &[u8]) -> io::Result<Self> {
let ctrl = relocate_above(memfd_with(blob)?, GO_FD + 1)?;
let ready = relocate_above(eventfd()?, GO_FD + 1)?;
let ctrl = relocate_above(memfd_with(blob)?, STUB_FD + 1)?;
let ready = relocate_above(eventfd()?, STUB_FD + 1)?;
let mut pipefd = [0i32; 2];
if unsafe { libc::pipe2(pipefd.as_mut_ptr(), libc::O_CLOEXEC) } != 0 {
return Err(io::Error::last_os_error());
}
let go_r = relocate_above(pipefd[0], GO_FD + 1)?;
let go_w = relocate_above(pipefd[1], GO_FD + 1)?;
Ok(StubChannel { ctrl, ready, go_r, go_w })
let go_r = relocate_above(pipefd[0], STUB_FD + 1)?;
let go_w = relocate_above(pipefd[1], STUB_FD + 1)?;
let stub = relocate_above(stub_memfd()?, STUB_FD + 1)?;
Ok(StubChannel { ctrl, ready, go_r, go_w, stub })
}

/// The `(child fd, supervisor fd)` pairs for `Sandbox`'s `extra_fds`.
Expand All @@ -81,6 +84,7 @@ impl StubChannel {
(CTRL_FD, self.ctrl.as_raw_fd()),
(READY_FD, self.ready.as_raw_fd()),
(GO_FD, self.go_r.as_raw_fd()),
(STUB_FD, self.stub.as_raw_fd()),
]
}
}
Expand Down Expand Up @@ -120,6 +124,13 @@ fn memfd_with(bytes: &[u8]) -> io::Result<RawFd> {
Ok(fd)
}

/// A sealed, executable memfd holding [`STUB_ELF`].
fn stub_memfd() -> io::Result<RawFd> {
use std::os::fd::IntoRawFd;

crate::sys::syscall::sealed_exec_memfd("sandlock-restore-stub", STUB_ELF).map(IntoRawFd::into_raw_fd)
}

/// Move `fd` to a number at or above `floor` and take ownership of it.
///
/// Two hazards make this mandatory rather than tidy. `dup2` returns success
Expand Down Expand Up @@ -297,21 +308,31 @@ mod tests {

#[test]
fn channel_fds_land_above_the_fixed_child_numbers() {
// A control fd allocated at 3/4/5 would be dup2'd onto itself in the
// A control fd allocated at 3 to 6 would be dup2'd onto itself in the
// child, which leaves FD_CLOEXEC set and closes it at execve.
let ch = StubChannel::new(b"blob").expect("channel");
for fd in [ch.ctrl.as_raw_fd(), ch.ready.as_raw_fd(),
ch.go_r.as_raw_fd(), ch.go_w.as_raw_fd()] {
assert!(fd > GO_FD, "fd {fd} must sit above the fixed control fds");
ch.go_r.as_raw_fd(), ch.go_w.as_raw_fd(), ch.stub.as_raw_fd()] {
assert!(fd > STUB_FD, "fd {fd} must sit above the fixed control fds");
}
let mut pairs = ch.extra_fds();
pairs.sort();
assert_eq!(
pairs.iter().map(|&(child, _)| child).collect::<Vec<_>>(),
vec![CTRL_FD, READY_FD, GO_FD],
vec![CTRL_FD, READY_FD, GO_FD, STUB_FD],
);
}

#[test]
fn stub_memfd_is_sealed_and_holds_the_stub() {
let ch = StubChannel::new(b"blob").expect("channel");
let fd = ch.stub.as_raw_fd();
let seals = unsafe { libc::fcntl(fd, libc::F_GET_SEALS) };
assert!(seals & libc::F_SEAL_WRITE != 0, "stub memfd must be write-sealed");
let got = std::fs::read(format!("/proc/self/fd/{fd}")).expect("read stub memfd");
assert_eq!(got, STUB_ELF);
}

#[test]
fn control_blob_memfd_reads_back_from_offset_zero() {
let ch = StubChannel::new(b"hello blob").expect("channel");
Expand Down Expand Up @@ -398,10 +419,11 @@ mod tests {
fn stub_links_at_the_reserved_base() {
use crate::checkpoint::restore_blob::{STUB_BASE, STUB_SPAN};

let Ok(elf) = std::fs::read(stub_path()) else {
let elf = STUB_ELF;
if elf.is_empty() {
eprintln!("skip: restore-stub not built");
return;
};
}
// ELF64 program headers: e_phoff@32, e_phentsize@54, e_phnum@56.
// Each PT_LOAD entry: p_type@0, p_vaddr@16, p_memsz@40.
let phoff = u64::from_le_bytes(elf[32..40].try_into().unwrap()) as usize;
Expand Down Expand Up @@ -435,10 +457,11 @@ mod tests {
#[test]
#[cfg(target_arch = "x86_64")]
fn stub_carries_no_stack_protector() {
let Ok(elf) = std::fs::read(stub_path()) else {
let elf = STUB_ELF;
if elf.is_empty() {
eprintln!("skip: restore-stub not built");
return;
};
}
const CANARY_LOAD: &[u8] = &[0x64, 0x48, 0x8b, 0x04, 0x25, 0x28, 0x00, 0x00, 0x00];
assert!(
!elf.windows(CANARY_LOAD.len()).any(|w| w == CANARY_LOAD),
Expand Down Expand Up @@ -468,9 +491,8 @@ mod tests {
const SENTINEL: u8 = 0x5A;
const PAGE: u64 = 0x1000;

let stub = stub_path();
if !stub.exists() {
eprintln!("skip: restore-stub not built ({})", stub.display());
if STUB_ELF.is_empty() {
eprintln!("skip: restore-stub not built");
return;
}

Expand Down Expand Up @@ -528,10 +550,7 @@ mod tests {
let plan = restore_blob::plan(&cp, None, &[]).expect("plan");
let channel = StubChannel::new(&plan.blob).expect("channel");

// Build the exec path before fork: CString::new allocates, and
// allocating between fork() and execve() in a multithreaded process (the
// test harness) can deadlock on the allocator lock.
let stub_path = std::ffi::CString::new(stub.to_str().unwrap()).unwrap();
let stub_name = c"sandlock-restore-stub";

// Relocate the sentinel pipe clear of every fixed number the child
// installs, OUT_FD included. Left at 3/4 (which is exactly where the
Expand All @@ -543,8 +562,12 @@ mod tests {
let pipe_w = relocate_above(pipefd[1], OUT_FD + 1).expect("relocate pipe write end");
let (pipe_r, pipe_w) = (pipe_r.into_raw_fd(), pipe_w.into_raw_fd());

let (ctrl, ready, go) =
(channel.ctrl.as_raw_fd(), channel.ready.as_raw_fd(), channel.go_r.as_raw_fd());
let (ctrl, ready, go, stub) = (
channel.ctrl.as_raw_fd(),
channel.ready.as_raw_fd(),
channel.go_r.as_raw_fd(),
channel.stub.as_raw_fd(),
);
let child = unsafe { libc::fork() };
assert!(child >= 0, "fork");
if child == 0 {
Expand All @@ -554,10 +577,14 @@ mod tests {
libc::dup2(ctrl, CTRL_FD);
libc::dup2(ready, READY_FD);
libc::dup2(go, GO_FD);
libc::dup2(stub, STUB_FD);
libc::dup2(pipe_w, OUT_FD);
let argv = [stub_path.as_ptr(), std::ptr::null()];
let envp = [std::ptr::null()];
libc::execve(stub_path.as_ptr(), argv.as_ptr(), envp.as_ptr());
let argv = [stub_name.as_ptr(), std::ptr::null()];
let envp: [*const libc::c_char; 1] = [std::ptr::null()];
libc::syscall(
libc::SYS_execveat, STUB_FD, c"".as_ptr(), argv.as_ptr(), envp.as_ptr(),
libc::AT_EMPTY_PATH,
);
libc::_exit(127);
}
}
Expand Down Expand Up @@ -610,9 +637,8 @@ mod tests {
const SENTINEL: u8 = 0x5A;
const PAGE: u64 = 0x1000;

let stub = stub_path();
if !stub.exists() {
eprintln!("skip: restore-stub not built ({})", stub.display());
if STUB_ELF.is_empty() {
eprintln!("skip: restore-stub not built");
return;
}

Expand Down Expand Up @@ -676,27 +702,35 @@ mod tests {
let plan = restore_blob::plan(&cp, None, &[]).expect("plan");
let channel = StubChannel::new(&plan.blob).expect("channel");

let stub_path = std::ffi::CString::new(stub.to_str().unwrap()).unwrap();
let stub_name = c"sandlock-restore-stub";

let mut pipefd = [0i32; 2];
assert_eq!(unsafe { libc::pipe(pipefd.as_mut_ptr()) }, 0);
let pipe_r = relocate_above(pipefd[0], OUT_FD + 1).expect("relocate pipe read end");
let pipe_w = relocate_above(pipefd[1], OUT_FD + 1).expect("relocate pipe write end");
let (pipe_r, pipe_w) = (pipe_r.into_raw_fd(), pipe_w.into_raw_fd());

let (ctrl, ready, go) =
(channel.ctrl.as_raw_fd(), channel.ready.as_raw_fd(), channel.go_r.as_raw_fd());
let (ctrl, ready, go, stub) = (
channel.ctrl.as_raw_fd(),
channel.ready.as_raw_fd(),
channel.go_r.as_raw_fd(),
channel.stub.as_raw_fd(),
);
let child = unsafe { libc::fork() };
assert!(child >= 0, "fork");
if child == 0 {
unsafe {
libc::dup2(ctrl, CTRL_FD);
libc::dup2(ready, READY_FD);
libc::dup2(go, GO_FD);
libc::dup2(stub, STUB_FD);
libc::dup2(pipe_w, OUT_FD);
let argv = [stub_path.as_ptr(), std::ptr::null()];
let envp = [std::ptr::null()];
libc::execve(stub_path.as_ptr(), argv.as_ptr(), envp.as_ptr());
let argv = [stub_name.as_ptr(), std::ptr::null()];
let envp: [*const libc::c_char; 1] = [std::ptr::null()];
libc::syscall(
libc::SYS_execveat, STUB_FD, c"".as_ptr(), argv.as_ptr(), envp.as_ptr(),
libc::AT_EMPTY_PATH,
);
libc::_exit(127);
}
}
Expand Down
Loading
Loading