PtcRunner.Sandbox (PtcRunner v0.12.0)

Copy Markdown View Source

Executes programs in isolated BEAM processes with resource limits.

Spawns isolated processes with configurable timeout and memory limits, ensuring safe program execution.

Resource Limits

ResourceDefaultOption
Timeout1,000 ms:timeout
Max Heap~10 MB (1,250,000 words):max_heap
Worker Max Heap= :max_heap:worker_max_heap
Max Parallel Workers8: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_heap

A 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

eval_fn()

@type eval_fn() :: (any(), PtcRunner.Context.t() ->
                {:ok, any(), map()}
                | {:error, {atom(), String.t()} | {atom(), String.t(), any()}})

Evaluator function that takes AST and context and returns result with memory.

metrics()

@type metrics() :: %{
  duration_ms: integer(),
  memory_bytes: integer(),
  eval_reductions: non_neg_integer()
}

Execution metrics for a program run.

Functions

execute(ast, context, opts \\ [])

@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

run_bounded(fun, opts \\ [])

@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 returned result
  • {: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}}