ExSandbox
Isolated execution sandboxes library with no host-application concepts.
ex_sandbox is a composition and evidence layer over operating-system facilities, not a new
isolation mechanism. Nothing here invents containment: cgroup v2, user and mount namespaces,
setpriv and bwrap do the confining. What this library adds is composing them correctly,
refusing to run when it cannot, and producing evidence that the boundary is real.
Installation
def deps do
[{:ex_sandbox, "~> 1.0"}]
end
⚠️ Linux is where this library does its job. It installs, compiles and runs its unit suite on
macOS, but capabilities/0 reports every gating capability unavailable there and
ExSandbox.Mechanism.Beam refuses to provision — deliberately, see Refusal is the design below.
ExSandbox.Mechanism.Docker exists for exactly that host.
Dependencies
:telemetry, and nothing else. The
Elixir floor is ~> 1.14 deliberately, so consumers are not forced onto the platform's version.
Both properties are enforced by tests rather than by convention — see dependency_tree_test.exs
and boundary_enforcement_test.exs.
The direction matters more than it looks. Research R2 established that a wrong-direction reference
inside this library compiles cleanly, exits 0, passes mix deps.tree, and fails only at runtime
inside a third-party consumer's application. --warnings-as-errors is the only build-time check
that catches it, which is why the gate is load-bearing rather than stylistic.
About the 005-FR-011-style identifiers, and the name Axonn
The source cites requirement IDs heavily — 012-FR-014, 005 R9, 029 T015. They are citations
into the specifications this library was built against, not dead references. What they
mean explains the scheme; the short version is that the number is a
specification, FR is a rule, SC an observable criterion, T a task and R a recorded
measurement.
The same page covers Axonn, which appears throughout these docs: it is the application this
library was extracted from, named where a decision was measured against a real caller. Nothing
here depends on it.
The interface
ExSandbox is the facade: provision/2, start/2, stop/2, destroy/2, status/2,
list_running/1, usage/2, capabilities/0. Each takes a mechanism module implementing the
ExSandbox.Mechanism behaviour.
{:ok, sandbox} = ExSandbox.provision(ExSandbox.Mechanism.Beam, %ExSandbox.Sandbox{...})
{:ok, running} = ExSandbox.start(ExSandbox.Mechanism.Beam, sandbox)
{:ok, :running} = ExSandbox.status(ExSandbox.Mechanism.Beam, running)
{:ok, stopped} = ExSandbox.stop(ExSandbox.Mechanism.Beam, running)
:ok = ExSandbox.destroy(ExSandbox.Mechanism.Beam, stopped)
What is public
ExSandbox, ExSandbox.Mechanism, ExSandbox.Sandbox, ExSandbox.Capability,
ExSandbox.Hardening, ExSandbox.Conformance, ExSandbox.Proxy, ExSandbox.Telemetry, and
ExSandbox.Conformance.{Lifecycle, Isolation, ResourceLimits, Helpers, Group} — the last group
public by consequence, since use ExSandbox.Conformance expands into calls on them inside the
consumer's own module.
A module not on that list is private, whether or not it is namespaced Internal
(012-FR-014). The ExSandbox.Internal.* prefix makes the common case obvious, but the list is
what defines the boundary — lacking the prefix does not make a module public. There is no
compatibility promise for private modules; calling one from a consuming application is the
coupling 012-FR-004 forbids. A consuming application can check for it mechanically:
priv/boundary.md ships inside the package and resolves at runtime through
Application.app_dir(:ex_sandbox, "priv/boundary.md"), so a consumer's own test can read the
public-interface table from the installed dependency rather than restating it.
The authoritative list lives in ExSandbox's own @moduledoc. This README summarises it; if the
two disagree, the moduledoc is right.
Refusal is the design
A host that cannot enforce confinement gets a refusal, not a weaker sandbox. capabilities/0
probes five things — resource limits, privilege separation, filesystem confinement, network
restriction, disk quota — and the BEAM mechanism refuses to provision when any it requires is
missing. A partially confined tenant is worse than none, because it looks contained.
⚠️ Probes must attempt what the launch actually does. This library has shipped the opposite
defect four times: a probe testing an easier operation than the real one reports a capability the
host does not have, and every launch then dies — or worse, succeeds unconfined. Privilege is what
hides it, since the easy and the hard form agree until privilege is removed.
CapabilityBuildParityTest pins this by asserting on the probe's source rather than by running
it, because running it on a privileged host returns true either way.
The conformance suite
ExSandbox.Conformance is the contract's enforcement, usable by any mechanism implementation, not
just the BEAM one:
defmodule MyMechanismTest do
use ExSandbox.Conformance, mechanism: MyMechanism
end
It scores three outcomes, not two: pass, guarantee failure, and capability unavailable. The third exists because "this host cannot demonstrate the guarantee" and "this mechanism breached the guarantee" lead to opposite actions, and collapsing them into a failure produces breach reports for boundaries that were never tested.
Tests
mix test # unit + contract; isolation excluded off Linux
docker compose -f docker/compose.isolation.yml up --build \
--abort-on-container-exit --exit-code-from isolation isolation
⚠️ A green mix test on macOS says nothing about whether tenant code is contained. Six of
005's ten success criteria rest on Linux kernel facilities with no macOS equivalent, so the
:isolation and :reclamation tags are excluded there — visibly not run, rather than passing
vacuously. The container is a real Linux host with systemd as PID 1 and all five capabilities
genuinely constructed; it has found more than a dozen defects in code that passed everything
locally, including a launch path that failed on every Linux host.
Contributing
⚠️ Read the isolation-harness warning above first. A pull request whose mix test is green on
macOS has verified nothing about containment, and the container is not optional for any change
touching ExSandbox.Hardening.*, ExSandbox.Egress.* or the mechanisms.
mix precommit is the gate: compile --warnings-as-errors --force, format --check-formatted,
deps.unlock --check-unused, test. The warnings flag is boundary enforcement rather than style —
see Dependencies above.
License
Apache-2.0. See LICENSE.