Skip to content

Commit 5c9bafe

Browse files
committed
Expose restore_interactive through the C ABI and Python SDK
Signed-off-by: Cong Wang <cwang@multikernel.io>
1 parent ead123c commit 5c9bafe

15 files changed

Lines changed: 762 additions & 131 deletions

File tree

crates/sandlock-core/src/checkpoint/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,14 @@ pub struct FdInfo {
7171
pub flags: i32,
7272
pub offset: u64,
7373
}
74+
75+
/// An fd that a restore could not transparently recreate (socket, pipe,
76+
/// memfd, deleted or pseudo-filesystem path). The restored process runs
77+
/// without it; such resources fall to the `app_state` hatch.
78+
#[derive(Debug, Clone, PartialEq, Eq)]
79+
pub struct SkippedFd {
80+
/// The fd number in the checkpointed process.
81+
pub fd: i32,
82+
/// The resource the fd pointed at (e.g. `pipe:[12345]`, `/memfd:x`).
83+
pub path: String,
84+
}

crates/sandlock-core/src/checkpoint/resume.rs

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::checkpoint::{Checkpoint, FdInfo, MemoryMap, MemorySegment};
1+
use crate::checkpoint::{Checkpoint, FdInfo, MemoryMap, MemorySegment, SkippedFd};
22

