internal/winsandbox
Reasonix's bundled native Windows process sandbox helpers.
This package provides a small Windows-only sandbox runner built from platform primitives:
- AppContainer for read-only launches.
- Low-integrity primary tokens for writable launches.
- Temporary ACL grants for writable roots, command temp roots, and executable roots.
- Temporary deny ACEs for forbid-read roots (files and directories).
- Per-command temp directory redirection.
- Kill-on-close Job Objects for process-tree cleanup.
- Per-root serialization so concurrent runs against a shared workspace cannot corrupt each other's ACL/label boundary.
- Best-effort cleanup of ACL/integrity-label residue left by a crashed run.
The package intentionally exposes a narrow API. It does not implement product policy, prompting, or shell parsing; callers pass an already-resolved argv and a small filesystem/network policy.
Usage
result, err := winsandbox.Run(winsandbox.Spec{
WritableRoots: []string{workspace},
ForbidReadRoots: []string{secretDir},
Network: true,
Writable: true,
TempPrefix: "myapp-sandbox-",
}, []string{"powershell", "-NoProfile", "-NonInteractive", "-Command", script}, winsandbox.RunOptions{
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
})
Concurrency And Crash Safety
The sandbox enforces its boundary by temporarily mutating a path's DACL and
integrity label and restoring them afterward, so two runs touching the same root
must not overlap. Each run takes a per-root named mutex (session-local
namespace) for its whole lifetime; runs on disjoint roots proceed in parallel,
runs on a shared root serialize. A crashed holder never deadlocks the next run
because the OS marks its mutex abandoned. The holder records its PID and a
command preview next to each lock, so a queued run's notice and timeout error
name what is blocking it. The queue wait defaults to 1 minute (an interactive
command should fail fast with the holder named, not hang); a caller whose run
nobody is blocked on — a background job — passes a longer Spec.LockWait
budget.
A writable run stamps a Low integrity label across its writable subtree and adds
a deny ACE (including the current user's SID) for each forbid-read root. Both are
undone on normal cleanup. If a run is force-killed before cleanup, the next run
sweeps the residue: the writable subtree is recursively reset to Medium
integrity, and lingering sandbox deny ACEs recorded by a now-dead process are
removed, so a crash cannot permanently lower a workspace's integrity or lock the
user out of a forbid-read path such as ~/.ssh.
Environment Overrides
| Variable | Effect |
|---|---|
WINDOWS_SANDBOX_WAIT_MS |
Max wall-clock a sandboxed child may run before it is killed. |
WINDOWS_SANDBOX_ICACLS_TIMEOUT_MS |
icacls timeout. Recursive (/T) operations default to a much larger ceiling than flat ones because they walk the whole subtree; this overrides both. |
WINDOWS_SANDBOX_LOCK_MS |
Max wall-clock to wait for a busy per-root lock before failing with a clear error instead of hanging. Overrides both defaults (1 minute interactive, 10 minutes for background bash jobs); stop the command named in the error before raising this. |
Network Semantics
Read-only launches use AppContainer. When Network is false, network
capabilities are omitted.
Writable launches use a low-integrity token so normal developer workspaces can
be written without requiring an elevated helper. Low-integrity tokens do not
provide reliable per-process network blocking without elevated firewall or WFP
setup, so writable launches with Network: false fail closed.
Platform Support
Available and Run return unavailable on non-Windows hosts. The module still
builds on non-Windows platforms so callers can depend on it unconditionally.
Verification Matrix
The Windows CI tests exercise both sandbox launch modes:
- writable low-integrity commands can write inside configured roots and command temp, but not outside configured roots;
- read-only AppContainer commands can read allowed roots but cannot write them;
ForbidReadRootsare denied in both writable and read-only launches;Network: falseAppContainer launches cannot connect to a loopback listener;- stdin, stdout, stderr, environment overrides, working directory, paths with spaces, and child exit codes are preserved;
- kill-on-close Job Objects clean up child process trees;
- temporary ACL grants/denies are removed after the command exits, leaving no Low integrity label or sandbox deny ACE behind;
- concurrent writable commands against a shared non-empty workspace all succeed and leave no residue (serialization regression guard).