diff --git a/crates/sandlock-core/src/checkpoint/capture.rs b/crates/sandlock-core/src/checkpoint/capture.rs index 34a65a56..31473836 100644 --- a/crates/sandlock-core/src/checkpoint/capture.rs +++ b/crates/sandlock-core/src/checkpoint/capture.rs @@ -199,7 +199,21 @@ fn capture_memory(pid: i32, maps: &[MemoryMap]) -> io::Result 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; diff --git a/crates/sandlock-core/src/checkpoint/resume.rs b/crates/sandlock-core/src/checkpoint/resume.rs index 71e6f4c2..da76ed39 100644 --- a/crates/sandlock-core/src/checkpoint/resume.rs +++ b/crates/sandlock-core/src/checkpoint/resume.rs @@ -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, crate::error::SandlockError> { use crate::checkpoint::inject; use crate::error::{SandboxRuntimeError, SandlockError}; @@ -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//maps and /proc//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 /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); @@ -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, @@ -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 @@ -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 @@ -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, crate::error::SandlockError> { Err(crate::error::SandlockError::Runtime( crate::error::SandboxRuntimeError::Child( @@ -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]; diff --git a/crates/sandlock-core/src/sandbox.rs b/crates/sandlock-core/src/sandbox.rs index 86401f1e..5f46a33a 100644 --- a/crates/sandlock-core/src/sandbox.rs +++ b/crates/sandlock-core/src/sandbox.rs @@ -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, crate::error::SandlockError> { @@ -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);