sandbox-command 0.1.0

An asynchronous `Command` like process launcher that uses `bwrap` or `sandbox-exec` for controlled sandbox process execution.
Documentation
  • Coverage
  • 17.14%
    6 out of 35 items documented0 out of 28 items with examples
  • Size
  • Source code size: 31.6 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 184.1 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 8s Average build duration of successful builds.
  • all releases: 8s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • paul-english/sandbox-command
    0 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • paul-english

sandbox-command

sandbox-command is an asynchronous Rust Command like process launcher that utilizes bwrap (linux) or sandbox-exec (osx) for launching child processes in a controlled sandbox environment. Useful for running untrusted code, tools, or for use with agents.

Requirements

  • A supported host operating system:
    • Linux with bwrap installed and usable by the current user.
    • macOS with /usr/bin/sandbox-exec available.

The platform sandbox executable is detected the first time SandboxCommand is initialized or spawned. The result is cached for the lifetime of the process.

Installing Bubblewrap on Linux

Install Bubblewrap using your distribution's package manager. For example:

# Debian or Ubuntu
sudo apt-get install bubblewrap

# Fedora
sudo dnf install bubblewrap

# Arch
pacman -S bubblewrap

Bubblewrap also depends on the host's user-namespace configuration. If the executable is installed but spawning fails, check that unprivileged user namespaces are enabled and that the system permits Bubblewrap to create a sandbox.

Usage

Run a command and collect its output

SandboxCommand configures the child, and SandboxChild::wait_with_output collects both output streams asynchronously. Use Stdio::piped() for any stream that should be captured.

use std::process::Stdio;

use sandbox_command::SandboxCommand;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    SandboxCommand::initialize()?;

    let mut command = SandboxCommand::new("/usr/bin/printf");
    command
        .arg("hello from the sandbox\n")
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());

    let output = command.spawn()?.wait_with_output().await?;

    if !output.status.success() {
        return Err(format!("command exited with {}", output.status).into());
    }

    print!("{}", String::from_utf8_lossy(&output.stdout));
    eprintln!("{}", String::from_utf8_lossy(&output.stderr));
    Ok(())
}

initialize is optional. Calling it during application startup is useful when you want to report an unavailable sandbox before doing other work; spawn performs the same platform check if initialization was skipped.

Set the working directory and filesystem access

The working directory is canonicalized before spawning. It is writable by default. On Linux, bind_ro and bind_rw add paths at their existing locations inside the sandbox. On macOS, the working directory and paths passed to bind_rw are granted write access; the sandbox profile already permits reads required to execute the command.

use std::{path::Path, process::Stdio};

use sandbox_command::SandboxCommand;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let workspace = Path::new("/tmp/job");
    let cache = Path::new("/tmp/tool-cache");

    let mut command = SandboxCommand::new("/usr/bin/env");
    command
        .current_dir(workspace)
        .bind_rw(cache)
        .bind_ro("/usr/local/bin")
        .stdout(Stdio::piped());

    let output = command.spawn()?.wait_with_output().await?;
    println!("{}", String::from_utf8_lossy(&output.stdout));
    Ok(())
}

Paths supplied to current_dir, bind_ro, and bind_rw should exist and be accessible to the parent process. The library does not create missing directories or copy files into the sandbox.

Stream output or interact with a child

spawn returns a SandboxChild instead of immediately waiting. Take the piped handles when you need to process output incrementally or send input to the child.

use std::process::Stdio;

use sandbox_command::SandboxCommand;
use tokio::io::AsyncReadExt;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut command = SandboxCommand::new("/usr/bin/printf");
    command.arg("streamed output").stdout(Stdio::piped());

    let mut child = command.spawn()?;
    let mut stdout = child.take_stdout().expect("stdout was piped");
    let mut output = String::new();
    stdout.read_to_string(&mut output).await?;
    let status = child.wait().await?;

    println!("{status}: {output}");
    Ok(())
}

