Runs a tenant's Elixir application on its own hardened OS-level BEAM node (005 T027).
What makes this an isolation boundary
Not the BEAM. A supervised process tree inside the platform's VM would give a
tenant its own supervision, its own registry, and its own name — and none of
FR-001 through FR-011. It would share the platform's atom table (never
garbage collected, so FR-006 would be unachievable), its memory, its
process list, its cluster, and its environment; and :erlang.halt/0 in tenant
code would take the platform down with it.
The boundary is the operating system: a separate OS process, launched
under ExSandbox.Hardening's confinement wrapper, addressed over distribution
with a per-sandbox cookie. ExSandbox.Mechanism.Beam.NodeLauncher holds that
launch; this module is the ExSandbox.Mechanism face of it.
Failures map into 003's closed set
Every error returned here is one of 003's five failure_reason atoms, with
the mechanism's own words carried separately in the detail. That closure is
what lets a host route on cause without knowing which mechanism answered
(012-FR-008) — and it is why probe/1's :unresponsive becomes :timeout
rather than a sixth atom that only this mechanism could produce.
Summary
Functions
Evaluates {module, function, args} inside a sandbox and returns the result.
Evaluates {module, function, args} in a sandbox without waiting for a result.
The host's OS pid for a running sandbox.
Why a sandbox that is no longer running stopped (FR-009).
Functions
@spec call(ExSandbox.Sandbox.t(), module(), atom(), [term()], timeout()) :: {:ok, term()} | {:error, term()}
Evaluates {module, function, args} inside a sandbox and returns the result.
⚠️ Routed over :peer's stdio control channel, never Erlang distribution.
A sandbox runs under --unshare-net and therefore has no network interfaces at
all, so :erpc.call/5 raises {:erpc, :noconnection} against a perfectly
healthy sandbox — measured, not inferred. The failure is doubly misleading: it
is indistinguishable from a crashed node, and it gets more likely the better
the confinement works.
Like provision_failure_reason/1, this is deliberately not an
ExSandbox.Mechanism callback: "evaluate this in the sandbox's runtime" is
meaningful for a BEAM node and meaningless for a mechanism whose tenant is a
container running arbitrary code.
Returns {:error, :unknown_sandbox} for an id this mechanism never launched
rather than raising, since a caller racing destroy/1 is an ordinary outcome.
@spec cast(ExSandbox.Sandbox.t(), module(), atom(), [term()]) :: :ok | {:error, term()}
Evaluates {module, function, args} in a sandbox without waiting for a result.
For work whose effect is the point and whose reply will never arrive —
halting the node being the motivating case. A blocking call/5 there waits out
its full timeout on a node that is already gone, turning a fast assertion into
a slow one and reporting a timeout for an operation that did exactly what was
asked.
Same stdio routing, and the same reason, as call/5.
@spec host_pid(ExSandbox.Sandbox.t()) :: {:ok, pos_integer()} | {:error, term()}
The host's OS pid for a running sandbox.
⚠️ Not what the sandbox reports about itself. Under --unshare-pid a sandbox's
:os.getpid() returns its namespace-local pid — 2 — while the host knows it
by an unrelated number. Anything that reads /proc/<pid> on the host and takes
the sandbox's own answer is inspecting a different process entirely, and will
happily report on one that is not confined.
Exposed so that verification does not have to ask the thing being verified: a compromised sandbox cannot misreport this.
@spec provision_failure_reason(ExSandbox.Sandbox.t()) :: {:error, atom()} | :ok
Why a sandbox that is no longer running stopped (FR-009).
Deliberately not an ExSandbox.Mechanism callback. 012's behaviour is a
frozen contract shared by every mechanism, and widening it for one of them
would oblige every future mechanism to answer a question only this one can.
A host that wants this asks the Beam mechanism by name.
The distinction it carries is the one an operator acts on: :resource_cap
means the tenant hit its own limit, :mechanism_error means the platform
broke. Reporting a cap breach as the latter sends someone debugging the
platform for a tenant's memory leak.