1use serde::{Serialize, Deserialize};
2use crate::sandbox::Sandbox;
3use crate::error::{SandlockError, SandboxRuntimeError};
4use std::io;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Serialize, Deserialize)]
9pub struct Checkpoint {
10 pub name: String,
11 pub policy: Sandbox,
12 pub process_state: ProcessState,
13 pub fd_table: Vec<FdInfo>,
14 pub cow_snapshot: Option<PathBuf>,
15 pub app_state: Option<Vec<u8>>,
16}
17
18#[derive(Debug, Serialize, Deserialize)]
20pub struct ProcessState {
21 pub pid: i32,
22 pub cwd: String,
23 pub exe: String,
24 pub regs: Vec<u64>,
25 pub memory_maps: Vec<MemoryMap>,
26 pub memory_data: Vec<MemorySegment>,
27}
28
29#[derive(Debug, Serialize, Deserialize)]
30pub struct MemorySegment {
31 pub start: u64,
32 pub data: Vec<u8>,
33}
34
35#[derive(Debug, Serialize, Deserialize)]
36pub struct MemoryMap {
37 pub start: u64,
38 pub end: u64,
39 pub perms: String,
40 pub offset: u64,
41 pub path: Option<String>,
42}
43
44impl MemoryMap {
45 pub fn writable(&self) -> bool {
46 self.perms.starts_with("rw")
47 }
48
49 pub fn private(&self) -> bool {
50 self.perms.contains('p')
51 }
52
53 pub fn is_special(&self) -> bool {
54 self.path.as_ref().map_or(false, |p| {
55 p.starts_with("[vdso]") || p.starts_with("[vvar]") || p.starts_with("[vsyscall]")
56 })
57 }
58}
59
60#[derive(Debug, Serialize, Deserialize)]
61pub struct FdInfo {
62 pub fd: i32,
63 pub path: String,
64 pub flags: i32,
65 pub offset: u64,
66}
67
68fn ptrace_seize(pid: i32) -> io::Result<()> {
73 let ret = unsafe {
74 libc::ptrace(libc::PTRACE_SEIZE as libc::c_uint, pid, 0, 0)
75 };
76 if ret < 0 {
77 return Err(io::Error::last_os_error());
78 }
79 let ret = unsafe {
81 libc::ptrace(libc::PTRACE_INTERRUPT as libc::c_uint, pid, 0, 0)
82 };
83 if ret < 0 {
84 return Err(io::Error::last_os_error());
85 }
86 let mut status: i32 = 0;
88 unsafe {
89 libc::waitpid(pid, &mut status, 0);
90 }
91 Ok(())
92}
93
94fn ptrace_detach(pid: i32) -> io::Result<()> {
95 let ret = unsafe { libc::ptrace(libc::PTRACE_DETACH, pid, 0, 0) };
96 if ret < 0 {
97 return Err(io::Error::last_os_error());
98 }
99 Ok(())
100}
101
102fn ptrace_getregs(pid: i32) -> io::Result<Vec<u64>> {
103 #[cfg(target_arch = "x86_64")]
104 {
105 let mut regs = vec![0u64; 27];
107 let ret = unsafe { libc::ptrace(libc::PTRACE_GETREGS, pid, 0, regs.as_mut_ptr()) };
108 if ret < 0 {
109 return Err(io::Error::last_os_error());
110 }
111 Ok(regs)
112 }
113
114 #[cfg(target_arch = "aarch64")]
115 {
116 const NT_PRSTATUS: libc::c_int = 1;
120 let mut regs = vec![0u64; 34];
121 let mut iov = libc::iovec {
122 iov_base: regs.as_mut_ptr() as *mut libc::c_void,
123 iov_len: regs.len() * std::mem::size_of::<u64>(),
124 };
125 let ret = unsafe {
126 libc::ptrace(
127 libc::PTRACE_GETREGSET,
128 pid,
129 NT_PRSTATUS as usize as *mut libc::c_void,
130 &mut iov as *mut libc::iovec as *mut libc::c_void,
131 )
132 };
133 if ret < 0 {
134 return Err(io::Error::last_os_error());
135 }
136 regs.truncate(iov.iov_len / std::mem::size_of::<u64>());
137 Ok(regs)
138 }
139
140 #[cfg(target_arch = "riscv64")]
141 {
142 const NT_PRSTATUS: libc::c_int = 1;
147 let mut regs = vec![0u64; 32];
148 let mut iov = libc::iovec {
149 iov_base: regs.as_mut_ptr() as *mut libc::c_void,
150 iov_len: regs.len() * std::mem::size_of::<u64>(),
151 };
152 let ret = unsafe {
153 libc::ptrace(
154 libc::PTRACE_GETREGSET,
155 pid,
156 NT_PRSTATUS as usize as *mut libc::c_void,
157 &mut iov as *mut libc::iovec as *mut libc::c_void,
158 )
159 };
160 if ret < 0 {
161 return Err(io::Error::last_os_error());
162 }
163 regs.truncate(iov.iov_len / std::mem::size_of::<u64>());
164 Ok(regs)
165 }
166
167 #[cfg(not(any(
168 target_arch = "x86_64",
169 target_arch = "aarch64",
170 target_arch = "riscv64"
171 )))]
172 {
173 let _ = pid;
174 Err(io::Error::new(
175 io::ErrorKind::Unsupported,
176 "checkpoint register capture is not implemented on this architecture",
177 ))
178 }
179}
180
181fn parse_proc_maps(pid: i32) -> io::Result<Vec<MemoryMap>> {
186 let content = std::fs::read_to_string(format!("/proc/{}/maps", pid))?;
187 let mut maps = Vec::new();
188 for line in content.lines() {
189 let parts: Vec<&str> = line.splitn(6, ' ').collect();
191 if parts.len() < 5 {
192 continue;
193 }
194 let addrs: Vec<&str> = parts[0].split('-').collect();
195 if addrs.len() != 2 {
196 continue;
197 }
198 let start = u64::from_str_radix(addrs[0], 16).unwrap_or(0);
199 let end = u64::from_str_radix(addrs[1], 16).unwrap_or(0);
200 let perms = parts[1].to_string();
201 let offset = u64::from_str_radix(parts[2], 16).unwrap_or(0);
202 let path = if parts.len() >= 6 {
203 let p = parts[5].trim();
204 if p.is_empty() {
205 None
206 } else {
207 Some(p.to_string())
208 }
209 } else {
210 None
211 };
212 maps.push(MemoryMap {
213 start,
214 end,
215 perms,
216 offset,
217 path,
218 });
219 }
220 Ok(maps)
221}
222
223fn capture_memory(pid: i32, maps: &[MemoryMap]) -> io::Result<Vec<MemorySegment>> {
228 let mut segments = Vec::new();
229
230 for map in maps {
231 if !map.writable() || !map.private() || map.is_special() {
232 continue;
233 }
234 let size = (map.end - map.start) as usize;
235 if size > 256 * 1024 * 1024 {
236 continue; }
238
239 let mut data = vec![0u8; size];
240
241 let local_iov = libc::iovec {
242 iov_base: data.as_mut_ptr() as *mut libc::c_void,
243 iov_len: size,
244 };
245 let remote_iov = libc::iovec {
246 iov_base: map.start as *mut libc::c_void,
247 iov_len: size,
248 };
249
250 let ret = unsafe {
251 libc::process_vm_readv(
252 pid as libc::pid_t,
253 &local_iov as *const libc::iovec,
254 1,
255 &remote_iov as *const libc::iovec,
256 1,
257 0,
258 )
259 };
260
261 if ret == size as isize {
262 segments.push(MemorySegment {
263 start: map.start,
264 data,
265 });
266 }
267 }
269 Ok(segments)
270}
271
272fn capture_fd_table(pid: i32) -> io::Result<Vec<FdInfo>> {
277 let fd_dir = format!("/proc/{}/fd", pid);
278 let mut fds = Vec::new();
279
280 for entry in std::fs::read_dir(&fd_dir)? {
281 let entry = entry?;
282 let fd_str = entry.file_name().into_string().unwrap_or_default();
283 let fd: i32 = match fd_str.parse() {
284 Ok(f) => f,
285 Err(_) => continue,
286 };
287
288 let path = std::fs::read_link(entry.path())
289 .map(|p| p.display().to_string())
290 .unwrap_or_default();
291
292 let (flags, offset) = parse_fdinfo(pid, fd).unwrap_or((0, 0));
294
295 fds.push(FdInfo {
296 fd,
297 path,
298 flags,
299 offset,
300 });
301 }
302
303 fds.sort_by_key(|f| f.fd);
304 Ok(fds)
305}
306
307fn parse_fdinfo(pid: i32, fd: i32) -> io::Result<(i32, u64)> {
308 let content = std::fs::read_to_string(format!("/proc/{}/fdinfo/{}", pid, fd))?;
309 let mut flags = 0i32;
310 let mut pos = 0u64;
311 for line in content.lines() {
312 if let Some(val) = line.strip_prefix("flags:\t") {
313 flags = i32::from_str_radix(val.trim(), 8).unwrap_or(0);
314 }
315 if let Some(val) = line.strip_prefix("pos:\t") {
316 pos = val.trim().parse().unwrap_or(0);
317 }
318 }
319 Ok((flags, pos))
320}
321
322pub(crate) fn capture(pid: i32, policy: &Sandbox) -> Result<Checkpoint, SandlockError> {
329 ptrace_seize(pid).map_err(|e| {
331 SandlockError::Runtime(SandboxRuntimeError::Child(format!("ptrace seize: {}", e)))
332 })?;
333
334 let regs = ptrace_getregs(pid).map_err(|e| {
336 SandlockError::Runtime(SandboxRuntimeError::Child(format!("ptrace getregs: {}", e)))
337 })?;
338
339 let maps =
341 parse_proc_maps(pid).map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
342
343 let memory_data =
345 capture_memory(pid, &maps).map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
346
347 let fd_table =
349 capture_fd_table(pid).map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
350
351 ptrace_detach(pid).map_err(|e| {
353 SandlockError::Runtime(SandboxRuntimeError::Child(format!("ptrace detach: {}", e)))
354 })?;
355
356 let cwd = std::fs::read_link(format!("/proc/{}/cwd", pid))
358 .map(|p| p.display().to_string())
359 .unwrap_or_default();
360 let exe = std::fs::read_link(format!("/proc/{}/exe", pid))
361 .map(|p| p.display().to_string())
362 .unwrap_or_default();
363
364 Ok(Checkpoint {
365 name: String::new(),
366 policy: policy.clone(),
367 process_state: ProcessState {
368 pid,
369 cwd,
370 exe,
371 regs,
372 memory_maps: maps,
373 memory_data,
374 },
375 fd_table,
376 cow_snapshot: None,
377 app_state: None,
378 })
379}
380
381fn io_err(e: impl std::fmt::Display) -> SandlockError {
400 SandlockError::Runtime(SandboxRuntimeError::Child(e.to_string()))
401}
402
403fn write_json<T: Serialize>(path: &Path, val: &T) -> Result<(), SandlockError> {
404 let json = serde_json::to_string_pretty(val).map_err(io_err)?;
405 std::fs::write(path, json).map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))
406}
407
408fn read_json<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, SandlockError> {
409 let data = std::fs::read_to_string(path)
410 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
411 serde_json::from_str(&data).map_err(io_err)
412}
413
414#[derive(Serialize, Deserialize)]
416struct MetaJson {
417 name: String,
418 cow_snapshot: Option<String>,
419}
420
421#[derive(Serialize, Deserialize)]
423struct InfoJson {
424 pid: i32,
425 cwd: String,
426 exe: String,
427}
428
429#[derive(Serialize, Deserialize)]
431struct FdJson {
432 fd: i32,
433 path: String,
434 flags: i32,
435 offset: u64,
436}
437
438#[derive(Serialize, Deserialize)]
440struct MemoryMapJson {
441 start: u64,
442 end: u64,
443 perms: String,
444 offset: u64,
445 path: Option<String>,
446}
447
448impl Checkpoint {
449 pub fn save(&self, dir: &Path) -> Result<(), SandlockError> {
453 let tmp = dir.with_extension("tmp");
454 if tmp.exists() {
455 std::fs::remove_dir_all(&tmp)
456 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
457 }
458 std::fs::create_dir_all(&tmp)
459 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
460
461 let res = self.save_inner(&tmp);
462 if res.is_err() {
463 let _ = std::fs::remove_dir_all(&tmp);
464 return res;
465 }
466
467 if dir.exists() {
469 std::fs::remove_dir_all(dir)
470 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
471 }
472 std::fs::rename(&tmp, dir)
473 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
474
475 Ok(())
476 }
477
478 fn save_inner(&self, dir: &Path) -> Result<(), SandlockError> {
479 write_json(&dir.join("meta.json"), &MetaJson {
481 name: self.name.clone(),
482 cow_snapshot: self.cow_snapshot.as_ref().map(|p| p.display().to_string()),
483 })?;
484
485 let policy_bytes = bincode::serialize(&self.policy).map_err(io_err)?;
487 std::fs::write(dir.join("policy.dat"), &policy_bytes)
488 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
489
490 if let Some(ref state) = self.app_state {
492 std::fs::write(dir.join("app_state.bin"), state)
493 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
494 }
495
496 let proc_dir = dir.join("process");
498 std::fs::create_dir(&proc_dir)
499 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
500
501 write_json(&proc_dir.join("info.json"), &InfoJson {
503 pid: self.process_state.pid,
504 cwd: self.process_state.cwd.clone(),
505 exe: self.process_state.exe.clone(),
506 })?;
507
508 let fds: Vec<FdJson> = self.fd_table.iter().map(|f| FdJson {
510 fd: f.fd,
511 path: f.path.clone(),
512 flags: f.flags,
513 offset: f.offset,
514 }).collect();
515 write_json(&proc_dir.join("fds.json"), &fds)?;
516
517 let maps: Vec<MemoryMapJson> = self.process_state.memory_data.iter().map(|seg| {
520 let map = self.process_state.memory_maps.iter()
522 .find(|m| m.start == seg.start);
523 match map {
524 Some(m) => MemoryMapJson {
525 start: m.start,
526 end: m.end,
527 perms: m.perms.clone(),
528 offset: m.offset,
529 path: m.path.clone(),
530 },
531 None => MemoryMapJson {
532 start: seg.start,
533 end: seg.start + seg.data.len() as u64,
534 perms: "rw-p".to_string(),
535 offset: 0,
536 path: None,
537 },
538 }
539 }).collect();
540 write_json(&proc_dir.join("memory_map.json"), &maps)?;
541
542 let threads_dir = proc_dir.join("threads");
544 std::fs::create_dir(&threads_dir)
545 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
546 let reg_bytes: Vec<u8> = self.process_state.regs.iter()
547 .flat_map(|r| r.to_le_bytes())
548 .collect();
549 std::fs::write(threads_dir.join("0.bin"), ®_bytes)
550 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
551
552 let mem_dir = proc_dir.join("memory");
554 std::fs::create_dir(&mem_dir)
555 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
556 for (i, seg) in self.process_state.memory_data.iter().enumerate() {
557 std::fs::write(mem_dir.join(format!("{}.bin", i)), &seg.data)
558 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
559 }
560
561 Ok(())
562 }
563
564 pub fn load(dir: &Path) -> Result<Self, SandlockError> {
566 if !dir.is_dir() {
567 return Err(SandlockError::Runtime(SandboxRuntimeError::Child(
568 format!("Checkpoint not found: {}", dir.display()),
569 )));
570 }
571
572 let meta: MetaJson = read_json(&dir.join("meta.json"))?;
574
575 let policy_bytes = std::fs::read(dir.join("policy.dat"))
577 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
578 let policy: Sandbox = bincode::deserialize(&policy_bytes).map_err(io_err)?;
579
580 let app_state_path = dir.join("app_state.bin");
582 let app_state = if app_state_path.exists() {
583 Some(std::fs::read(&app_state_path)
584 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?)
585 } else {
586 None
587 };
588
589 let proc_dir = dir.join("process");
591
592 let info: InfoJson = read_json(&proc_dir.join("info.json"))?;
594
595 let fds_json: Vec<FdJson> = read_json(&proc_dir.join("fds.json"))?;
597 let fd_table: Vec<FdInfo> = fds_json.into_iter().map(|f| FdInfo {
598 fd: f.fd,
599 path: f.path,
600 flags: f.flags,
601 offset: f.offset,
602 }).collect();
603
604 let maps_json: Vec<MemoryMapJson> = read_json(&proc_dir.join("memory_map.json"))?;
606 let memory_maps: Vec<MemoryMap> = maps_json.iter().map(|m| MemoryMap {
607 start: m.start,
608 end: m.end,
609 perms: m.perms.clone(),
610 offset: m.offset,
611 path: m.path.clone(),
612 }).collect();
613
614 let reg_bytes = std::fs::read(proc_dir.join("threads").join("0.bin"))
616 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
617 let regs: Vec<u64> = reg_bytes.chunks_exact(8)
618 .map(|chunk| u64::from_le_bytes(chunk.try_into().unwrap()))
619 .collect();
620
621 let mem_dir = proc_dir.join("memory");
623 let mut memory_data = Vec::new();
624 for (i, map) in maps_json.iter().enumerate() {
625 let seg_path = mem_dir.join(format!("{}.bin", i));
626 let data = std::fs::read(&seg_path)
627 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
628 memory_data.push(MemorySegment {
629 start: map.start,
630 data,
631 });
632 }
633
634 Ok(Checkpoint {
635 name: meta.name,
636 policy,
637 process_state: ProcessState {
638 pid: info.pid,
639 cwd: info.cwd,
640 exe: info.exe,
641 regs,
642 memory_maps,
643 memory_data,
644 },
645 fd_table,
646 cow_snapshot: meta.cow_snapshot.map(PathBuf::from),
647 app_state,
648 })
649 }
650}
651
652#[cfg(test)]
653mod tests {
654 use super::*;
655 use std::process::Command;
656
657 #[test]
662 fn ptrace_getregs_captures_program_counter() {
663 let mut child = Command::new("sleep")
664 .arg("30")
665 .spawn()
666 .expect("spawn sleep child");
667 let pid = child.id() as i32;
668
669 let result = (|| -> io::Result<Vec<u64>> {
670 ptrace_seize(pid)?;
671 let regs = ptrace_getregs(pid)?;
672 ptrace_detach(pid)?;
673 Ok(regs)
674 })();
675
676 let _ = child.kill();
677 let _ = child.wait();
678
679 let regs = result.expect("register capture should succeed on this architecture");
680
681 #[cfg(target_arch = "x86_64")]
683 assert_eq!(regs.len(), 27, "x86_64 user_regs_struct is 27 u64");
684 #[cfg(target_arch = "aarch64")]
685 assert_eq!(regs.len(), 34, "aarch64 user_pt_regs is 34 u64");
686 #[cfg(target_arch = "riscv64")]
687 assert_eq!(regs.len(), 32, "riscv64 user_regs_struct is 32 u64");
688
689 #[cfg(target_arch = "x86_64")]
692 let pc = regs[16]; #[cfg(target_arch = "aarch64")]
694 let pc = regs[32]; #[cfg(target_arch = "riscv64")]
696 let pc = regs[0]; #[cfg(any(
699 target_arch = "x86_64",
700 target_arch = "aarch64",
701 target_arch = "riscv64"
702 ))]
703 assert!(pc != 0, "captured program counter should be non-zero, got {:#x}", pc);
704 }
705
706 #[test]
712 fn capture_save_load_roundtrips() {
713 let mut child = Command::new("sleep")
714 .arg("30")
715 .spawn()
716 .expect("spawn sleep child");
717 let pid = child.id() as i32;
718
719 let policy = Sandbox::builder().build().expect("build policy");
720 let captured = capture(pid, &policy);
721
722 let _ = child.kill();
723 let _ = child.wait();
724
725 let cp = captured.expect("capture should succeed on this architecture");
726 assert!(!cp.process_state.regs.is_empty(), "captured registers");
727 assert!(!cp.process_state.memory_maps.is_empty(), "captured memory maps");
728 assert!(!cp.fd_table.is_empty(), "captured fd table");
729
730 let dir = std::env::temp_dir()
732 .join(format!("sandlock-cp-roundtrip-{}", std::process::id()));
733 cp.save(&dir).expect("save checkpoint");
734 let loaded = Checkpoint::load(&dir).expect("load checkpoint");
735 let _ = std::fs::remove_dir_all(&dir);
736
737 assert_eq!(loaded.process_state.regs, cp.process_state.regs, "regs roundtrip");
738 assert_eq!(
739 loaded.process_state.memory_data.len(),
740 cp.process_state.memory_data.len(),
741 "memory segment count roundtrip"
742 );
743 assert_eq!(loaded.fd_table.len(), cp.fd_table.len(), "fd count roundtrip");
744 assert_eq!(loaded.process_state.pid, cp.process_state.pid, "pid roundtrip");
745 assert!(!loaded.process_state.exe.is_empty(), "exe path captured");
746 }
747}