Hyperlang (Hyperlang v0.1.0)

Copy Markdown View Source

Compile and run untrusted Erlang-shaped guest programs.

The whole public API is four functions:

  • program/1 compiles an immutable source value into a Hyperlang.Program. The manifest, the binding MFA set, the export set and the atom universe are fixed here and enter the program hash.
  • program/2 returns a program cached under a caller-owned key and calls the loader only on a miss.
  • run/4 runs one declared export with exactly the authority in its spec.
  • module/1 returns the program's confined OTP callback module. Every callback goes through run/4.

Errors are sanitized maps whose :category comes from a closed taxonomy; see doc/SECURITY_SPECIFICATION.md.

Summary

Functions

Return the loaded confined module for an immutable program value.

Compile an immutable, closed-world source value.

Return a program cached under a caller-owned key, compiling the loader's source value on a miss.

Run a declared export using only the authority present in spec.

Types

errors()

@type errors() :: [map()]

manifest()

@type manifest() :: map()

source_bundle()

@type source_bundle() :: binary() | %{optional(atom() | binary()) => term()}

Functions

module(program)

@spec module(Hyperlang.Program.t()) :: module()

Return the loaded confined module for an immutable program value.

The module is a GenServer callback module. Start it with GenServer.start(Hyperlang.module(program), {spec, arg}); init/1 receives arg, and every callback runs under spec. Raises ArgumentError when the program cannot be loaded.

From init/1 until it exits, an actor pins the program's code slot, so the module stays loaded under it even after release or code-pool pressure. The pin is charged to the actor's scope (spec.policy.scope, else the actor's own default scope), which may pin at most max_code_pins distinct slots. init/1 stops with a code_pool error when that limit is reached (pin_limit) or when the slot was reclaimed before the actor started (not_loaded); call module/1 again and restart.

program(code)

@spec program(binary() | map()) :: {:ok, Hyperlang.Program.t()} | {:error, errors()}

Compile an immutable, closed-world source value.

Source maps use %{entry: "name.erl", files: %{"name.erl" => source}} and may carry a compiler manifest under :manifest. A binary is the compact single-source form.

program(key, loader_fn)

@spec program(term(), (-> binary()
                    | map()
                    | {:ok, binary() | map()}
                    | {:error, term()})) ::
  {:ok, Hyperlang.Program.t()} | {:error, errors()}

Return a program cached under a caller-owned key, compiling the loader's source value on a miss.

run(spec, program, export, args)

@spec run(map(), Hyperlang.Program.t(), atom() | binary(), list()) ::
  {:ok, term(),
   %{
     stdout: [binary()],
     reductions: non_neg_integer(),
     wall_us: non_neg_integer()
   }}
  | {:error, term()}

Run a declared export using only the authority present in spec.

spec is %{bindings: %{name => {module, state}}, policy: map} plus an optional syscall host door. bindings must name exactly the bindings the program's manifest declares, with the same modules. export is :entry (the manifest result root, called with hd(args)), an atom, or an "name/arity" export id.

Success returns {:ok, value, meta} where meta is %{stdout: [binary], reductions: non_neg_integer, wall_us: non_neg_integer}: the sanitized guest stdout chunks in order, the reductions the guest call consumed in its own process, and the wall time of the call in microseconds. Failure returns {:error, error} with a sanitized error map.