use crate::checkpoint::{Checkpoint, FdInfo, MemoryMap, MemorySegment, SkippedFd};
#[derive(Debug)]
pub(crate) enum RestoreRegion {
RemapFromFile { start: u64, end: u64, perms: String, offset: u64, path: String },
WriteBytes { start: u64, end: u64, perms: String, data: Vec<u8> },
}
pub(crate) fn build_memory_plan(
maps: &[MemoryMap],
data: &[MemorySegment],
) -> Vec<RestoreRegion> {
let mut plan = Vec::new();
for m in maps {
if m.is_special() { continue; }
if let Some(seg) = data.iter().find(|s| s.start == m.start) {
plan.push(RestoreRegion::WriteBytes {
start: m.start, end: m.end, perms: m.perms.clone(), data: seg.data.clone(),
});
} else if let Some(ref p) = m.path {
if p.starts_with('/') {
plan.push(RestoreRegion::RemapFromFile {
start: m.start, end: m.end, perms: m.perms.clone(),
offset: m.offset, path: p.clone(),
});
}
}
}
plan
}
fn is_restorable_file_path(path: &str) -> bool {
path.starts_with('/')
&& !path.starts_with("/memfd:")
&& !path.ends_with(" (deleted)")
&& !path.starts_with("/proc/")
&& !path.starts_with("/sys/")
&& !path.starts_with("/dev/")
}
pub(crate) fn build_fd_plan(fds: &[FdInfo]) -> (Vec<FdInfo>, Vec<SkippedFd>) {
let mut restorable = Vec::new();
let mut skipped = Vec::new();
for f in fds {
if is_restorable_file_path(&f.path) {
restorable.push(f.clone());
} else {
skipped.push(SkippedFd { fd: f.fd, path: f.path.clone() });
}
}
(restorable, skipped)
}
fn find_named_map(maps: &[MemoryMap], name: &str) -> Option<(u64, u64)> {
maps.iter()
.find(|m| m.path.as_deref() == Some(name))
.map(|m| (m.start, m.end))
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct VdsoMove {
pub cur_start: u64,
pub len: u64,
pub target_start: u64,
}
pub(crate) fn plan_vdso_moves(
cur: &[MemoryMap],
cp: &[MemoryMap],
) -> Result<Vec<VdsoMove>, String> {
let mut remaining: Vec<VdsoMove> = Vec::new();
for name in ["[vvar]", "[vvar_vclock]", "[vdso]"] {
if let (Some((cs, ce)), Some((ts, _))) =
(find_named_map(cur, name), find_named_map(cp, name))
{
if cs != ts {
remaining.push(VdsoMove { cur_start: cs, len: ce - cs, target_start: ts });
}
}
}
let mut ordered = Vec::new();
while !remaining.is_empty() {
let pick = remaining.iter().position(|m| {
let (t_start, t_end) = (m.target_start, m.target_start + m.len);
!remaining
.iter()
.any(|o| o != m && t_start < (o.cur_start + o.len) && o.cur_start < t_end)
});
match pick {
Some(i) => ordered.push(remaining.remove(i)),
None => {
return Err(
"vdso/vvar relocation requires swapping overlapping ranges".into(),
)
}
}
}
Ok(ordered)
}
fn prot_from_perms(perms: &str) -> libc::c_int {
let mut prot = 0;
if perms.as_bytes().first() == Some(&b'r') { prot |= libc::PROT_READ; }
if perms.as_bytes().get(1) == Some(&b'w') { prot |= libc::PROT_WRITE; }
if perms.as_bytes().get(2) == Some(&b'x') { prot |= libc::PROT_EXEC; }
if prot == 0 { prot = libc::PROT_NONE; }
prot
}
#[cfg(target_arch = "x86_64")]
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};
const MMAP: u64 = 9;
const MPROTECT: u64 = 10;
const MUNMAP: u64 = 11;
const MREMAP: u64 = 25;
const OPEN: u64 = 2;
const CLOSE: u64 = 3;
const LSEEK: u64 = 8;
const DUP2: u64 = 33;
let err = |msg: String| SandlockError::Runtime(SandboxRuntimeError::Child(msg));
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);
let tramp = inject::setup_trampoline(pid, &cp.process_state.memory_maps)
.map_err(|e| err(format!("restore setup trampoline: {e}")))?;
let scratch = tramp + 64;
const SCRATCH_MAX: usize = 4096 - 64;
let write_path = |path: &str| -> Result<(), SandlockError> {
let mut p = path.as_bytes().to_vec();
p.push(0);
if p.len() > SCRATCH_MAX {
return Err(err("restore path too long for scratch".into()));
}
inject::write_child_mem(pid, scratch, &p)
.map_err(|e| err(format!("restore write path {path}: {e}")))
};
for region in &plan {
match region {
RestoreRegion::WriteBytes { start, end, perms, data } => {
let len = (end - start) as usize;
let r = inject::inject_syscall_at(
pid,
tramp,
MMAP,
[
*start,
len as u64,
(libc::PROT_READ | libc::PROT_WRITE) as u64,
(libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_FIXED) as u64,
(-1i64) as u64,
0,
],
)
.map_err(|e| err(format!("restore anon mmap at {start:#x}: {e}")))?;
if r as u64 != *start {
return Err(err(format!("restore anon mmap at {start:#x} -> {r:#x}")));
}
let n = data.len().min(len);
inject::write_child_mem(pid, *start, &data[..n])
.map_err(|e| err(format!("restore write bytes at {start:#x}: {e}")))?;
let prot = prot_from_perms(perms);
if prot != (libc::PROT_READ | libc::PROT_WRITE) {
let m = inject::inject_syscall_at(
pid,
tramp,
MPROTECT,
[*start, len as u64, prot as u64, 0, 0, 0],
)
.map_err(|e| err(format!("restore mprotect {start:#x}: {e}")))?;
if m != 0 {
return Err(err(format!("restore mprotect {start:#x}")));
}
}
}
RestoreRegion::RemapFromFile { start, end, perms, offset, path } => {
let len = (end - start) as usize;
let prot = prot_from_perms(perms);
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 {open_path}: {e}")))?;
if fd < 0 {
return Err(err(format!("restore open {open_path} -> {fd}")));
}
let r = inject::inject_syscall_at(
pid,
tramp,
MMAP,
[
*start,
len as u64,
prot as u64,
(libc::MAP_PRIVATE | libc::MAP_FIXED) as u64,
fd as u64,
*offset,
],
)
.map_err(|e| err(format!("restore file mmap at {start:#x}: {e}")))?;
if r as u64 != *start {
return Err(err(format!("restore file mmap at {start:#x} -> {r:#x}")));
}
let cl = inject::inject_syscall_at(pid, tramp, CLOSE, [fd as u64, 0, 0, 0, 0, 0])
.map_err(|e| err(format!("restore close fd {fd}: {e}")))?;
if cl < 0 { return Err(err(format!("restore close fd {fd} -> {cl}"))); }
}
}
}
for f in &restorable_fds {
let open_path = to_open_path(&f.path);
write_path(&open_path)?;
let safe_flags = f.flags & !(libc::O_CREAT | libc::O_TRUNC | libc::O_EXCL);
let opened = inject::inject_syscall_at(
pid,
tramp,
OPEN,
[scratch, safe_flags as u64, 0, 0, 0, 0],
)
.map_err(|e| err(format!("restore fd open {}: {e}", open_path)))?;
if opened < 0 {
return Err(err(format!("restore fd open {} -> {opened}", open_path)));
}
if opened as i32 != f.fd {
let d = inject::inject_syscall_at(pid, tramp, DUP2, [opened as u64, f.fd as u64, 0, 0, 0, 0])
.map_err(|e| err(format!("restore dup2 {opened}->{}: {e}", f.fd)))?;
if d < 0 { return Err(err(format!("restore dup2 {} -> {} failed: {d}", opened, f.fd))); }
let cl2 = inject::inject_syscall_at(pid, tramp, CLOSE, [opened as u64, 0, 0, 0, 0, 0])
.map_err(|e| err(format!("restore close dup src {opened}: {e}")))?;
if cl2 < 0 { return Err(err(format!("restore close dup src {opened} -> {cl2}"))); }
}
let ls = inject::inject_syscall_at(
pid,
tramp,
LSEEK,
[f.fd as u64, f.offset, libc::SEEK_SET as u64, 0, 0, 0],
)
.map_err(|e| err(format!("restore lseek fd {}: {e}", f.fd)))?;
if ls < 0 {
return Err(err(format!("restore lseek fd {}", f.fd)));
}
}
{
let cur = crate::checkpoint::capture::parse_proc_maps(pid)
.map_err(|e| err(format!("restore read maps for vdso relocation: {e}")))?;
let moves = plan_vdso_moves(&cur, &cp.process_state.memory_maps)
.map_err(|m| err(format!("restore vdso relocation: {m}")))?;
for mv in moves {
let flags = (libc::MREMAP_MAYMOVE | libc::MREMAP_FIXED) as u64;
let r = inject::inject_syscall_at(
pid,
tramp,
MREMAP,
[mv.cur_start, mv.len, mv.len, flags, mv.target_start, 0],
)
.map_err(|e| {
err(format!(
"restore mremap {:#x}->{:#x}: {e}",
mv.cur_start, mv.target_start
))
})?;
if r as u64 != mv.target_start {
return Err(err(format!(
"restore mremap {:#x}->{:#x} -> {r:#x}",
mv.cur_start, mv.target_start
)));
}
}
}
let mu = inject::inject_syscall_at(pid, tramp, MUNMAP, [tramp, 4096, 0, 0, 0, 0])
.map_err(|e| err(format!("restore munmap trampoline {tramp:#x}: {e}")))?;
if mu != 0 { return Err(err(format!("restore munmap trampoline {tramp:#x} -> {mu}"))); }
crate::checkpoint::regs::set_fp_regs(pid, &cp.process_state.fpregs)
.map_err(|e| err(format!("restore set fp regs: {e}")))?;
const RAX: usize = 10;
const ORIG_RAX: usize = 15;
const RIP: usize = 16;
let mut regs = cp.process_state.regs.clone();
if let (Some(&rax), Some(&orig_rax)) = (regs.get(RAX), regs.get(ORIG_RAX)) {
let rax_signed = rax as i64;
if matches!(rax_signed, -512 | -513 | -514 | -516) {
regs[RAX] = orig_rax;
regs[RIP] = regs[RIP].wrapping_sub(2);
}
}
crate::checkpoint::regs::set_gp_regs(pid, ®s)
.map_err(|e| err(format!("restore set gp regs: {e}")))?;
Ok(skipped)
}
#[cfg(not(target_arch = "x86_64"))]
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(
"injection-based restore is only implemented on x86_64".into(),
),
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::checkpoint::{FdInfo, MemoryMap, MemorySegment, SkippedFd};
#[test]
fn fd_plan_keeps_regular_files_only() {
let fds = vec![
FdInfo { fd: 3, path: "/etc/hostname".into(), flags: 0, offset: 5 },
FdInfo { fd: 4, path: "socket:[12345]".into(), flags: 0, offset: 0 },
FdInfo { fd: 5, path: "pipe:[6789]".into(), flags: 0, offset: 0 },
];
let (restorable, skipped) = build_fd_plan(&fds);
assert_eq!(restorable.len(), 1);
assert_eq!(restorable[0].fd, 3);
assert_eq!(skipped, vec![
SkippedFd { fd: 4, path: "socket:[12345]".into() },
SkippedFd { fd: 5, path: "pipe:[6789]".into() },
]);
}
#[test]
fn fd_plan_skips_deleted_and_memfd() {
let fds = vec![
FdInfo { fd: 3, path: "/etc/hostname".into(), flags: 0, offset: 5 },
FdInfo { fd: 6, path: "/tmp/gone (deleted)".into(), flags: 0, offset: 0 },
FdInfo { fd: 7, path: "/memfd:scratch (deleted)".into(), flags: 0, offset: 0 },
FdInfo { fd: 8, path: "/proc/1234/maps".into(), flags: 0, offset: 0 },
FdInfo { fd: 9, path: "/dev/pts/3".into(), flags: 0, offset: 0 },
FdInfo { fd: 10, path: "/sys/kernel/x".into(), flags: 0, offset: 0 },
];
let (restorable, skipped) = build_fd_plan(&fds);
assert_eq!(restorable.len(), 1);
assert_eq!(restorable[0].fd, 3);
assert!(restorable.iter().all(|f| f.fd != 6 && f.fd != 7 && f.fd != 8 && f.fd != 9 && f.fd != 10),
"deleted, memfd, and pseudo-filesystem fds must not appear in restorable");
assert!(skipped.contains(&SkippedFd { fd: 6, path: "/tmp/gone (deleted)".into() }));
assert!(skipped.contains(&SkippedFd { fd: 7, path: "/memfd:scratch (deleted)".into() }));
assert!(skipped.contains(&SkippedFd { fd: 8, path: "/proc/1234/maps".into() }),
"/proc/ paths must be skipped");
assert!(skipped.contains(&SkippedFd { fd: 9, path: "/dev/pts/3".into() }),
"/dev/ paths must be skipped");
assert!(skipped.contains(&SkippedFd { fd: 10, path: "/sys/kernel/x".into() }),
"/sys/ paths must be skipped");
}
fn map(start: u64, end: u64, path: Option<&str>) -> MemoryMap {
MemoryMap { start, end, perms: "rw-p".into(), offset: 0, path: path.map(Into::into) }
}
#[test]
fn vdso_moves_relocate_present_mappings_only() {
let cur = vec![
map(0x1000, 0x2000, Some("[vvar]")),
map(0x2000, 0x3000, Some("[vdso]")),
map(0x9000, 0xa000, None),
];
let cp = vec![
map(0x5000, 0x6000, Some("[vvar]")),
map(0x6000, 0x7000, Some("[vdso]")),
];
let moves = plan_vdso_moves(&cur, &cp).expect("no cycle");
assert_eq!(moves.len(), 2, "both special mappings relocate");
assert!(moves.iter().any(|m| m.cur_start == 0x1000 && m.target_start == 0x5000));
assert!(moves.iter().any(|m| m.cur_start == 0x2000 && m.target_start == 0x6000));
}
#[test]
fn vdso_moves_skip_when_base_already_matches() {
let cur = vec![map(0x5000, 0x6000, Some("[vdso]"))];
let cp = vec![map(0x5000, 0x6000, Some("[vdso]"))];
assert!(plan_vdso_moves(&cur, &cp).unwrap().is_empty(),
"no move when the base already matches");
}
#[test]
fn vdso_moves_skip_when_absent_from_checkpoint() {
let cur = vec![map(0x2000, 0x3000, Some("[vdso]"))];
let cp = vec![map(0x2000, 0x3000, None)];
assert!(plan_vdso_moves(&cur, &cp).unwrap().is_empty());
}
#[test]
fn vdso_moves_order_avoids_clobbering_a_source() {
let cur = vec![
map(0x1000, 0x2000, Some("[vvar]")),
map(0x5000, 0x6000, Some("[vdso]")),
];
let cp = vec![
map(0x5000, 0x6000, Some("[vvar]")), map(0x8000, 0x9000, Some("[vdso]")),
];
let moves = plan_vdso_moves(&cur, &cp).expect("no cycle");
assert_eq!(moves[0].cur_start, 0x5000, "vdso relocates before vvar overwrites its base");
assert_eq!(moves[1].cur_start, 0x1000);
}
#[test]
fn vdso_moves_reject_unresolvable_swap() {
let cur = vec![
map(0x1000, 0x2000, Some("[vvar]")),
map(0x5000, 0x6000, Some("[vdso]")),
];
let cp = vec![
map(0x5000, 0x6000, Some("[vvar]")),
map(0x1000, 0x2000, Some("[vdso]")),
];
assert!(plan_vdso_moves(&cur, &cp).is_err(), "a pure swap is rejected, not corrupted");
}
#[test]
fn plan_classifies_regions() {
let maps = vec![
MemoryMap { start: 0x1000, end: 0x2000, perms: "r-xp".into(), offset: 0,
path: Some("/bin/app".into()) }, MemoryMap { start: 0x3000, end: 0x4000, perms: "rw-p".into(), offset: 0,
path: None }, MemoryMap { start: 0x5000, end: 0x6000, perms: "r--p".into(), offset: 0,
path: Some("[vvar]".into()) }, ];
let data = vec![MemorySegment { start: 0x3000, data: vec![7u8; 0x1000] }];
let plan = build_memory_plan(&maps, &data);
assert!(matches!(plan[0], RestoreRegion::RemapFromFile { .. }));
assert!(matches!(plan[1], RestoreRegion::WriteBytes { .. }));
assert_eq!(plan.len(), 2, "special regions are skipped, not planned");
}
#[test]
#[cfg(target_arch = "x86_64")]
fn restore_into_reconstructs_memory_and_regs() {
const DON: u64 = 0x4500_0000_0000;
const PAT: u8 = 0xC7;
let donor = unsafe { libc::fork() };
if donor == 0 {
unsafe {
let p = libc::mmap(
DON as *mut libc::c_void,
4096,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_FIXED,
-1,
0,
);
if p != DON as *mut libc::c_void {
libc::_exit(1);
}
let mut i = 0usize;
while i < 4096 {
*(DON as *mut u8).add(i) = PAT;
i += 1;
}
loop {
libc::pause();
}
}
}
assert!(donor > 0, "fork donor");
unsafe {
libc::usleep(50_000);
}
let policy = crate::Sandbox::builder().build().unwrap();
let cp = crate::checkpoint::capture::capture(donor as i32, &policy).expect("capture");
unsafe {
libc::kill(donor, libc::SIGKILL);
let mut s = 0;
libc::waitpid(donor, &mut s, 0);
}
let seg = cp
.process_state
.memory_data
.iter()
.find(|s| s.start == DON)
.expect("donor DON page must be captured");
assert!(
seg.data.len() >= 4096 && seg.data[..4096].iter().all(|&b| b == PAT),
"captured DON page must be all 0x{PAT:02x}"
);
let stub = unsafe { libc::fork() };
if stub == 0 {
unsafe {
libc::ptrace(libc::PTRACE_TRACEME, 0, 0, 0);
libc::raise(libc::SIGSTOP);
libc::_exit(0); }
}
assert!(stub > 0, "fork stub");
let mut st = 0i32;
unsafe {
libc::waitpid(stub, &mut st, 0);
}
let _skipped = restore_into(stub, &cp, None, &[]).expect("restore_into");
let mut buf = vec![0u8; 4096];
let local = libc::iovec {
iov_base: buf.as_mut_ptr() as *mut libc::c_void,
iov_len: 4096,
};
let remote = libc::iovec {
iov_base: DON as *mut libc::c_void,
iov_len: 4096,
};
let n = unsafe { libc::process_vm_readv(stub, &local, 1, &remote, 1, 0) };
let read_regs = crate::checkpoint::capture::ptrace_getregs(stub);
unsafe {
libc::kill(stub, libc::SIGKILL);
let mut s = 0;
libc::waitpid(stub, &mut s, 0);
}
assert_eq!(n, 4096, "process_vm_readv of restored DON page");
assert!(
buf.iter().all(|&b| b == PAT),
"restored DON page must be all 0x{PAT:02x}"
);
let read_regs = read_regs.expect("read stub regs");
let mut expected = cp.process_state.regs.clone();
const RAX: usize = 10;
const ORIG_RAX: usize = 15;
const RIP: usize = 16;
let rax_signed = expected[RAX] as i64;
if matches!(rax_signed, -512 | -513 | -514 | -516) {
expected[RAX] = expected[ORIG_RAX];
expected[RIP] = expected[RIP].wrapping_sub(2);
}
assert_eq!(
read_regs, expected,
"restored GP registers must match the checkpoint (with syscall-restart re-arm)"
);
}
}