procjail 0.1.1

Process sandbox for running untrusted code — Linux namespaces, seccomp, firejail, bubblewrap, rlimits
Documentation

procjail — Process sandboxing for untrusted code execution

License Tests Crates.io

Why

Security scanners execute potentially malicious packages, snippets, and transforms, so an isolation boundary is mandatory.

procjail gives you one shared runtime for Linux process containment with multiple strategy levels (unshare, bubblewrap, firejail, rlimits fallback), letting you keep the same API while adapting to host capability.

It also standardizes process I/O and teardown so scanners can focus on findings instead of child-process mechanics.

Quick Start

use procjail::{SandboxConfig, SandboxedProcess};
use std::path::Path;

fn main() -> anyhow::Result<()> {
    let config = SandboxConfig::builder()
        .runtime("node")
        .runtime_args(&["--input-type=commonjs"]) // optional
        .max_memory_mb(256)
        .max_cpu_seconds(15)
        .allow_localhost(false)
        .env_passthrough(&["PATH", "HOME"])
        .env_strip_secrets(true)
        .build();

    let harness = Path::new("tools/safe-harness.js");
    let work_dir = Path::new("/tmp/work");

    let mut proc = SandboxedProcess::spawn(harness, work_dir, &config)?;
    proc.send(r#"{"op":"ping"}"#)?;

    if let Some(line) = proc.recv()? {
        println!("sandbox response: {}", line.trim());
    }

    let usage = proc.wait_with_usage()?;
    println!(
        "strategy={} exit={} mem={}B cpu={:.2}s wall={:.2}s",
        proc.strategy(),
        usage.exit_code,
        usage.peak_memory_bytes,
        usage.cpu_time_secs,
        usage.wall_time_secs,
    );

    Ok(())
}

Features

  • Multi-strategy containment: unshare, bubblewrap, firejail, rlimits-only.
  • Uniform spawn/communicate/teardown API via SandboxedProcess.
  • Builder-driven hardening (max_memory, max_cpu_seconds, file descriptor and process limits, mounts, env policy).
  • Probe helpers for host capability detection and strategy selection.
  • Watchdog-aware timeout behavior and usage reporting (ResourceUsage).

TOML Configuration

You can load a SandboxConfig directly from a TOML file using SandboxConfig::load(path).

Example sandbox.toml:

max_memory_bytes = 268435456
max_cpu_seconds = 30
max_fds = 64
max_disk_bytes = 52428800
max_processes = 32
allow_localhost = false
runtime_path = "/usr/bin/node"
runtime_args = ["--experimental-vm-modules"]
env_passthrough = ["HOME", "PATH", "NODE_PATH"]
env_set = []
env_strip = []
env_strip_secrets = true
env_mode = "strip-secrets"
timeout_seconds = 60
capture_stderr = false
readonly_mounts = []
writable_mounts = []

Loading in Rust:

use procjail::SandboxConfig;

let config = SandboxConfig::load("sandbox.toml").unwrap();

API Overview

  • SandboxConfig / SandboxConfigBuilder: all runtime controls.
  • SandboxedProcess: spawn child, send/recv JSON lines, wait, collect usage.
  • ResourceUsage: post-run resource metrics.
  • Strategy: containment variant metadata and capability checks.
  • probe_capabilities, available_strategy: pick the best strategy for host runtime.
  • quick_spawn: convenience helper for ephemeral harness runs.
  • Traits: SandboxRunner, SandboxedIO for pluggable sandbox backends.

Examples

1) Detect available containment and print strategy capabilities

use procjail::detect_capabilities;

fn main() {
    let level = procjail::probe_capabilities();
    println!("best strategy: {}", level.best_strategy);
    println!("unshare: {}", level.has_unshare);
    println!("bubblewrap: {}", level.has_bubblewrap);
    println!("firejail: {}", level.has_firejail);
}

2) Use capability defaults with a quick spawn

use procjail::quick_spawn;
use std::path::Path;

fn main() -> anyhow::Result<()> {
    let _proc = quick_spawn("node", Path::new("tools/harness.js"), Path::new("/tmp/work"))?;
    Ok(())
}

3) Implement a custom sandbox runner for testing

use anyhow::Result;
use procjail::{SandboxedIO, SandboxRunner};
use std::collections::VecDeque;
use std::path::Path;

struct MockIo {
    buf: VecDeque<String>,
}

impl SandboxedIO for MockIo {
    fn send(&mut self, line: &str) -> Result<()> {
        self.buf.push_back(line.to_string());
        Ok(())
    }

    fn recv(&mut self) -> Result<Option<String>> {
        Ok(self.buf.pop_front())
    }

    fn kill(&mut self) {}

    fn is_alive(&mut self) -> bool {
        false
    }
}

struct MockRunner;

impl SandboxRunner for MockRunner {
    fn spawn(&self, _harness: &Path, _work_dir: &Path) -> Result<Box<dyn SandboxedIO>> {
        Ok(Box::new(MockIo { buf: VecDeque::new() }))
    }
}

Traits

procjail defines SandboxRunner and SandboxedIO. Use SandboxRunner if you need alternate backends (containers, VMs, remote executors) and implement SandboxedIO for your transport.

Related Crates

License

MIT, Corum Collective LLC

Docs: https://docs.rs/procjail

Santh ecosystem: https://santh.io