ExSandbox.Mechanism.Beam (ExSandbox v1.0.0)

Copy Markdown View Source

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

call(sandbox, module, function, args, timeout \\ 10000)

@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.

cast(sandbox, module, function, args)

@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.

host_pid(sandbox)

@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.

provision_failure_reason(sandbox)

@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.