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
3 changes: 2 additions & 1 deletion crates/sandlock-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,8 @@ async fn run_command(args: RunArgs) -> Result<()> {
let _ = network_registry::update_ports(&reg_name, ports.clone());
});

policy.spawn(&cmd_strs).await?;
policy.create_interactive(&cmd_strs).await?;
policy.start()?;

let pid = policy.pid().unwrap_or(0);
let registered_hosts: Vec<String> = policy
Expand Down
9 changes: 6 additions & 3 deletions crates/sandlock-core/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,9 @@ async fn run_pipeline(stages: Vec<Stage>) -> Result<RunResult, SandlockError> {
};

let cmd_refs: Vec<&str> = stage.args.iter().map(|s| s.as_str()).collect();
sb.spawn_with_io(&cmd_refs, stdin_fd, stdout_fd, stderr_fd)
sb.create_with_io(&cmd_refs, stdin_fd, stdout_fd, stderr_fd)
.await?;
sb.start()?;

sandboxes.push(sb);
}
Expand Down Expand Up @@ -339,7 +340,8 @@ async fn run_gather(
let mut sb = ns.stage.sandbox.clone().with_name(name);
let stdout_fd = source_pipes[i].1.as_raw_fd();
let cmd_refs: Vec<&str> = ns.stage.args.iter().map(|s| s.as_str()).collect();
sb.spawn_with_io(&cmd_refs, None, Some(stdout_fd), None).await?;
sb.create_with_io(&cmd_refs, None, Some(stdout_fd), None).await?;
sb.start()?;
sandboxes.push(sb);
}

Expand All @@ -366,13 +368,14 @@ async fn run_gather(
}

