Cross-platform process sandboxing for Rust. Launch child processes with restricted filesystem and network access using native OS mechanisms.
| Platform | Isolation |
|---|---|
| Linux | User/mount/PID/net/IPC namespaces + seccomp-BPF |
| macOS | Seatbelt (sandbox_init SBPL profiles) |
| Windows | AppContainer + ACLs + Job Objects |
All enforcement is kernel-level. No in-process hooking, no TOCTOU races. Only the child process is sandboxed -- the caller is never restricted. Works without root/admin.
Add to Cargo.toml:
[dependencies]
lot = "0.1"use lot::{SandboxPolicyBuilder, SandboxCommand, spawn};
let policy = SandboxPolicyBuilder::new()
.include_platform_exec_paths().expect("exec paths")
.include_platform_lib_paths().expect("lib paths")
.allow_network(false)
.build()
.expect("policy invalid");
let mut cmd = SandboxCommand::new("/bin/echo");
cmd.arg("hello from sandbox");
let child = spawn(&policy, &cmd).expect("spawn failed");
let output = child.wait_with_output().expect("wait failed");
println!("{}", String::from_utf8_lossy(&output.stdout));| Flag | Effect |
|---|---|
tokio |
Enables SandboxedChild::wait_with_output_timeout for async wait with timeout. |
[dependencies]
lot = { version = "0.1", features = ["tokio"] }Full API documentation is available on docs.rs.
Install:
cargo install --path lot-cliRun a program inside a sandbox defined by a YAML config file.
lot run --config sandbox.yaml -- ./my-program arg1 arg2
lot run -c sandbox.yaml -t 30 -- ./long-task # 30s timeout
lot run -c sandbox.yaml --dry-run # validate only
lot run -c sandbox.yaml --verbose -- ./my-program # verbose outputExit code: forwards the child's exit code. Timeout exits 124 (GNU timeout convention). Setup failure exits 1.
Stdio is inherited by default -- the sandboxed process reads/writes the terminal directly.
# Filesystem access -- all paths are auto-canonicalized.
# Non-existent paths are skipped with a warning (--verbose).
filesystem:
read:
- /usr/lib
- /project/data
write:
- /tmp/output
exec:
- /usr/bin
- /project/bin
deny:
- /project/data/secrets # block access to subtree within granted paths
include_platform_exec: true # /usr/bin, /bin, System32, etc.
include_platform_lib: true # /usr/lib, /usr/include, Framework dirs, etc.
include_temp: true # Platform temp directory -> write_paths
# Network access. Default: false (denied).
network:
allow: false
# Environment variables for the child process.
environment:
forward_common: true # Forward PATH, HOME, USER, LANG, etc.
vars:
RUST_LOG: debug
MY_VAR: value
# Working directory for the child. Optional -- defaults to "/".
process:
cwd: /projectAll sections are optional except that at least one grant path (read, write, or exec) is required. An empty policy with no paths is rejected by validate(). A minimal config with only system paths (via include_platform_exec: true) is valid.
Configure platform prerequisites. Windows only (no-op on other platforms).
lot setup --verbose # grant prerequisites (requires elevation)
lot setup --check # check without modifyingPrint platform sandboxing capabilities.
lot probeOutput:
appcontainer=true
job_objects=true
namespaces=false
seccomp=false
seatbelt=false
AppContainer-sandboxed processes cannot open the Windows NUL device (\\.\NUL) by default, and cannot traverse ancestor directories of policy paths without explicit ACEs. This requires a one-time setup step.
This is a known Windows limitation with no built-in fix from Microsoft.
Use the CLI or the library API to grant prerequisites. Requires elevation (run as administrator). The changes persist across reboots and do not weaken AppContainer isolation.
CLI:
lot setup --verbose # grant prerequisites
lot setup --check # check without modifyingLibrary API (Windows-only):
is_elevated() -> bool-- checks if the current process has administrator privilegesgrant_appcontainer_prerequisites_for_policy(policy) -> Result<()>-- grants NUL device access and ancestor traverse ACEs for all policy pathsappcontainer_prerequisites_met_for_policy(policy) -> bool-- checks if prerequisites are already in place
For user-owned directories, spawn() grants ancestor traverse ACEs automatically at spawn time. The setup step is only needed for the NUL device and system directories.
- macOS
mach-lookupis unrestricted in Seatbelt profiles (restricting it breaks most programs). - Linux namespace tests require
kernel.apparmor_restrict_unprivileged_userns=0on Ubuntu 24.04+. - Windows: AppContainer processes cannot access
\\.\NULwithout a one-time system fix (see above). - Deny paths:
stat()succeeds on denied paths on Linux (shows empty tmpfs metadata) but fails on macOS/Windows. File access is blocked on all platforms. - Linux kernels < 5.9: parallel
spawn()calls from multi-threaded processes may hit ETXTBSY due to missingclose_rangesyscall. Works correctly on 5.9+.
- Rust 1.85+ (edition 2024)
- Linux: kernel 5.15+, unprivileged user namespaces enabled
- macOS: Seatbelt support (all recent macOS versions)
- Windows: Windows 10+ (AppContainer support)
MIT OR Apache-2.0 (LICENSE-MIT, LICENSE-APACHE)