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
16 changes: 15 additions & 1 deletion crates/sandlock-core/src/checkpoint/capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,21 @@ fn capture_memory(pid: i32, maps: &[MemoryMap]) -> io::Result<Vec<MemorySegment>
let mut segments = Vec::new();

for map in maps {
if !map.writable() || !map.private() || map.is_special() {
if map.is_special() || !map.private() {
continue;
}
// Dump writable regions (their contents are live process state) and any
// region backed by an unreopenable file. A memfd (e.g. the memfd the
// launcher execs the image binary from) or a "(deleted)" path has no
// file restore can reopen, so its bytes must travel inside the image or
// the mapping — including the program text of an imaged workload — is
// lost. Read-only regions backed by a real file are left to be remapped
// from that file at restore, which is cheaper and shares pages.
let unreopenable = map
.path
.as_deref()
.map_or(false, |p| p.starts_with("/memfd:") || p.ends_with(" (deleted)"));
if !map.writable() && !unreopenable {
continue;
}
let size = (map.end - map.start) as usize;
Expand Down
41 changes: 34 additions & 7 deletions crates/sandlock-core/src/checkpoint/resume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ fn prot_from_perms(perms: &str) -> libc::c_int {
pub(crate) fn restore_into(
pid: i32,
cp: &Checkpoint,
chroot_root: Option<&std::path::Path>,
mounts: &[(std::path::PathBuf, std::path::PathBuf)],
) -> Result<Vec<SkippedFd>, crate::error::SandlockError> {
use crate::checkpoint::inject;
use crate::error::{SandboxRuntimeError, SandlockError};
Expand All @@ -188,6 +190,27 @@ pub(crate) fn restore_into(
// Build SandlockError::Runtime(Child(..)) the same way capture.rs does.
let err = |msg: String| SandlockError::Runtime(SandboxRuntimeError::Child(msg));

// A confined process records its mapping/fd paths as HOST paths: capture
// reads /proc/<pid>/maps and /proc/<pid>/fd, whose entries are resolved in
// the mount namespace and so ignore the process's chroot (a file the
// workload sees at /lib/ld-musl... appears as <rootfs>/lib/ld-musl...).
// The open()s injected below run in the already-confined child, where those
// host paths do not resolve. Translate each back to the child's in-chroot
// view so the reopen lands on the same file. Without a chroot the paths are
// already the child's view, so they pass through unchanged.
let to_open_path = |host_path: &str| -> String {
match chroot_root {
Some(root) => crate::chroot::resolve::host_to_virtual(
root,
mounts,
std::path::Path::new(host_path),
)
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|| host_path.to_string()),
None => host_path.to_string(),
}
};

let plan = build_memory_plan(&cp.process_state.memory_maps, &cp.process_state.memory_data);
let (restorable_fds, skipped) = build_fd_plan(&cp.fd_table);

Expand Down Expand Up @@ -255,16 +278,17 @@ pub(crate) fn restore_into(
RestoreRegion::RemapFromFile { start, end, perms, offset, path } => {
let len = (end - start) as usize;
let prot = prot_from_perms(perms);
write_path(path)?;
let open_path = to_open_path(path);
write_path(&open_path)?;
let fd = inject::inject_syscall_at(
pid,
tramp,
OPEN,
[scratch, libc::O_RDONLY as u64, 0, 0, 0, 0],
)
.map_err(|e| err(format!("restore open {path}: {e}")))?;
.map_err(|e| err(format!("restore open {open_path}: {e}")))?;
if fd < 0 {
return Err(err(format!("restore open {path} -> {fd}")));
return Err(err(format!("restore open {open_path} -> {fd}")));
}
let r = inject::inject_syscall_at(
pid,
Expand Down Expand Up @@ -292,7 +316,8 @@ pub(crate) fn restore_into(

// Reopen transparently restorable fds at their saved numbers/offsets.
for f in &restorable_fds {
write_path(&f.path)?;
let open_path = to_open_path(&f.path);
write_path(&open_path)?;
// Mask creation/truncation flags so the restored open cannot create,
// truncate, or fail-exclusive on the workload's real file. The kernel
// strips these in fdinfo, but mask defensively since O_TRUNC would be
Expand All @@ -304,9 +329,9 @@ pub(crate) fn restore_into(
OPEN,
[scratch, safe_flags as u64, 0, 0, 0, 0],
)
.map_err(|e| err(format!("restore fd open {}: {e}", f.path)))?;
.map_err(|e| err(format!("restore fd open {}: {e}", open_path)))?;
if opened < 0 {
return Err(err(format!("restore fd open {} -> {opened}", f.path)));
return Err(err(format!("restore fd open {} -> {opened}", open_path)));
}
if opened as i32 != f.fd {
// dup2 may clobber an inherited stub fd at this number; that is
Expand Down Expand Up @@ -426,6 +451,8 @@ pub(crate) fn restore_into(
pub(crate) fn restore_into(
_pid: i32,
_cp: &Checkpoint,
_chroot_root: Option<&std::path::Path>,
_mounts: &[(std::path::PathBuf, std::path::PathBuf)],
) -> Result<Vec<SkippedFd>, crate::error::SandlockError> {
Err(crate::error::SandlockError::Runtime(
crate::error::SandboxRuntimeError::Child(
Expand Down Expand Up @@ -649,7 +676,7 @@ mod tests {
libc::waitpid(stub, &mut st, 0);
} // catch the SIGSTOP-stop

let _skipped = restore_into(stub, &cp).expect("restore_into");
let _skipped = restore_into(stub, &cp, None, &[]).expect("restore_into");

// Read the restored DON page back out of the still-stopped stub.
let mut buf = vec![0u8; 4096];
Expand Down
11 changes: 10 additions & 1 deletion crates/sandlock-core/src/sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,13 @@ impl Sandbox {
// 'static closure, so move a clone of `cp` in. The clone resets the
// policy's runtime to None (Sandbox::clone), which is harmless here:
// restore_into reads only process_state + fd_table, never policy.
// Resolve the confinement's chroot root and mounts so restore_into can
// translate the checkpoint's HOST-recorded mapping/fd paths back into
// the child's in-chroot view before reopening them (see restore_into).
// Empty/None when there is no chroot, leaving paths untranslated.
let chroot_root = crate::chroot::resolve::resolve_chroot_root(self.chroot.as_deref())?;
let mounts = crate::chroot::resolve::resolve_chroot_mounts(&self.fs_mount);

let cp = cp.clone();
let skipped = tokio::task::spawn_blocking(
move || -> Result<Vec<crate::checkpoint::SkippedFd>, crate::error::SandlockError> {
Expand All @@ -919,7 +926,9 @@ impl Sandbox {
// saved registers (including rip at the checkpoint pc) loaded.
// On error, best-effort detach so the child is not left seized
// with a dangling tracer thread.
let skipped = match crate::checkpoint::resume::restore_into(pid, &cp) {
let skipped = match crate::checkpoint::resume::restore_into(
pid, &cp, chroot_root.as_deref(), &mounts,
) {
Ok(s) => s,
Err(e) => {
let _ = crate::checkpoint::capture::ptrace_detach(pid);
Expand Down
Loading