33
/// One planned memory-restore action for a saved region.
44
#[derive(Debug)]
@@ -53,17 +53,17 @@ fn is_restorable_file_path(path: &str) -> bool {
5353

5454
/// Split the saved fd table into transparently restorable regular files and a
5555
/// list of skipped non-regular fds (sockets, pipes, eventfd, ...). The skipped
56-
/// list is logged by the caller; those resources fall to the app_state hatch.
56+
/// list is surfaced to the caller; those resources fall to the app_state hatch.
5757
/// memfd, "(deleted)", and pseudo-filesystem (/proc/, /sys/, /dev/) paths start
5858
/// with '/' but are not transparently reopenable, so they are skipped.
59-
pub(crate) fn build_fd_plan(fds: &[FdInfo]) -> (Vec<FdInfo>, Vec<String>) {
59+
pub(crate) fn build_fd_plan(fds: &[FdInfo]) -> (Vec<FdInfo>, Vec<SkippedFd>) {
6060
let mut restorable = Vec::new();
6161
let mut skipped = Vec::new();
6262
for f in fds {
6363
if is_restorable_file_path(&f.path) {
6464
restorable.push(f.clone());
6565
} else {
66-
skipped.push(f.path.clone());
66+
skipped.push(SkippedFd { fd: f.fd, path: f.path.clone() });
6767
}
6868
}
6969
(restorable, skipped)
@@ -83,8 +83,8 @@ fn prot_from_perms(perms: &str) -> libc::c_int {
8383
/// a valid executable rip). Drives the rebuild entirely via ptrace syscall
8484
/// injection through a trampoline placed in a hole of the CHECKPOINT's layout.
8585
/// Leaves the child stopped with the saved registers loaded; the caller resumes
86-
/// it (PTRACE_CONT / detach). Returns the list of non-transparently-restored
87-
/// resource names (skipped fds) for the caller to log.
86+
/// it (PTRACE_CONT / detach). Returns the non-transparently-restored fds
87+
/// (as [`SkippedFd`] fd + path entries) for the caller to surface.
8888
/// On `Err`, the child is left half-built and still ptrace-stopped; the caller
8989
/// MUST kill and reap it.
9090
/// Limitation: file-backed regions are restored `MAP_PRIVATE` from the on-disk
@@ -98,7 +98,7 @@ fn prot_from_perms(perms: &str) -> libc::c_int {
9898
pub(crate) fn restore_into(
9999
pid: i32,
100100
cp: &Checkpoint,
101-
) -> Result<Vec<String>, crate::error::SandlockError> {
101+
) -> Result<Vec<SkippedFd>, crate::error::SandlockError> {
102102
use crate::checkpoint::inject;
103103
use crate::error::{SandboxRuntimeError, SandlockError};
104104

@@ -316,7 +316,7 @@ pub(crate) fn restore_into(
316316
pub(crate) fn restore_into(
317317
_pid: i32,
318318
_cp: &Checkpoint,
319-
) -> Result<Vec<String>, crate::error::SandlockError> {
319+
) -> Result<Vec<SkippedFd>, crate::error::SandlockError> {
320320
Err(crate::error::SandlockError::Runtime(
321321
crate::error::SandboxRuntimeError::Child(
322322
"injection-based restore is only implemented on x86_64".into(),
@@ -327,7 +327,7 @@ pub(crate) fn restore_into(
327327
#[cfg(test)]
328328
mod tests {
329329
use super::*;
330-
use crate::checkpoint::{FdInfo, MemoryMap, MemorySegment};
330+
use crate::checkpoint::{FdInfo, MemoryMap, MemorySegment, SkippedFd};
331331

332332
#[test]
333333
fn fd_plan_keeps_regular_files_only() {
@@ -339,7 +339,10 @@ mod tests {
339339
let (restorable, skipped) = build_fd_plan(&fds);
340340
assert_eq!(restorable.len(), 1);
341341
assert_eq!(restorable[0].fd, 3);
342-
assert_eq!(skipped, vec!["socket:[12345]".to_string(), "pipe:[6789]".to_string()]);
342+
assert_eq!(skipped, vec![
343+
SkippedFd { fd: 4, path: "socket:[12345]".into() },
344+
SkippedFd { fd: 5, path: "pipe:[6789]".into() },
345+
]);
343346
}
344347

345348
#[test]
@@ -357,13 +360,13 @@ mod tests {
357360
assert_eq!(restorable[0].fd, 3);
358361
assert!(restorable.iter().all(|f| f.fd != 6 && f.fd != 7 && f.fd != 8 && f.fd != 9 && f.fd != 10),
359362
"deleted, memfd, and pseudo-filesystem fds must not appear in restorable");
360-
assert!(skipped.contains(&"/tmp/gone (deleted)".to_string()));
361-
assert!(skipped.contains(&"/memfd:scratch (deleted)".to_string()));
362-
assert!(skipped.contains(&"/proc/1234/maps".to_string()),
363+
assert!(skipped.contains(&SkippedFd { fd: 6, path: "/tmp/gone (deleted)".into() }));
364+
assert!(skipped.contains(&SkippedFd { fd: 7, path: "/memfd:scratch (deleted)".into() }));
365+
assert!(skipped.contains(&SkippedFd { fd: 8, path: "/proc/1234/maps".into() }),
363366
"/proc/ paths must be skipped");
364-
assert!(skipped.contains(&"/dev/pts/3".to_string()),
367+
assert!(skipped.contains(&SkippedFd { fd: 9, path: "/dev/pts/3".into() }),
365368
"/dev/ paths must be skipped");
366-
assert!(skipped.contains(&"/sys/kernel/x".to_string()),
369+
assert!(skipped.contains(&SkippedFd { fd: 10, path: "/sys/kernel/x".into() }),
367370
"/sys/ paths must be skipped");
368371
}
369372

crates/sandlock-core/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ mod transparent_proxy;
3333

3434
pub use error::SandlockError;
3535
pub use sys::structs::{SeccompData, SeccompNotif};
36-
pub use checkpoint::Checkpoint;
36+
pub use checkpoint::{Checkpoint, SkippedFd};
3737
pub use protection::{Protection, ProtectionState, ProtectionPolicy, ProtectionStatus};
3838
pub use sandbox::{Confinement, ConfinementBuilder, Process, Sandbox, SandboxBuilder, StdioMode};
3939
pub use result::{RunResult, ExitStatus};

crates/sandlock-core/src/sandbox.rs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,11 @@ pub struct Sandbox {
438438
// Heap-allocated runtime state; `None` when not started.
439439
#[serde(skip)]
440440
runtime: Option<Box<Runtime>>,
441+
442+
// Fds the last `restore_interactive` could not transparently recreate.
443+
// Runtime state: not serialized, not cloned.
444+
#[serde(skip)]
445+
restore_skipped: Vec<crate::checkpoint::SkippedFd>,
441446
}
442447

443448
impl std::fmt::Debug for Sandbox {
@@ -521,6 +526,8 @@ impl Clone for Sandbox {
521526
work_fn: self.work_fn.clone(),
522527
// Runtime is NOT cloned — the clone starts with no runtime.
523528
runtime: None,
529+
// Restore diagnostics belong to the original's run, not the clone.
530+
restore_skipped: Vec::new(),
524531
}
525532
}
526533
}
@@ -818,8 +825,10 @@ impl Sandbox {
818825
/// full notify stack in place (the child parks before execve), then takes the
819826
/// parked child over with ptrace and injects the checkpoint image over it via
820827
/// `restore_into`, resuming it at the saved program counter. The process comes
821-
/// up already sandboxed. Returns the list of non-transparently-restored fd
822-
/// paths (skipped; the caller should log them). x86_64 restore engine only.
828+
/// up already sandboxed and running; like [`Sandbox::popen`], the returned
829+
/// [`Process`] is the handle to it (no `start()` step). Fds that could not be
830+
/// transparently recreated are recorded on this `Sandbox`; query them with
831+
/// [`Sandbox::restore_skipped`]. x86_64 restore engine only.
823832
///
824833
/// Limitation: transparent restore currently works for vDSO-free programs.
825834
/// libc/glibc programs that call vDSO functions (e.g. `clock_gettime`) crash
@@ -831,7 +840,7 @@ impl Sandbox {
831840
pub async fn restore_interactive(
832841
&mut self,
833842
cp: &crate::checkpoint::Checkpoint,
834-
) -> Result<Vec<String>, crate::error::SandlockError> {
843+
) -> Result<Process<'_>, crate::error::SandlockError> {
835844
use crate::error::SandboxRuntimeError;
836845

837846
// The exe to launch is the checkpoint's original binary (within the
@@ -856,7 +865,7 @@ impl Sandbox {
856865
// restore_into reads only process_state + fd_table, never policy.
857866
let cp = cp.clone();
858867
let skipped = tokio::task::spawn_blocking(
859-
move || -> Result<Vec<String>, crate::error::SandlockError> {
868+
move || -> Result<Vec<crate::checkpoint::SkippedFd>, crate::error::SandlockError> {
860869
// PTRACE_SEIZE + PTRACE_INTERRUPT + waitpid to reach the ptrace-stop.
861870
crate::checkpoint::capture::ptrace_seize(pid).map_err(|e| {
862871
SandboxRuntimeError::Child(format!("restore ptrace seize {pid}: {e}"))
@@ -884,7 +893,16 @@ impl Sandbox {
884893
.await
885894
.map_err(|e| SandboxRuntimeError::Child(format!("restore join error: {e}")))??;
886895

887-
Ok(skipped)
896+
self.restore_skipped = skipped;
897+
Ok(Process { sandbox: self })
898+
}
899+
900+
/// Fds that the last [`Sandbox::restore_interactive`] on this sandbox could
901+
/// not transparently recreate (sockets, pipes, memfds, pseudo-filesystem
902+
/// paths); the restored process runs without them. Empty if this sandbox
903+
/// never restored a checkpoint or every fd was restored.
904+
pub fn restore_skipped(&self) -> &[crate::checkpoint::SkippedFd] {
905+
&self.restore_skipped
888906
}
889907

890908
/// Wait for the child to finish `execve`. Detected by `/proc/<pid>/exe`

crates/sandlock-core/src/sandbox/builder.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,7 @@ impl SandboxBuilder {
782782
init_fn: self.init_fn,
783783
work_fn: self.work_fn,
784784
runtime: None,
785+
restore_skipped: Vec::new(),
785786
})
786787
}
787788

crates/sandlock-core/tests/integration/test_restore.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,11 @@ async fn test_restore_real_program_resumes() {
112112
// Sentinel: prove the *restored* process (not a leftover original) is writing.
113113
std::fs::write(&counter, b"0\n").unwrap();
114114

115-
// Restore into a fresh, fully-sandboxed process.
115+
// Restore into a fresh, fully-sandboxed process. The returned Process is
116+
// the handle; the sandbox owns the child, so dropping it here is fine.
116117
let mut sb2 = policy.clone().with_name("restore-dst");
117-
let skipped = sb2.restore_interactive(&cp).await.unwrap();
118-
eprintln!("restore skipped fds: {skipped:?}");
118+
let _ = sb2.restore_interactive(&cp).await.unwrap();
119+
eprintln!("restore skipped fds: {:?}", sb2.restore_skipped());
119120

120121
// Poll up to ~3s for the restored process to resume and advance the counter
121122
// past the checkpointed baseline.

crates/sandlock-ffi/include/sandlock.h

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,62 @@ const uint8_t *sandlock_checkpoint_app_state(const sandlock_checkpoint_t *cp, ui
12311231
*/
12321232
void sandlock_checkpoint_free(sandlock_checkpoint_t *cp);
12331233

1234+
/**
1235+
* Restore a checkpoint into a fresh, fully-sandboxed process.
1236+
*
1237+
* Builds a new sandbox from `policy` (with the optional `name`, as in
1238+
* `sandlock_create`) and injects the checkpoint image over a parked child,
1239+
* which resumes at the saved program counter under the full confinement.
1240+
* Returns a live handle: the process is already running, so do NOT call
1241+
* `sandlock_start` on it; manage it with `sandlock_handle_kill` /
1242+
* `sandlock_handle_wait` / `sandlock_handle_free` as usual. Returns NULL on
1243+
* error (any half-built child is reaped before returning).
1244+
*
1245+
* Fds that could not be transparently restored (sockets, pipes, memfds,
1246+
* pseudo-filesystem paths) are recorded on the handle; enumerate them with
1247+
* `sandlock_handle_restore_skipped_len` / `_fd` / `_path`.
1248+
*
1249+
* x86_64 restore engine only. Transparent restore currently holds for
1250+
* vDSO-free programs; see `Sandbox::restore_interactive` in sandlock-core.
1251+
*
1252+
* # Safety
1253+
* `policy` must be a valid policy pointer and `cp` a valid checkpoint
1254+
* pointer. `name` may be NULL to auto-generate a sandbox name, or a valid
1255+
* NUL-terminated string.
1256+
*/
1257+
sandlock_handle_t *sandlock_restore_interactive(const sandlock_sandbox_t *policy,
1258+
const char *name,
1259+
const sandlock_checkpoint_t *cp);
1260+
1261+
/**
1262+
* Number of fds the restore that produced this handle could not
1263+
* transparently recreate. 0 for a NULL handle or a handle not produced by
1264+
* `sandlock_restore_interactive`.
1265+
*
1266+
* # Safety
1267+
* `h` must be null or a valid handle.
1268+
*/
1269+
uintptr_t sandlock_handle_restore_skipped_len(const sandlock_handle_t *h);
1270+
1271+
/**
1272+
* The fd number of the i-th skipped entry (its fd in the checkpointed
1273+
* process). Returns -1 if `h` is NULL or `i` is out of range.
1274+
*
1275+
* # Safety
1276+
* `h` must be null or a valid handle.
1277+
*/
1278+
int sandlock_handle_restore_skipped_fd(const sandlock_handle_t *h, uintptr_t i);
1279+
1280+
/**
1281+
* The resource path of the i-th skipped entry (e.g. `pipe:[12345]`).
1282+
* Returns a malloc'd C string to free with `sandlock_string_free`, or NULL
1283+
* if `h` is NULL or `i` is out of range.
1284+
*
1285+
* # Safety
1286+
* `h` must be null or a valid handle.
1287+
*/
1288+
char *sandlock_handle_restore_skipped_path(const sandlock_handle_t *h, uintptr_t i);
1289+
12341290
/**
12351291
* Query the Landlock ABI version supported by the running kernel.
12361292
* Returns the ABI version (>= 1), or -1 if Landlock is unavailable.

0 commit comments

Comments
 (0)