Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions crates/sandlock-core/src/checkpoint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,14 @@ pub struct FdInfo {
pub flags: i32,
pub offset: u64,
}

/// An fd that a restore could not transparently recreate (socket, pipe,
/// memfd, deleted or pseudo-filesystem path). The restored process runs
/// without it; such resources fall to the `app_state` hatch.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkippedFd {
/// The fd number in the checkpointed process.
pub fd: i32,
/// The resource the fd pointed at (e.g. `pipe:[12345]`, `/memfd:x`).
pub path: String,
}
33 changes: 18 additions & 15 deletions crates/sandlock-core/src/checkpoint/resume.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::checkpoint::{Checkpoint, FdInfo, MemoryMap, MemorySegment};
use crate::checkpoint::{Checkpoint, FdInfo, MemoryMap, MemorySegment, SkippedFd};

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

/// Split the saved fd table into transparently restorable regular files and a
/// list of skipped non-regular fds (sockets, pipes, eventfd, ...). The skipped
/// list is logged by the caller; those resources fall to the app_state hatch.
/// list is surfaced to the caller; those resources fall to the app_state hatch.
/// memfd, "(deleted)", and pseudo-filesystem (/proc/, /sys/, /dev/) paths start
/// with '/' but are not transparently reopenable, so they are skipped.
pub(crate) fn build_fd_plan(fds: &[FdInfo]) -> (Vec<FdInfo>, Vec<String>) {
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(f.path.clone());
skipped.push(SkippedFd { fd: f.fd, path: f.path.clone() });
}
}
(restorable, skipped)
Expand All @@ -83,8 +83,8 @@ fn prot_from_perms(perms: &str) -> libc::c_int {
/// a valid executable rip). Drives the rebuild entirely via ptrace syscall
/// injection through a trampoline placed in a hole of the CHECKPOINT's layout.
/// Leaves the child stopped with the saved registers loaded; the caller resumes
/// it (PTRACE_CONT / detach). Returns the list of non-transparently-restored
/// resource names (skipped fds) for the caller to log.
/// it (PTRACE_CONT / detach). Returns the non-transparently-restored fds
/// (as [`SkippedFd`] fd + path entries) for the caller to surface.
/// On `Err`, the child is left half-built and still ptrace-stopped; the caller
/// MUST kill and reap it.
/// Limitation: file-backed regions are restored `MAP_PRIVATE` from the on-disk
Expand All @@ -98,7 +98,7 @@ fn prot_from_perms(perms: &str) -> libc::c_int {
pub(crate) fn restore_into(
pid: i32,
cp: &Checkpoint,
) -> Result<Vec<String>, crate::error::SandlockError> {
) -> Result<Vec<SkippedFd>, crate::error::SandlockError> {
use crate::checkpoint::inject;
use crate::error::{SandboxRuntimeError, SandlockError};

Expand Down Expand Up @@ -316,7 +316,7 @@ pub(crate) fn restore_into(
pub(crate) fn restore_into(
_pid: i32,
_cp: &Checkpoint,
) -> Result<Vec<String>, crate::error::SandlockError> {
) -> 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(),
Expand All @@ -327,7 +327,7 @@ pub(crate) fn restore_into(
#[cfg(test)]
mod tests {
use super::*;
use crate::checkpoint::{FdInfo, MemoryMap, MemorySegment};
use crate::checkpoint::{FdInfo, MemoryMap, MemorySegment, SkippedFd};

#[test]
fn fd_plan_keeps_regular_files_only() {
Expand All @@ -339,7 +339,10 @@ mod tests {
let (restorable, skipped) = build_fd_plan(&fds);
assert_eq!(restorable.len(), 1);
assert_eq!(restorable[0].fd, 3);
assert_eq!(skipped, vec!["socket:[12345]".to_string(), "pipe:[6789]".to_string()]);
assert_eq!(skipped, vec![
SkippedFd { fd: 4, path: "socket:[12345]".into() },
SkippedFd { fd: 5, path: "pipe:[6789]".into() },
]);
}

#[test]
Expand All @@ -357,13 +360,13 @@ mod tests {
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(&"/tmp/gone (deleted)".to_string()));
assert!(skipped.contains(&"/memfd:scratch (deleted)".to_string()));
assert!(skipped.contains(&"/proc/1234/maps".to_string()),
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(&"/dev/pts/3".to_string()),
assert!(skipped.contains(&SkippedFd { fd: 9, path: "/dev/pts/3".into() }),
"/dev/ paths must be skipped");
assert!(skipped.contains(&"/sys/kernel/x".to_string()),
assert!(skipped.contains(&SkippedFd { fd: 10, path: "/sys/kernel/x".into() }),
"/sys/ paths must be skipped");
}

Expand Down
2 changes: 1 addition & 1 deletion crates/sandlock-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ mod transparent_proxy;

pub use error::SandlockError;
pub use sys::structs::{SeccompData, SeccompNotif};
pub use checkpoint::Checkpoint;
pub use checkpoint::{Checkpoint, SkippedFd};
pub use protection::{Protection, ProtectionState, ProtectionPolicy, ProtectionStatus};
pub use sandbox::{Confinement, ConfinementBuilder, Process, Sandbox, SandboxBuilder, StdioMode};
pub use result::{RunResult, ExitStatus};
Expand Down
28 changes: 23 additions & 5 deletions crates/sandlock-core/src/sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,11 @@ pub struct Sandbox {
// Heap-allocated runtime state; `None` when not started.
#[serde(skip)]
runtime: Option<Box<Runtime>>,

// Fds the last `restore_interactive` could not transparently recreate.
// Runtime state: not serialized, not cloned.
#[serde(skip)]
restore_skipped: Vec<crate::checkpoint::SkippedFd>,
}

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

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

Ok(skipped)
self.restore_skipped = skipped;
Ok(Process { sandbox: self })
}

/// Fds that the last [`Sandbox::restore_interactive`] on this sandbox could
/// not transparently recreate (sockets, pipes, memfds, pseudo-filesystem
/// paths); the restored process runs without them. Empty if this sandbox
/// never restored a checkpoint or every fd was restored.
pub fn restore_skipped(&self) -> &[crate::checkpoint::SkippedFd] {
&self.restore_skipped
}

/// Wait for the child to finish `execve`. Detected by `/proc/<pid>/exe`
Expand Down
1 change: 1 addition & 0 deletions crates/sandlock-core/src/sandbox/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,7 @@ impl SandboxBuilder {
init_fn: self.init_fn,
work_fn: self.work_fn,
runtime: None,
restore_skipped: Vec::new(),
})
}

Expand Down
7 changes: 4 additions & 3 deletions crates/sandlock-core/tests/integration/test_restore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,11 @@ async fn test_restore_real_program_resumes() {
// Sentinel: prove the *restored* process (not a leftover original) is writing.
std::fs::write(&counter, b"0\n").unwrap();

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

// Poll up to ~3s for the restored process to resume and advance the counter
// past the checkpointed baseline.
Expand Down
56 changes: 56 additions & 0 deletions crates/sandlock-ffi/include/sandlock.h
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,62 @@ const uint8_t *sandlock_checkpoint_app_state(const sandlock_checkpoint_t *cp, ui
*/
void sandlock_checkpoint_free(sandlock_checkpoint_t *cp);

/**
* Restore a checkpoint into a fresh, fully-sandboxed process.
*
* Builds a new sandbox from `policy` (with the optional `name`, as in
* `sandlock_create`) and injects the checkpoint image over a parked child,
* which resumes at the saved program counter under the full confinement.
* Returns a live handle: the process is already running, so do NOT call
* `sandlock_start` on it; manage it with `sandlock_handle_kill` /
* `sandlock_handle_wait` / `sandlock_handle_free` as usual. Returns NULL on
* error (any half-built child is reaped before returning).
*
* Fds that could not be transparently restored (sockets, pipes, memfds,
* pseudo-filesystem paths) are recorded on the handle; enumerate them with
* `sandlock_handle_restore_skipped_len` / `_fd` / `_path`.
*
* x86_64 restore engine only. Transparent restore currently holds for
* vDSO-free programs; see `Sandbox::restore_interactive` in sandlock-core.
*
* # Safety
* `policy` must be a valid policy pointer and `cp` a valid checkpoint
* pointer. `name` may be NULL to auto-generate a sandbox name, or a valid
* NUL-terminated string.
*/
sandlock_handle_t *sandlock_restore_interactive(const sandlock_sandbox_t *policy,
const char *name,
const sandlock_checkpoint_t *cp);

/**
* Number of fds the restore that produced this handle could not
* transparently recreate. 0 for a NULL handle or a handle not produced by
* `sandlock_restore_interactive`.
*
* # Safety
* `h` must be null or a valid handle.
*/
uintptr_t sandlock_handle_restore_skipped_len(const sandlock_handle_t *h);

/**
* The fd number of the i-th skipped entry (its fd in the checkpointed
* process). Returns -1 if `h` is NULL or `i` is out of range.
*
* # Safety
* `h` must be null or a valid handle.
*/
int sandlock_handle_restore_skipped_fd(const sandlock_handle_t *h, uintptr_t i);

/**
* The resource path of the i-th skipped entry (e.g. `pipe:[12345]`).
* Returns a malloc'd C string to free with `sandlock_string_free`, or NULL
* if `h` is NULL or `i` is out of range.
*
* # Safety
* `h` must be null or a valid handle.
*/
char *sandlock_handle_restore_skipped_path(const sandlock_handle_t *h, uintptr_t i);

/**
* Query the Landlock ABI version supported by the running kernel.
* Returns the ABI version (>= 1), or -1 if Landlock is unavailable.
Expand Down
Loading
Loading