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/3turns 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/3does 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/1andexit_text/1describe any term without running anything of it: no protocol of its structs, no exception message callback (an exception's:messagefield is read as data). That is what a trusted process uses for a term it still receives, a:DOWNreason say. Both are total;plain/1turns 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.
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
@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.
@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.
@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).
@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).
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)?