let cmd_refs: Vec<&str> = consumer.args.iter().map(|s| s.as_str()).collect();
consumer_sb.spawn_with_gather_io(
consumer_sb.create_with_gather_io(
&cmd_refs,
Some(stdin_fd),
Some(cap_stdout_w.as_raw_fd()),
Some(cap_stderr_w.as_raw_fd()),
extra_fds,
).await?;
consumer_sb.start()?;
sandboxes.push(consumer_sb);

// Close pipe ends in parent
Expand Down
102 changes: 71 additions & 31 deletions crates/sandlock-core/src/sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ struct Runtime {
#[allow(clippy::type_complexity)]
on_bind: Option<Box<dyn Fn(&HashMap<u16, u16>) + Send + Sync>>,
extra_handlers: Vec<(i64, Arc<dyn crate::seccomp::dispatch::Handler>)>,
ready_w: Option<std::os::fd::OwnedFd>,
}

/// Lifecycle state for the runtime.
Expand Down Expand Up @@ -949,22 +950,31 @@ impl Sandbox {
Ok(RunResult { exit_status, stdout, stderr })
}

/// Spawn a sandboxed process without waiting for it to exit.
/// Use `wait()` to collect the exit status when done.
#[doc(hidden)]
pub async fn spawn(&mut self, cmd: &[&str]) -> Result<(), crate::error::SandlockError> {
self.do_spawn(cmd, false).await
/// Fork the sandboxed child and install policy (seccomp + notif
/// supervisor + rlimits + landlock + COW + network/HTTP proxies).
/// The child is parked between policy install and `execve`; call
/// `start()` to release it. Stdout/stderr are captured for later
/// retrieval via `wait()`.
pub async fn create(&mut self, cmd: &[&str]) -> Result<(), crate::error::SandlockError> {
self.do_create(cmd, true).await
}

/// Like `spawn` but captures stdout and stderr (available via `wait()`).
#[doc(hidden)]
pub async fn spawn_captured(&mut self, cmd: &[&str]) -> Result<(), crate::error::SandlockError> {
self.do_spawn(cmd, true).await
/// Like `create` but inherits stdio (no capture).
pub async fn create_interactive(&mut self, cmd: &[&str]) -> Result<(), crate::error::SandlockError> {
self.do_create(cmd, false).await
}

/// Spawn with explicit stdin/stdout/stderr fd redirection.
/// Release a previously `create()`d child to `execve` the configured
/// command. Returns immediately; use `wait()` to collect the exit
/// status when the child finishes.
pub fn start(&mut self) -> Result<(), crate::error::SandlockError> {
self.do_start()
}

/// Create with explicit stdin/stdout/stderr fd redirection. Child is
/// parked after policy install; call `start()` to release.
#[doc(hidden)]
pub async fn spawn_with_io(
pub async fn create_with_io(
&mut self,
cmd: &[&str],
stdin_fd: Option<std::os::unix::io::RawFd>,
Expand All @@ -973,12 +983,12 @@ impl Sandbox {
) -> Result<(), crate::error::SandlockError> {
self.ensure_runtime()?;
self.rt_mut().io_overrides = Some((stdin_fd, stdout_fd, stderr_fd));
self.do_spawn(cmd, false).await
self.do_create(cmd, false).await
}

/// Like `spawn_with_io` but also maps extra fds into the child.
/// Like `create_with_io` but also maps extra fds into the child.
#[doc(hidden)]
pub async fn spawn_with_gather_io(
pub async fn create_with_gather_io(
&mut self,
cmd: &[&str],
stdin_fd: Option<std::os::unix::io::RawFd>,
Expand All @@ -989,7 +999,7 @@ impl Sandbox {
self.ensure_runtime()?;
self.rt_mut().io_overrides = Some((stdin_fd, stdout_fd, stderr_fd));
self.rt_mut().extra_fds = extra_fds;
self.do_spawn(cmd, false).await
self.do_create(cmd, false).await
}

/// Commit COW writes to the original directory.
Expand Down Expand Up @@ -1073,7 +1083,8 @@ impl Sandbox {
&mut self,
cmd: &[&str],
) -> Result<crate::result::RunResult, crate::error::SandlockError> {
self.do_spawn(cmd, true).await?;
self.do_create(cmd, true).await?;
self.do_start()?;
self.wait().await
}

Expand All @@ -1082,7 +1093,8 @@ impl Sandbox {
&mut self,
cmd: &[&str],
) -> Result<crate::result::RunResult, crate::error::SandlockError> {
self.do_spawn(cmd, false).await?;
self.do_create(cmd, false).await?;
self.do_start()?;
self.wait().await
}

Expand All @@ -1100,7 +1112,8 @@ impl Sandbox {
let pending = sandbox_collect_extra_handlers(extra_handlers, self)?;
self.ensure_runtime()?;
self.rt_mut().extra_handlers = pending;
self.do_spawn(cmd, true).await?;
self.do_create(cmd, true).await?;
self.do_start()?;
self.wait().await
}

Expand All @@ -1118,18 +1131,20 @@ impl Sandbox {
let pending = sandbox_collect_extra_handlers(extra_handlers, self)?;
self.ensure_runtime()?;
self.rt_mut().extra_handlers = pending;
self.do_spawn(cmd, false).await?;
self.do_create(cmd, false).await?;
self.do_start()?;
self.wait().await
}

/// Dry-run: spawn, wait, collect filesystem changes, then abort.
/// Dry-run: create, start, wait, collect filesystem changes, then abort.
pub async fn dry_run(
&mut self,
cmd: &[&str],
) -> Result<crate::dry_run::DryRunResult, crate::error::SandlockError> {
self.on_exit = BranchAction::Keep;
self.on_error = BranchAction::Keep;
self.do_spawn(cmd, true).await?;
self.do_create(cmd, true).await?;
self.do_start()?;
let run_result = self.wait().await?;
let changes = self.collect_changes().await;
self.do_abort().await;
Expand All @@ -1143,7 +1158,8 @@ impl Sandbox {
) -> Result<crate::dry_run::DryRunResult, crate::error::SandlockError> {
self.on_exit = BranchAction::Keep;
self.on_error = BranchAction::Keep;
self.do_spawn(cmd, false).await?;
self.do_create(cmd, false).await?;
self.do_start()?;
let run_result = self.wait().await?;
let changes = self.collect_changes().await;
self.do_abort().await;
Expand Down Expand Up @@ -1288,6 +1304,7 @@ impl Sandbox {
http_acl_handle: None,
on_bind: None,
extra_handlers: Vec::new(),
ready_w: None,
}));
clones.push(clone_sb);
}
Expand Down Expand Up @@ -1332,7 +1349,8 @@ impl Sandbox {
let mut reducer = self.clone().with_name(reducer_name);
reducer.ensure_runtime()?;
reducer.rt_mut().io_overrides = Some((Some(stdin_fds[0]), None, None));
reducer.do_spawn(cmd, true).await?;
reducer.do_create(cmd, true).await?;
reducer.do_start()?;
unsafe { libc::close(stdin_fds[0]) };

let _ = write_handle.await;
Expand Down Expand Up @@ -1371,6 +1389,7 @@ impl Sandbox {
http_acl_handle: None,
on_bind: None,
extra_handlers: Vec::new(),
ready_w: None,
}));
Ok(())
}
Expand Down Expand Up @@ -1403,14 +1422,15 @@ impl Sandbox {
}

// ================================================================
// Internal: do_spawn (the main fork/confinement entry point)
// Internal: do_create (fork + policy install; child parks at the
// ready_r read, awaiting do_start to release it to execve).
// ================================================================

async fn do_spawn(&mut self, cmd: &[&str], capture: bool) -> Result<(), crate::error::SandlockError> {
async fn do_create(&mut self, cmd: &[&str], capture: bool) -> Result<(), crate::error::SandlockError> {
use std::ffi::CString;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use crate::error::SandboxRuntimeError;
use crate::context::{PipePair, read_u32_fd, write_u32_fd};
use crate::context::{PipePair, read_u32_fd};
use crate::cow::{CowBranch, overlayfs::OverlayBranch, branchfs::BranchFsBranch};
use crate::network;
use crate::seccomp::ctx::SupervisorCtx;
Expand Down Expand Up @@ -1563,7 +1583,8 @@ impl Sandbox {
self.rt_mut()._stderr_read = stderr_r.map(|(r, _w)| r);

self.rt_mut().child_pid = Some(pid);
self.rt_mut().state = RuntimeState::Running;
// State remains `Created` until `do_start` writes ready_w to release
// the child to execve.

let pidfd = match syscall::pidfd_open(pid as u32, 0) {
Ok(fd) => Some(fd),
Expand Down Expand Up @@ -1788,11 +1809,30 @@ impl Sandbox {
}
}

write_u32_fd(pipes.ready_w.as_raw_fd(), 1)
.map_err(|e| SandboxRuntimeError::Child(format!("write ready signal: {}", e)))?;

self.rt_mut().pidfd = pidfd;
self.rt_mut().ready_w = Some(pipes.ready_w);

Ok(())
}

// ================================================================
// Internal: do_start (release the parked child to execve)
// ================================================================

fn do_start(&mut self) -> Result<(), crate::error::SandlockError> {
use std::os::fd::AsRawFd;
use crate::context::write_u32_fd;
use crate::error::SandboxRuntimeError;

if !matches!(self.rt().state, RuntimeState::Created) {
return Err(SandboxRuntimeError::Child("start() requires a created sandbox".into()).into());
}
let ready_w = self.rt_mut().ready_w.take()
.ok_or_else(|| SandboxRuntimeError::Child("start() called without a prior create()".into()))?;
write_u32_fd(ready_w.as_raw_fd(), 1)
.map_err(|e| SandboxRuntimeError::Child(format!("write ready signal: {}", e)))?;
drop(ready_w);
self.rt_mut().state = RuntimeState::Running;
Ok(())
}
}
Expand All @@ -1805,7 +1845,7 @@ impl Drop for Sandbox {
fn drop(&mut self) {
if let Some(ref mut rt) = self.runtime {
if let Some(pid) = rt.child_pid {
if matches!(rt.state, RuntimeState::Running | RuntimeState::Paused) {
if matches!(rt.state, RuntimeState::Created | RuntimeState::Running | RuntimeState::Paused) {
unsafe { libc::killpg(pid, libc::SIGKILL) };
let mut status: i32 = 0;
unsafe { libc::waitpid(pid, &mut status, 0) };
Expand Down
15 changes: 10 additions & 5 deletions crates/sandlock-core/tests/integration/test_checkpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ async fn test_checkpoint_save_load() {
let mut sb = policy.clone().with_name("test");
// We need to spawn something that stays alive long enough to checkpoint
// Use "sleep 60" — we'll kill it after checkpoint
sb.spawn(&["sleep", "60"]).await.unwrap();
sb.create_interactive(&["sleep", "60"]).await.unwrap();
sb.start().unwrap();

// Give it a moment to start
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
Expand Down Expand Up @@ -58,7 +59,8 @@ async fn test_checkpoint_memory_maps() {
.build().unwrap();

let mut sb = policy.clone().with_name("test");
sb.spawn(&["sleep", "60"]).await.unwrap();
sb.create_interactive(&["sleep", "60"]).await.unwrap();
sb.start().unwrap();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;

let cp = sb.checkpoint().await.unwrap();
Expand All @@ -85,7 +87,8 @@ async fn test_checkpoint_app_state_roundtrip() {
.build().unwrap();

let mut sb = policy.clone().with_name("test");
sb.spawn(&["sleep", "60"]).await.unwrap();
sb.create_interactive(&["sleep", "60"]).await.unwrap();
sb.start().unwrap();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;

let mut cp = sb.checkpoint().await.unwrap();
Expand Down Expand Up @@ -118,7 +121,8 @@ async fn test_checkpoint_no_app_state_file() {
.build().unwrap();

let mut sb = policy.clone().with_name("test");
sb.spawn(&["sleep", "60"]).await.unwrap();
sb.create_interactive(&["sleep", "60"]).await.unwrap();
sb.start().unwrap();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;

let cp = sb.checkpoint().await.unwrap();
Expand All @@ -145,7 +149,8 @@ async fn test_checkpoint_process_info() {
.build().unwrap();

let mut sb = policy.clone().with_name("test");
sb.spawn(&["sleep", "60"]).await.unwrap();
sb.create_interactive(&["sleep", "60"]).await.unwrap();
sb.start().unwrap();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;

let expected_pid = sb.pid().unwrap();
Expand Down
6 changes: 4 additions & 2 deletions crates/sandlock-core/tests/integration/test_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,8 @@ async fn test_spawn_and_kill() {
let policy = base_policy().build().unwrap();
let mut sb = policy.clone().with_name("test");

sb.spawn(&["sleep", "300"]).await.unwrap();
sb.create_interactive(&["sleep", "300"]).await.unwrap();
sb.start().unwrap();
sb.kill().unwrap();

let result = sb.wait().await.unwrap();
Expand Down Expand Up @@ -326,7 +327,8 @@ async fn test_pause_resume() {
let policy = base_policy().build().unwrap();
let mut sb = policy.clone().with_name("test");

sb.spawn(&["sleep", "300"]).await.unwrap();
sb.create_interactive(&["sleep", "300"]).await.unwrap();
sb.start().unwrap();

sb.pause().expect("pause should succeed");
sb.resume().expect("resume should succeed");
Expand Down
3 changes: 2 additions & 1 deletion crates/sandlock-core/tests/integration/test_sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ async fn test_nested_sandbox() {

// Spawn outer, then nest inner inside it
let mut outer_sb = outer.clone().with_name("test");
outer_sb.spawn(&["sleep", "10"]).await.unwrap();
outer_sb.create_interactive(&["sleep", "10"]).await.unwrap();
outer_sb.start().unwrap();

// The inner sandbox runs in the same parent process context —
// Landlock from the outer is NOT applied to the parent, only to
Expand Down
Loading
Loading