Rho.Untrusted (rho v0.1.0)

Copy Markdown View Source

Text and data from code rho doesn't trust, the Dyn layer's (Rho.Dyn), for the processes it does: the loop, the Keeper, the caller of a compile or a selftest.

Turning a term into text can run code the term chose: Exception.message/1 calls the exception's module, and inspect/2, interpolation, to_string/1 and Jason.encode/1 dispatch protocols, which Dyn code may implement for its own structs. Run in a trusted process, that code could loop forever, raise, or send messages as that process. So:

  • format/3 turns a caught raise, throw or exit into text in the untrusted process that caught it (a tool's task, a selftest, a compile), in a throwaway helper with a time and heap bound that never ends in a crash report and dies with its caller; if that fails or runs over, it falls back to describing the term;
  • crash/3 does that and exits with {:rho_crash, text}, the exit reason a host's own untrusted processes can use so a watcher (the Keeper, say) reports the text as it is;
  • describe/1 and exit_text/1 describe any term without running anything of it: no protocol of its structs, no exception message callback (an exception's :message field is read as data). That is what a trusted process uses for a term it still receives, a :DOWN reason say. Both are total;
  • plain/1 turns a Dyn tool's answer into plain data (no structs, no funs) before it leaves the task.

Every text is valid UTF-8 and at most max bytes (4000 by default).

Crash reports are a separate path: OTP logs a crashed process's report and Elixir's logger translator formats it, calling the exception's message/1. Left alone, a Dyn exception whose message/1 throws or exits there makes OTP remove the translator VM-wide (later crash logs come out untranslated). Rho.Dyn.LogGuard, installed by the Keeper, replaces Dyn terms in log events before the translator sees them, which keeps that call out of the translator; it lets through an event too big to check (more than 10,000 terms before the Dyn one), and a Logger restart removes the filter.

This is about crashes and mistakes, not hostile code: it keeps a Dyn term the Core receives from running Dyn code there, which lowers the chance that a built tool's crash takes a trusted process down. Dyn code that sets out to reach a trusted process can still do so by other routes (see Rho.Dyn.Check).

Summary

Functions

Formats the caught kind, reason (format/4) and exits with it as the reason ({:rho_crash, text}), so whoever watches this process gets the text, not the term: exit_text/1 reads it back. An orderly exit (:normal, :shutdown, {:shutdown, _}) stays as it was.

Describes term without running any of its code: inspect/2 with structs: false (a struct prints as the map it is, so no Inspect implementation runs), integers past 1024 bits as a placeholder (their decimal text costs seconds), cut to max bytes.

Text for an exit reason (a :DOWN or :EXIT message's), without running any of its code: an exception with its stacktrace reads as ** (Module) message and the stack (the :message field when it is a string, else the exception's fields), a reason a process formatted itself (crash/3) as that text, anything else as describe/1 does. Total: any term gives a text (improper lists, odd stack entries and deep or huge terms are described, cut to max).

A caught kind, reason, stack as text, as Exception.format/3 writes it (banner and stacktrace), formatted in a throwaway helper process so the caller's own mailbox and state stay out of reach. The helper has a heap limit, never ends in a crash report and dies with its caller; if it fails or runs over :timeout_ms (1000), the term is described instead (as exit_text/2 and describe/2 do). Call it in the untrusted process that caught the error, never in a trusted one. Options: :timeout_ms, :max (bytes).

term as plain data: every struct a map of its fields (without __struct__ and __exception__), every fun its describe/1 text, through lists, tuples and map keys and values. What a Dyn tool answers goes through it in its own task, before anything trusted touches it.

Is module untrusted code: a Dyn generation's module or a protocol implementation for one (Rho.Dyn.dyn_module?/1)?

Functions

crash(kind, reason, stack)

@spec crash(:error | :exit | :throw, term(), Exception.stacktrace()) :: no_return()

Formats the caught kind, reason (format/4) and exits with it as the reason ({:rho_crash, text}), so whoever watches this process gets the text, not the term: exit_text/1 reads it back. An orderly exit (:normal, :shutdown, {:shutdown, _}) stays as it was.

describe(term, max \\ 4000)

@spec describe(term(), pos_integer()) :: String.t()

Describes term without running any of its code: inspect/2 with structs: false (a struct prints as the map it is, so no Inspect implementation runs), integers past 1024 bits as a placeholder (their decimal text costs seconds), cut to max bytes.

exit_text(reason, max \\ 4000)

@spec exit_text(term(), pos_integer()) :: String.t()

Text for an exit reason (a :DOWN or :EXIT message's), without running any of its code: an exception with its stacktrace reads as ** (Module) message and the stack (the :message field when it is a string, else the exception's fields), a reason a process formatted itself (crash/3) as that text, anything else as describe/1 does. Total: any term gives a text (improper lists, odd stack entries and deep or huge terms are described, cut to max).

format(kind, reason, stack, opts \\ [])

@spec format(:error | :exit | :throw, term(), Exception.stacktrace(), keyword()) ::
  String.t()

A caught kind, reason, stack as text, as Exception.format/3 writes it (banner and stacktrace), formatted in a throwaway helper process so the caller's own mailbox and state stay out of reach. The helper has a heap limit, never ends in a crash report and dies with its caller; if it fails or runs over :timeout_ms (1000), the term is described instead (as exit_text/2 and describe/2 do). Call it in the untrusted process that caught the error, never in a trusted one. Options: :timeout_ms, :max (bytes).

plain(map)

@spec plain(term()) :: term()

term as plain data: every struct a map of its fields (without __struct__ and __exception__), every fun its describe/1 text, through lists, tuples and map keys and values. What a Dyn tool answers goes through it in its own task, before anything trusted touches it.

untrusted?(module)

@spec untrusted?(module()) :: boolean()

Is module untrusted code: a Dyn generation's module or a protocol implementation for one (Rho.Dyn.dyn_module?/1)?