Macaw.Data.Derived.Sandbox (macaw_data v0.5.3)

Copy Markdown View Source

The sandbox a derived source's function runs in (spec/02 §6, visualize D-194).

A derived source is a source computed from other sources by an Elixir function, rows in and rows out (visualize#643). A function stored in a document as source text is untrusted, so it is checked against an allowlist before anything runs (check/2), compiled once into a closure (compile/2, cached by Macaw.Data.Derived.Sandbox.Cache), and called in a bounded process (run/3) — as is a host's own trusted function.

iex> alias Macaw.Data.Derived.Sandbox
iex> {:ok, compiled} = Sandbox.compile("fn rows -> Enum.map(rows, & &1.v * 2) end")
iex> Sandbox.run(compiled, [[%{v: 1}, %{v: 2}]])
{:ok, [2, 4]}
iex> {:error, [{1, reason}]} = Sandbox.check("fn _ -> File.read(\"/etc/passwd\") end")
iex> reason
{:restricted, {"File", "read", 1}}

A call's cost is the reductions its process spends, held to max_reductions:, so a runaway's verdict is the same on a loaded host; a call blocked in receive is :hung at a separate, generous timeout: (visualize#852, visualize D-220). Nothing is rescued: a raise, a heap overrun, a runaway and a hang are read from the process the call ran in, through its monitor.

Summary

Types

A checker fault: the source line, or nil, and the reason (spec/02 §6.1).

What a call can end in besides a value (spec/02 §6.4).

Functions

As check/2 with no options.

A stored function checked against the allowlist (spec/02 §6.1): {:ok, quoted}, or {:error, faults} with every fault, each {line, reason}, ordered by line. Nothing is evaluated.

As compile/2 with no options.

A stored function checked, then compiled once into a closure (spec/02 §6.3): {:ok, %Compiled{}}, or {:error, faults} — the checker's, or {nil, {:compile, message}} for a checked source the compiler refuses. The compilation runs in the bounded runner. Options: the checker's and the runner's.

One checker fault as a line a person reads (spec/02 §6.1).

A source's SHA-256 as 64 lowercase hexadecimal digits: its key in the cache (spec/08 §10.5.3).

As run/3 with no options.

A function called with args in a process of its own (spec/02 §6.4): {:ok, value}, or {:error, kind, detail} — {:error, :reductions, cap} when its process spent more than max_reductions: (100_000_000), {:error, :hung, ms} when it had not answered within timeout: (30_000 ms) — blocked, not slow — {:error, :memory, words} when its heap passed max_heap: (10_000_000 words) and the VM killed it, {:error, :raised, module} with the exception's module when it raised. The process is spawned with its heap limit set and monitored; a killed process is waited for before the call returns. Each bound may be lowered, never raised: a value above its default is ArgumentError.

Types

fault()

@type fault() :: {pos_integer() | nil, term()}

A checker fault: the source line, or nil, and the reason (spec/02 §6.1).

kind()

@type kind() :: :reductions | :hung | :memory | :raised

What a call can end in besides a value (spec/02 §6.4).

Functions

check(source)

@spec check(String.t()) :: {:ok, Macro.t()} | {:error, [fault(), ...]}

As check/2 with no options.

check(source, opts)

@spec check(String.t(), keyword()) :: {:ok, Macro.t()} | {:error, [fault(), ...]}

A stored function checked against the allowlist (spec/02 §6.1): {:ok, quoted}, or {:error, faults} with every fault, each {line, reason}, ordered by line. Nothing is evaluated.

Options: arity: (the arities the function may take, an integer or a list, [1, 2]: its inputs, or its inputs and its context), fields: (the names a row.field may read; nil reads any), max_source: (bytes, 10_000, which may be lowered, never raised) and operations: (the registry a Macaw.Data.Ops call's name is held to, macaw_data's own package by default; nil holds the name only to being a literal, for a source checked before it is placed, spec/04 §6.1).

compile(source)

@spec compile(String.t()) ::
  {:ok, Macaw.Data.Derived.Sandbox.Compiled.t()} | {:error, [fault(), ...]}

As compile/2 with no options.

compile(source, opts)

@spec compile(String.t(), keyword()) ::
  {:ok, Macaw.Data.Derived.Sandbox.Compiled.t()} | {:error, [fault(), ...]}

A stored function checked, then compiled once into a closure (spec/02 §6.3): {:ok, %Compiled{}}, or {:error, faults} — the checker's, or {nil, {:compile, message}} for a checked source the compiler refuses. The compilation runs in the bounded runner. Options: the checker's and the runner's.

format_error(arg)

@spec format_error(fault()) :: String.t()

One checker fault as a line a person reads (spec/02 §6.1).

iex> Macaw.Data.Derived.Sandbox.format_error({3, {:restricted, {"Process", :send, 3}}})
"line 3: Process.send/3 is not allowed in a derived source"

hash(source)

@spec hash(String.t()) :: String.t()

A source's SHA-256 as 64 lowercase hexadecimal digits: its key in the cache (spec/08 §10.5.3).

iex> Macaw.Data.Derived.Sandbox.hash("fn rows -> rows end")
"a07f4aab6263d19d009830e7ad7b37931d654ff286700db61a32f064ea6d98d8"

run(function, args)

@spec run(Macaw.Data.Derived.Sandbox.Compiled.t() | function(), list()) ::
  {:ok, term()} | {:error, kind(), term()}

As run/3 with no options.

run(function, args, opts)

@spec run(Macaw.Data.Derived.Sandbox.Compiled.t() | function(), list(), keyword()) ::
  {:ok, term()} | {:error, kind(), term()}

A function called with args in a process of its own (spec/02 §6.4): {:ok, value}, or {:error, kind, detail} — {:error, :reductions, cap} when its process spent more than max_reductions: (100_000_000), {:error, :hung, ms} when it had not answered within timeout: (30_000 ms) — blocked, not slow — {:error, :memory, words} when its heap passed max_heap: (10_000_000 words) and the VM killed it, {:error, :raised, module} with the exception's module when it raised. The process is spawned with its heap limit set and monitored; a killed process is waited for before the call returns. Each bound may be lowered, never raised: a value above its default is ArgumentError.