Skip to main content

sandlock_core/checkpoint/
image.rs

1use 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
7// ---------------------------------------------------------------------------
8// Save / Load -- directory-based format
9// ---------------------------------------------------------------------------
10//
11// Layout:
12//   <dir>/
13//   ├── meta.json            # name, cow_snapshot
14//   ├── policy.dat           # bincode-serialized Sandbox
15//   ├── app_state.bin        # optional raw app state
16//   └── process/
17//       ├── info.json        # pid, cwd, exe
18//       ├── fds.json         # file descriptor table
19//       ├── memory_map.json  # FULL region metadata (every mapping)
20//       ├── threads/
21//       │   └── 0.bin        # raw register bytes (main thread)
22//       └── memory/
23//           └── <start_hex>.bin  # captured bytes for a region, keyed by its
24//                                # start address (only regions with data)
25//
26// `memory_map.json` lists *every* mapping (not just captured ones) so that
27// restore can `RemapFromFile` the file-backed regions (e.g. the executable
28// text) that were never captured into `memory/`. Each `memory/<start_hex>.bin`
29// is re-associated with its map entry by matching start address.
30
31const 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/// JSON schema for meta.json.
49#[derive(Serialize, Deserialize)]
50struct MetaJson {
51    name: String,
52    cow_snapshot: Option<String>,
53    #[serde(default)]
54    version: u32,
55}
56
57/// JSON schema for process/info.json.
58#[derive(Serialize, Deserialize)]
59struct InfoJson {
60    pid: i32,
61    cwd: String,
62    exe: String,
63}
64
65/// JSON schema for each entry in process/fds.json.
66#[derive(Serialize, Deserialize)]
67struct FdJson {
68    fd: i32,
69    path: String,
70    flags: i32,
71    offset: u64,
72}
73
74/// JSON schema for each entry in process/memory_map.json.
75#[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    /// Persist this checkpoint to a directory.
86    ///
87    /// Writes atomically: creates `<dir>.tmp`, populates it, then renames.
88    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        // Atomic rename into place
104        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        // meta.json
116        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        // policy.dat (bincode -- complex struct, not human-readable anyway)
123        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        // app_state.bin
128        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        // process/
134        let proc_dir = dir.join("process");
135        std::fs::create_dir(&proc_dir)
136            .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
137
138        // process/info.json
139        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        // process/fds.json
146        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        // process/memory_map.json -- FULL region metadata (every mapping), so
155        // restore can RemapFromFile the file-backed regions (e.g. the
156        // executable text) that were never captured into memory_data.
157        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        // process/threads/0.bin -- main thread register state
167        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"), &reg_bytes)
174            .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
175        // process/threads/fpregs.bin -- FPU/extended register state
176        std::fs::write(threads_dir.join("fpregs.bin"), &self.process_state.fpregs)
177            .map_err(|e| SandlockError::Runtime(SandboxRuntimeError::Io(e)))?;
178
179        // process/memory/<start_hex>.bin -- captured segment bytes, keyed by
180        // start address so load can re-associate each blob with its (full) map
181        // entry regardless of how many uncaptured maps sit between them.
182        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    /// Load a checkpoint from a directory.
194    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        // meta.json
202        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        // policy.dat
211        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        // app_state.bin
216        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        // process/
225        let proc_dir = dir.join("process");
226
227        // process/info.json
228        let info: InfoJson = read_json(&proc_dir.join("info.json"))?;
229
230        // process/fds.json
231        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        // process/memory_map.json -- FULL region list (every mapping).
240        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        // process/threads/0.bin
250        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        // Read FP/extended registers; tolerate absence defensively.
257        let fpregs = std::fs::read(threads_dir.join("fpregs.bin")).unwrap_or_default();
258
259        // process/memory/<start_hex>.bin -- captured segments only, matched to
260        // their map entry by start address. Maps without a blob (file-backed
261        // regions like the executable text) carry no data and are restored via
262        // RemapFromFile during resume.
263        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}