Other child-management operations include:

let process_id = child.id();
let status_if_finished = child.try_wait()?;
child.start_kill()?;       // request termination without awaiting it
child.kill().await?;       // terminate and await the kill operation

When using wait_with_output, the parent's stdin handle is closed automatically so a child waiting for EOF does not deadlock output collection.

Configure environment variables and standard I/O

The builder supports the usual environment operations. env_clear removes the inherited environment before applying later env or envs values. Standard streams use Tokio's Stdio configuration.

use std::process::Stdio;

let mut command = SandboxCommand::new("/usr/bin/my-tool");
command
    .env_clear()
    .env("HOME", "/tmp/sandbox-home")
    .envs([(String::from("MODE"), String::from("check"))])
    .env_remove("SECRET_TOKEN")
    .stdin(Stdio::null())
    .stdout(Stdio::piped())
    .stderr(Stdio::piped())
    .kill_on_drop(true);

API overview

SandboxCommand

Method Purpose
new(program) Create a command for the executable path or name.
arg(value), args(values) Add command-line arguments.
env(key, value), envs(values) Set environment variables.
env_remove(key), env_clear() Remove selected or all inherited environment variables.
current_dir(path) Set and canonicalize the child's working directory.
stdin(cfg), stdout(cfg), stderr(cfg) Configure standard I/O.
kill_on_drop(kill) Pass Tokio's kill-on-drop setting to the child process.
bind_ro(path) Add a read-only path binding on Linux.
bind_rw(path) Add a writable path binding.
spawn() Launch the sandboxed process.

SandboxChild

SandboxChild exposes the child's optional stdin, stdout, and stderr handles and provides take_stdin, take_stdout, and take_stderr helpers. It also provides id, wait, try_wait, kill, start_kill, and wait_with_output.

The child owns any temporary macOS sandbox profile created for the process. The profile is removed after the child exits, fails to spawn, or is dropped.

Errors

Operations return SandboxCommandError for library-level failures:

Variant Meaning
Initialization The host sandbox is unavailable or its configuration could not be prepared.
Spawn The wrapped command could not be started. The original std::io::Error is preserved as the source.
InvalidPath The working directory could not be canonicalized.

Child operations such as wait and kill return Tokio's std::io::Result because they operate on an already-spawned process.

Sandbox behavior and limitations

The library is a process-launching wrapper, not a complete container runtime. Keep the host-specific behavior in mind when choosing it for a workload:

  • Linux uses Bubblewrap and shares the host network with the child (--share-net). It exposes /usr, /proc, and /dev as needed for execution and makes the configured working directory available inside the sandbox.
  • macOS generates a temporary sandbox-exec profile for each child. The profile allows network access, process execution/forking, and broad file reads while restricting writes to the working directory, temporary directories, and paths passed to bind_rw.
  • The library does not impose CPU, memory, wall-clock, or network quotas.
  • The library does not validate whether a command is trusted, sanitize arguments, or automatically collect output.
  • A command can still consume resources or produce large output. Applications should add timeouts, output limits, and resource controls appropriate to their threat model.
  • Platform sandbox semantics and system policy remain authoritative. Test the exact host configuration used in production.

Troubleshooting

required sandbox executable ... was not found

Install the platform dependency and ensure it is discoverable:

  • Linux: install bubblewrap and make sure bwrap is on PATH.
  • macOS: verify that /usr/bin/sandbox-exec exists.

sandbox_apply: Operation not permitted on macOS

The executable may be present while the host policy still prevents sandbox creation. This commonly occurs in restricted CI runners, containers, or other nested sandbox environments. Run the command on a host that permits sandbox-exec, or adjust the runner's security policy.

Bubblewrap starts but the child cannot run

Confirm that the working directory and any paths passed to bind_ro or bind_rw already exist. Also verify that the host permits unprivileged user namespaces and that the command's executable and runtime files are available through the sandbox's filesystem view.