sandlock_core/checkpoint/
image.rs1use serde::{Serialize, Deserialize};
2use std::path::{Path, PathBuf};
3use super::{Checkpoint, ProcessState, MemorySegment, MemoryMap, FdInfo};
4use crate::error::{SandlockError, SandboxRuntimeError};
5use crate::sandbox::Sandbox;
6
7const IMAGE_VERSION: u32 = 2;
32
33fn io_err(e: impl std::fmt::Display) -> SandlockError {
34 SandlockError::Runtime(SandboxRuntimeError::Child(e.to_string()))
35}
36
37fn write_json<T: Serialize>(path: &Path, val: &T) -> Result<(), SandlockError> {
38 let json = serde_json::to_string_pretty(val).map_err(io_err)?;
39 std::fs::write(path, json).map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))
40}
41
42fn read_json<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, SandlockError> {
43 let data = std::fs::read_to_string(path)
44 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
45 serde_json::from_str(&data).map_err(io_err)
46}
47
48#[derive(Serialize, Deserialize)]
50struct MetaJson {
51 name: String,
52 cow_snapshot: Option<String>,
53 #[serde(default)]
54 version: u32,
55}
56
57#[derive(Serialize, Deserialize)]
59struct InfoJson {
60 pid: i32,
61 cwd: String,
62 exe: String,
63}
64
65#[derive(Serialize, Deserialize)]
67struct FdJson {
68 fd: i32,
69 path: String,
70 flags: i32,
71 offset: u64,
72}
73
74#[derive(Serialize, Deserialize)]
76struct MemoryMapJson {
77 start: u64,
78 end: u64,
79 perms: String,
80 offset: u64,
81 path: Option<String>,
82}
83
84impl Checkpoint {
85 pub fn save(&self, dir: &Path) -> Result<(), SandlockError> {
89 let tmp = dir.with_extension("tmp");
90 if tmp.exists() {
91 std::fs::remove_dir_all(&tmp)
92 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
93 }
94 std::fs::create_dir_all(&tmp)
95 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
96
97 let res = self.save_inner(&tmp);
98 if res.is_err() {
99 let _ = std::fs::remove_dir_all(&tmp);
100 return res;
101 }
102
103 if dir.exists() {
105 std::fs::remove_dir_all(dir)
106 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
107 }
108 std::fs::rename(&tmp, dir)
109 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
110
111 Ok(())
112 }
113
114 fn save_inner(&self, dir: &Path) -> Result<(), SandlockError> {
115 write_json(&dir.join("meta.json"), &MetaJson {
117 name: self.name.clone(),
118 cow_snapshot: self.cow_snapshot.as_ref().map(|p| p.display().to_string()),
119 version: IMAGE_VERSION,
120 })?;
121
122 let policy_bytes = bincode::serialize(&self.policy).map_err(io_err)?;
124 std::fs::write(dir.join("policy.dat"), &policy_bytes)
125 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
126
127 if let Some(ref state) = self.app_state {
129 std::fs::write(dir.join("app_state.bin"), state)
130 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
131 }
132
133 let proc_dir = dir.join("process");
135 std::fs::create_dir(&proc_dir)
136 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
137
138 write_json(&proc_dir.join("info.json"), &InfoJson {
140 pid: self.process_state.pid,
141 cwd: self.process_state.cwd.clone(),
142 exe: self.process_state.exe.clone(),
143 })?;
144
145 let fds: Vec<FdJson> = self.fd_table.iter().map(|f| FdJson {
147 fd: f.fd,
148 path: f.path.clone(),
149 flags: f.flags,
150 offset: f.offset,
151 }).collect();
152 write_json(&proc_dir.join("fds.json"), &fds)?;
153
154 let maps: Vec<MemoryMapJson> = self.process_state.memory_maps.iter().map(|m| MemoryMapJson {
158 start: m.start,
159 end: m.end,
160 perms: m.perms.clone(),
161 offset: m.offset,
162 path: m.path.clone(),
163 }).collect();
164 write_json(&proc_dir.join("memory_map.json"), &maps)?;
165
166 let threads_dir = proc_dir.join("threads");
168 std::fs::create_dir(&threads_dir)
169 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
170 let reg_bytes: Vec<u8> = self.process_state.regs.iter()
171 .flat_map(|r| r.to_le_bytes())
172 .collect();
173 std::fs::write(threads_dir.join("0.bin"), ®_bytes)
174 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
175 std::fs::write(threads_dir.join("fpregs.bin"), &self.process_state.fpregs)
177 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
178
179 let mem_dir = proc_dir.join("memory");
183 std::fs::create_dir(&mem_dir)
184 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
185 for seg in self.process_state.memory_data.iter() {
186 std::fs::write(mem_dir.join(format!("{:x}.bin", seg.start)), &seg.data)
187 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
188 }
189
190 Ok(())
191 }
192
193 pub fn load(dir: &Path) -> Result<Self, SandlockError> {
195 if !dir.is_dir() {
196 return Err(SandlockError::Runtime(SandboxRuntimeError::Child(
197 format!("Checkpoint not found: {}", dir.display()),
198 )));
199 }
200
201 let meta: MetaJson = read_json(&dir.join("meta.json"))?;
203 if meta.version != IMAGE_VERSION {
204 return Err(SandlockError::Runtime(SandboxRuntimeError::Child(
205 format!("unsupported checkpoint image version {} (expected {})",
206 meta.version, IMAGE_VERSION),
207 )));
208 }
209
210 let policy_bytes = std::fs::read(dir.join("policy.dat"))
212 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
213 let policy: Sandbox = bincode::deserialize(&policy_bytes).map_err(io_err)?;
214
215 let app_state_path = dir.join("app_state.bin");
217 let app_state = if app_state_path.exists() {
218 Some(std::fs::read(&app_state_path)
219 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?)
220 } else {
221 None
222 };
223
224 let proc_dir = dir.join("process");
226
227 let info: InfoJson = read_json(&proc_dir.join("info.json"))?;
229
230 let fds_json: Vec<FdJson> = read_json(&proc_dir.join("fds.json"))?;
232 let fd_table: Vec<FdInfo> = fds_json.into_iter().map(|f| FdInfo {
233 fd: f.fd,
234 path: f.path,
235 flags: f.flags,
236 offset: f.offset,
237 }).collect();
238
239 let maps_json: Vec<MemoryMapJson> = read_json(&proc_dir.join("memory_map.json"))?;
241 let memory_maps: Vec<MemoryMap> = maps_json.iter().map(|m| MemoryMap {
242 start: m.start,
243 end: m.end,
244 perms: m.perms.clone(),
245 offset: m.offset,
246 path: m.path.clone(),
247 }).collect();
248
249 let threads_dir = proc_dir.join("threads");
251 let reg_bytes = std::fs::read(threads_dir.join("0.bin"))
252 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
253 let regs: Vec<u64> = reg_bytes.chunks_exact(8)
254 .map(|chunk| u64::from_le_bytes(chunk.try_into().unwrap()))
255 .collect();
256 let fpregs = std::fs::read(threads_dir.join("fpregs.bin")).unwrap_or_default();
258
259 let mem_dir = proc_dir.join("memory");
264 let mut memory_data = Vec::new();
265 for map in &maps_json {
266 let seg_path = mem_dir.join(format!("{:x}.bin", map.start));
267 if seg_path.exists() {
268 let data = std::fs::read(&seg_path)
269 .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
270 memory_data.push(MemorySegment {
271 start: map.start,
272 data,
273 });
274 }
275 }
276
277 Ok(Checkpoint {
278 name: meta.name,
279 policy,
280 process_state: ProcessState {
281 pid: info.pid,
282 cwd: info.cwd,
283 exe: info.exe,
284 regs,
285 fpregs,
286 memory_maps,
287 memory_data,
288 },
289 fd_table,
290 cow_snapshot: meta.cow_snapshot.map(PathBuf::from),
291 app_state,
292 })
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::Checkpoint;
299
300 #[test]
301 fn image_rejects_wrong_version() {
302 let dir = std::env::temp_dir().join(format!("sandlock-ver-{}", std::process::id()));
303 let _ = std::fs::remove_dir_all(&dir);
304 std::fs::create_dir_all(dir.join("process/threads")).unwrap();
305 std::fs::create_dir_all(dir.join("process/memory")).unwrap();
306 std::fs::write(dir.join("meta.json"),
307 br#"{"name":"x","cow_snapshot":null,"version":999}"#).unwrap();
308 let res = Checkpoint::load(&dir);
309 let _ = std::fs::remove_dir_all(&dir);
310 let msg = res.unwrap_err().to_string();
311 assert!(msg.contains("version"), "error should mention version, got: {msg}");
312 }
313}