No-admin Windows process sandboxing with a std::process::Command-like API.
Rust · Windows 11 / Windows Server 2025 · AppContainer · Windows Sandbox API · process isolation · no-admin process sandbox · Job Object · agent sandbox · untrusted code execution · filesystem ACL · CI security evals
sandboxrs-windows is a Rust library that launches Windows child processes
inside a validated, reusable authority boundary. It prefers Windows' modern
composable sandbox API when available and falls back to a regular AppContainer
without requiring administrator privileges.
use sandboxrs_windows::{Sandbox, Stdio};
let sandbox = Sandbox::builder(r"C:\repo")
.read_only(r"C:\Users\me\.rustup")
.read_write(r"C:\temp\sandboxrs")
.build()?;
let output = sandbox
.command("cargo")
.arg("test")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()?;Modern tooling keeps handing hostile or untrusted workloads a full user token:
AI coding agents, plugin hosts, build scripts, test runners, and package
installers all execute arbitrary commands. sandboxrs-windows gives those
systems an explicit filesystem authority boundary without asking for a daemon,
a service, a kernel driver, Docker, WSL, Hyper-V, or administrator rights.
The library is mechanism only. It understands paths, environment, stdio, timeouts, and resource limits. It does not know about agents, plans, LLMs, tools, or workflows.
Backend selection is capability probing, never OS-version guessing:
- Load
processmodel.dllfrom System32 and resolveExperimental_CreateProcessInSandbox. - Query
Experimental_QuerySandboxSupportwhen present. - Perform a real minimal sandbox launch and outside-write denial.
- If the modern API is unavailable or disabled, probe a real AppContainer
launch through
rappct. - If neither works,
build()fails closed. There is no silentstd::process::Commandfallback.
One validated Sandbox can run many commands without rebuilding policy.
Sandbox
├── command("cargo") cargo check / cargo test
├── command("git") git status / git diff
└── command("python") python script
The public surface intentionally stays small and familiar:
use std::time::Duration;
use sandboxrs_windows::{BackendKind, Sandbox, Stdio};
let sandbox = Sandbox::builder(r"C:\repo")
.read_only(r"C:\Users\me\.rustup")
.read_write(r"C:\temp\sandboxrs")
.timeout(Duration::from_secs(120))
.max_memory(2 * 1024 * 1024 * 1024)
.max_processes(32)
.build()?;
let mut child = sandbox
.command("cargo")
.args(["test", "--workspace"])
.env("RUST_BACKTRACE", "1")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let status = child.wait()?;Key types:
SandboxBuildervalidates and initializes a sandbox.Sandboxis a reusable, immutable authority boundary.SandboxCommandmirrorsstd::process::Command.SandboxChildowns the process tree and Job Object.SandboxOutputreturns status, stdout, stderr, backend, and duration.BackendKindreportswindows-sandbox-apiorappcontainer.
std::process::Stdio is opaque, so the crate exposes its own
sandboxrs_windows::Stdio::{inherit, null, piped}.
The workspace passed to Sandbox::builder is read-write. Explicit read_only
and read_write roots are compiled into a private FilesystemPlan before any
backend sees them.
C:\repo RW
C:\repo\.readonly RO
C:\secret HIDDEN (default)
Rules are normalized, duplicates are resolved, and overlapping policy uses
"more specific path wins." If a backend cannot faithfully represent a rule, it
returns UnsupportedPolicy instead of silently flattening authority.
sandboxrs.exe is a thin adapter for Node, Python, Java, Go, shell scripts,
and other runtimes:
sandboxrs.exe exec --workspace C:\repo --ro C:\Users\me\.rustup -- cargo test
sandboxrs.exe exec --json --workspace C:\repo -- cargo check
sandboxrs.exe doctorJSON mode keeps child stdout/stderr as child data:
{
"backend": "appcontainer",
"exit_code": 0,
"duration_ms": 1831,
"stdout": "...",
"stderr": ""
}The deterministic eval suite (sandboxrs-eval) runs on clean GitHub-hosted
Windows VMs and proves every result twice: first that the operation succeeds
outside the sandbox, then that the sandbox contains it. Reports are uploaded
as CI artifacts and stored in evals/results.
| Suite | Windows Server 2025 (26100) | Windows 11 ARM64 (26200) |
|---|---|---|
| Backend selected | AppContainer | AppContainer |
| Security evidence PASS | 43 | 43 |
| Security ESCAPE | 0 | 0 |
| Security ERROR (invalid test) | 0 | 0 |
| Security UNSUPPORTED | 1 | 1 |
| Developer compatibility | 1 / 5 | 2 / 5 |
| Privilege | standard user | standard user |
Every security case in Evals V2 records four-state evidence
(pass / escape / error / unsupported), requires a native control proving
the attack is possible outside the sandbox, restores fixture state after the
control, and verifies filesystem postconditions. The measured result is
43 pass, 0 escapes, 0 invalid tests on both runners under a real
standard Windows user.
The single unsupported case is job.breakaway: GitHub's hosted runners
themselves run inside a Job Object that already denies
CREATE_BREAKAWAY_FROM_JOB natively, so that mode is reported as unavailable
rather than credited to the sandbox. On a Windows 11 VDI where the native
control succeeds, it becomes a required pass.
This is an experimental benchmark, not a security certification: the headline is "zero escapes and zero invalid tests," not "100% secure."
Security contract:
- native control writes outside the sandbox must succeed
- workspace read / write / delete must succeed
- readonly read must succeed; readonly write and delete must fail
- hidden secret read and write must fail
- root, child, and grandchild escape attempts must all fail
Path escape:
..traversal, absolute paths, case variations,\\?\extended paths- junctions, symlinks, and nested junctions
Lifecycle containment:
- explicit kill terminates descendants
- timeout terminates the process tree
max_processescontains a 1000-process bombmax_memoryterminates a 2 GB allocation
Compatibility:
node --versionandcargo --versionpass on both runnersgit --versionpasses on Windows 11 ARM64cmd.exeandpythoncurrently need additional runtime grants and fail at process initialization on these VMs; this is a compatibility gap, not an escape
The workflow is intentionally honest about that distinction: filesystem, descendant, reparse, path, handle, Job-containment, resource, and environment suites are required and gate CI, while compatibility failures are reported but do not turn the workflow red by themselves.
Eval results use a four-state outcome model:
Pass attack precondition valid, attack executed, OS blocked it
Escape forbidden operation succeeded
Error test never validly exercised the property
Unsupported backend/platform genuinely cannot run the test
A process that fails to launch is Error, never Pass. Every forbidden
security test has a native control proving the attack is possible outside the
sandbox, and every descendant test first proves the same capability works
inside the workspace before asserting it fails in the secret area.
The eval covers:
- filesystem authority with postcondition verification
- child, grandchild, and great-grandchild authority propagation
- real directory symlinks, file symlinks, NTFS junctions, and nested junctions
- dot-dot, absolute, extended-length, case, and relative path representations
- rename/move and hard-link attacks
- inherited handle exfiltration (read through a pre-opened secret handle)
- Job breakaway, detached, and suspended-resume descendants
- process-count, memory, and timeout boundary tests
env_clear()isolation where the child must still launch with a correctly rebuilt environment block (including the real=C:=<current directory>drive variables for every present logical drive)- cross-sandbox policy isolation (forced backend on every nested
Sandbox) - a real malicious Rust fixture built through
cargo
Evals V2.1 additionally:
- fails when a required backend is unavailable unless
--allow-unsupportedwas explicitly passed (observed results never mutate that flag) - forces the evaluated backend into every nested
Sandboxthrough onesandbox_for()helper, so resource, cross-sandbox, and Cargo suites cannot silently fall back to Auto - makes the hostile
build.rsactively leak aLEAKED_SECRET.txt, attempt an outside write, and spawn a child outside write; a Cargo build only passes compatibility when all three remain blocked - proves breakaway/detached/suspended child creation works natively before the
sandboxed run, and marks host-blocked modes
unsupported - runs the inherited-handle test from the workspace cwd so only handle inheritance is under test
GitHub CI runs the AppContainer suite as a temporary standard user and fails
if the eval reports admin: true, making the no-admin claim testable.
--backend windows-sandbox-api runs the same contract against the modern API
and reports unsupported when the host feature is disabled.
Run locally:
cargo run --bin sandboxrs-eval -- --backend appcontainer --require-standard-user
cargo run --bin sandboxrs-eval -- --backend windows-sandbox-api --allow-unsupportedsandboxrs-windows is not a promise that arbitrary hostile binaries are
perfectly contained. It is a no-admin process launcher with constrained
filesystem authority. Reparse points, inherited handles, and experimental API
instability are treated as adversarial in the eval suite.
- Fail closed: no backend means no
Sandbox. - No silent downgrade: unrepresentable policy is an error.
- No daemon, service, kernel driver, DLL injection, or machine-wide firewall changes.
Experimental_CreateProcessInSandboxis experimental and may change. All experimental FFI is isolated behind a private backend module.- Baseline system roots are granted read/execute best-effort; user policy is never flattened silently.
cargo build --all-targets
cargo test --all-targets
cargo run --bin sandboxrs -- doctor --json
cargo run --bin sandboxrs-eval -- --report sandboxrs-eval.jsonCI:
windows.ymlbuilds and tests onwindows-latest, then reports the ignored backend contract tests on Windows 11 ARM64.security-evals.ymlruns the deterministic benchmark onwindows-2025andwindows-11-arm, uploading the JSON report as an artifact.
Agentic evals are intentionally separate; see
evals/agent-scenarios.md and
evals/run-agent-evals.ps1.
- M0 real backend probe: passed on AppContainer.
- M1 modern backend + reusable Rust API: implemented; modern API is export-present but feature-disabled on the public Windows runners tested.
- M3 AppContainer fallback via
rappct: implemented and benchmarked. - M4 process quality: pipes, timeout, kill, memory/process limits, diagnostics.
- M5 EXE:
sandboxrs exec,sandboxrs doctor,--json.
See PLAN.md for the full v1 contract and FUTUREPLAN.md for the backlog.