Rho.Dyn.Check (rho v0.1.0)

Copy Markdown View Source

The static check every staged file passes before it is compiled (the pipeline's step 2, see Rho.Dyn), and beam/2, a second pass over the compiled binaries.

A best-effort guard against mistakes, not a sandbox. It is there for the model's mistakes: a tool that by accident calls something it shouldn't, reaches into the Core, or installs code in a process that runs for others. It does not hold against code written to get around it; the BEAM has no capability isolation. Known ways around it, for a host with stronger needs:

  • an atom handed in as data and called without parentheses, a function value smuggled in through a message;
  • raw messages to OTP's processes, a report_cb sent to :logger_proxy say, which then runs Dyn code there;
  • modules the deny-lists don't name: Logger.App (stopping Logger drops Rho.Dyn.LogGuard), Logger.Backends.*, LoggerBackends;
  • starting a child under a trusted supervisor (:supervisor, Supervisor, DynamicSupervisor call the child's start function in the supervisor), :alarm_handler (a :gen_event wrapper);
  • CPU, memory and message floods.

What keeps a mistake from sticking is the rest of the pipeline: versioned modules, selftests in contained processes, screen-lock approval, probation and automatic revert.

Source rules (each violation carries file:line; <ns> is the namespace, Rho.Dyn.Config):

  • only defmodule at the top level of a file, named <ns>.* (<ns>.G<n> is reserved for compiled generations); no defprotocol, macros (defmacro, quote, unquote) or @on_load; defimpl only inside a module and for a Dyn module (for: left out, or a <ns>.* module), so the implementation is compiled as <Protocol>.<ns>.G<n>.X, which beam/2 lets through;
  • no calls to, or references of: :code, Code, Module, Port, Node, Mob.Dist, MobDeliver (the Core's own updates), File (Dyn code's files go through a Core module the host allows, inside its roots), Path.wildcard/2, :init, :file, :os.cmd, :erlang.halt and the other code-loading and node-control functions listed in the :partial defaults, System.halt/stop/cmd, Application.start/stop/put_env, dynamic atoms (String.to_atom, ...), :erlang.binary_to_term (it makes any atom, a forbidden one too, and funs), and Protocol.derive / consolidate (they define or replace implementations for any type, the stdlib's too; @derive and defimpl for a Dyn module are the way);
  • nothing that controls or watches other processes or the VM: tracing (:erlang.trace*, :trace, :seq_trace, :dbg, :erl_tracer), :erlang.suspend_process/resume_process, system_monitor / system_profile, :erts_debug, :erts_internal, :observer_backend, and listing every process (Process.list/0, :erlang.processes/0); :persistent_term only to read (put / erase change VM-wide state and trigger a global GC);
  • nothing that installs code to run in other processes: :logger, :error_logger, :logger_backends, :telemetry, :gen_event, :gen_statem (it calls any OTP server), and Logger's translators, backends and configuration (Logger.info/1 and friends are fine); a Dyn process's own crash report is kept out of the logger's processes by Rho.Dyn.LogGuard;
  • not the router's name, :mob_screen: whatever arrives there is handed to the front host as a native reply, so a front screen sending to it could forge a capability grant (a picked file); nor :mob_file_result, the tag of a raw native file result, which the front host decodes and grants when one arrives; nor the native layers :mob_nif and :mob_secure_store_nif as values, since one held in a variable could be called without parentheses (:forging, atoms Dyn code may not hold);
  • the Core's own modules (Rho.* and the host's :core prefixes) are off limits except Rho.Tool (the behaviour a Dyn tool implements) and the host's :allowed ones, and so is the sign-in store (MobSecureStore, its NIF :mob_secure_store_nif); of Mob only the app-facing modules (screens, UI, theming, device features; the :gated defaults), not its native layer :mob_nif (Mob's Elixir modules are the API, and the host decides which of them Dyn code gets), and of Mob.Screen not the functions that start or drive screens outside the front (start_root/3, ...); the capability plugins (MobCamera, MobLocation, MobSensors, MobMishka, MobThemes, ...), Nx and NxTfliteMob are allowed;
  • Process.exit/2 only on self(); apply/3, spawn/3 and friends only with a module written out literally; no calls on a module held in a variable (mod.fun()); no :"Elixir.Foo" atoms; modules with forbidden functions (System, :erlang, Process, ...) may be called but not passed around as values or imported.

Aliases (alias System, as: S), multi-aliases and ~MOB templates (whose {...} expressions only exist once the sigil expands, :sigils) are resolved first, so S.halt() and <Text text={System.halt()} /> are caught too.

The lists are a host seam (config :rho, :dyn, check: [...], merged over defaults/0: Operator's lists minus the entries naming Operator's own code; see Rho.Dyn.Config).

Summary

Functions

The backstop on compiled generation n: every module of the result is the generation's (<ns>.G<n>.*) or a protocol implementation for one of them, named as Elixir names it (Inspect.<ns>.G<n>.X, from a defimpl or a library's use), and none may call a forbidden function (import table) or name a forbidden module (atom table), whatever macro produced the code.

The default lists, as config :rho, :dyn, check: [...] takes them: Operator's, except for the entries that name Operator's own code (its sign-in NIF :operator_secure_store, and the caller-side functions of Operator.Core.Files), which its config adds. The host's entries are merged over these, keys written as modules or strings alike (a nil value drops one; :handlers replaces the list).

Violations as text, one file:line: message per line (what a rejection's reason says).

Is mod (bin) one of generation n's modules, or a protocol implementation for one of the build's (mods), named as Elixir names it?

The check's rules as they apply now: the defaults with the host's config over them.

Parses and checks sources (%{relative_path => source}). Returns the parsed files, or every violation found.

The template sigils whose expressions are code (:sigils), as their macro names.

Types

target()

@type target() :: :dyn | :dynamic | {:elixir, String.t()} | {:erlang, atom()}

violation()

@type violation() :: %{
  file: String.t(),
  line: non_neg_integer() | nil,
  message: String.t()
}

Functions

beam(binaries, n)

@spec beam([{module(), binary()}], pos_integer()) :: :ok | {:error, [violation()]}

The backstop on compiled generation n: every module of the result is the generation's (<ns>.G<n>.*) or a protocol implementation for one of them, named as Elixir names it (Inspect.<ns>.G<n>.X, from a defimpl or a library's use), and none may call a forbidden function (import table) or name a forbidden module (atom table), whatever macro produced the code.

defaults()

@spec defaults() :: keyword()

The default lists, as config :rho, :dyn, check: [...] takes them: Operator's, except for the entries that name Operator's own code (its sign-in NIF :operator_secure_store, and the caller-side functions of Operator.Core.Files), which its config adds. The host's entries are merged over these, keys written as modules or strings alike (a nil value drops one; :handlers replaces the list).

format(violations)

@spec format([violation()]) :: String.t()

Violations as text, one file:line: message per line (what a rejection's reason says).

generation_module?(mod, bin, n, mods)

@spec generation_module?(module(), binary(), pos_integer(), MapSet.t(module())) ::
  boolean()

Is mod (bin) one of generation n's modules, or a protocol implementation for one of the build's (mods), named as Elixir names it?

policy()

@spec policy() :: map()

The check's rules as they apply now: the defaults with the host's config over them.

run(sources)

@spec run(%{required(String.t()) => String.t()}) ::
  {:ok, [{String.t(), Macro.t()}]} | {:error, [violation()]}

Parses and checks sources (%{relative_path => source}). Returns the parsed files, or every violation found.

sigils()

@spec sigils() :: [atom()]

The template sigils whose expressions are code (:sigils), as their macro names.