Executes programs in isolated BEAM processes with resource limits.
Spawns isolated processes with configurable timeout and memory limits, ensuring safe program execution.
Resource Limits
| Resource | Default | Option |
|---|---|---|
| Timeout | 1,000 ms | :timeout |
| Max Heap | ~10 MB (1,250,000 words) | :max_heap |
| Worker Max Heap | = :max_heap | :worker_max_heap |
| Max Parallel Workers | 8 | :max_parallel_workers |
:max_heap sets a max_heap_size flag on the sandbox process. That
flag is per-process and is not inherited by child processes, so the
PTC-Lisp pmap/pcalls builtins spawn each worker (via
PtcRunner.Lisp.Eval.ParallelRunner) with its OWN fixed
max_heap_size of :worker_max_heap words. The number of parallel
workers alive at once — across the whole run, at every nesting depth —
is capped by a shared slot semaphore of :max_parallel_workers
(PtcRunner.Lisp.Eval.ParallelBudget). Aggregate live parallel heap
is therefore bounded by:
max_parallel_workers × worker_max_heapA pmap/pcalls worker that cannot obtain a slot fails the run with
:parallel_capacity_exceeded (no sequential fallback). The top-level
sandbox process is not counted as a parallel slot.
The :max_heap sandbox limit and each :worker_max_heap parallel-worker
limit are enforced via BEAM's :max_heap_size process flag with
include_shared_binaries: true, so they account for both process-local heap
terms and shared (refc) binaries referenced by the process. This prevents
binary-heavy programs from exceeding the memory budget via off-heap
allocations.
Note that this is a per-process BEAM budget, not a whole-node or container memory limit. For adversarial multi-tenant deployments, back this with an OS/container memory limit around the VM or an isolated worker process.
Configuration
Limits can be set per-call:
PtcRunner.Lisp.run(program, timeout: 5000, max_heap: 5_000_000)Or as application-level defaults in config.exs:
config :ptc_runner,
default_timeout: 2000,
default_max_heap: 2_500_000
Summary
Types
Evaluator function that takes AST and context and returns result with memory.
Execution metrics for a program run.
Functions
Executes an AST in an isolated sandbox process.
Runs an arbitrary function in an isolated process with resource limits.
Types
Functions
@spec execute(any(), PtcRunner.Context.t(), keyword()) :: {:ok, any(), metrics(), map()} | {:error, {atom(), non_neg_integer()} | {atom(), String.t()} | {atom(), String.t(), any()}}
Executes an AST in an isolated sandbox process.
Arguments
- ast: The AST to execute
- context: The execution context
- opts: Options (timeout, max_heap, eval_fn)
:eval_fn- Evaluator function (required):timeout- Timeout in milliseconds (default: 1000, configurable via:default_timeout):max_heap- Max heap size in words (default: 1_250_000, configurable via:default_max_heap)
Returns
{:ok, result, metrics, memory}on success{:error, reason}on failure
@spec run_bounded( (-> term()), keyword() ) :: {:ok, term()} | {:error, {:timeout, non_neg_integer()} | {:memory_exceeded, non_neg_integer()} | {:execution_error, String.t()}}
Runs an arbitrary function in an isolated process with resource limits.
Unlike execute/3 which is specialized for Lisp evaluation, this function
runs any zero-arity function under the same process isolation primitives
(timeout, max_heap_size, monitored child).
Options
:timeout- Timeout in milliseconds (default: 1000):max_heap- Max heap size in words (default: 1_250_000)
Returns
{:ok, result}— the function returnedresult{:error, {:timeout, ms}}— killed after timeout{:error, {:memory_exceeded, bytes}}— heap limit hit{:error, {:execution_error, message}}— process crashed
Examples
iex> PtcRunner.Sandbox.run_bounded(fn -> 1 + 1 end)
{:ok, 2}
iex> PtcRunner.Sandbox.run_bounded(fn -> :timer.sleep(:infinity) end, timeout: 50)
{:error, {:timeout, 50}}