Atom vocabularies and safe ETF

Copy Markdown View Source

SafeRPC decodes frames with:

:erlang.binary_to_term(binary, [:safe])

The :safe option refuses to create new atoms. This protects the VM atom table, but it means a client must already have any atoms that appear in a reply.

Server compile time

SafeRPC builds a service atom vocabulary at compile time. For use SafeRPC services it includes:

  • the service atom from service: ...;
  • operation module and function atoms;
  • literal atoms from @rpc specs;
  • literal atoms from @rpc function bodies;
  • literal atoms from @rpc options;
  • optional explicit atoms from use SafeRPC, atoms: ....

SafeRPC only inspects RPC boundaries. It does not collect atoms from unrelated helper functions.

Example:

defmodule MyApp.AdminRPC do
  use SafeRPC, service: :my_app

  @rpc true
  @spec status(map(), map(), term()) :: {:ok, :ready | :degraded}
  def status(_payload, _meta, _state), do: {:ok, :ready}
end

The vocabulary contains :my_app, MyApp.AdminRPC, :status, :ok, :ready, and :degraded.

Frameworks that generate SafeRPC services should put compile-time metadata that crosses the RPC boundary in the generated @rpc body. SafeRPC will see literal maps, structs, tuples, and keyword lists there and collect their atoms without framework-specific hooks.

Client runtime

The client asks for the vocabulary at runtime:

{:ok, atoms} = SafeRPC.atoms(client)

Then it can validate and intern it:

:ok =
  SafeRPC.prepare(client,
    max_atoms: 1_000,
    max_atom_length: 128,
    allow: [~r/^[a-z][a-z0-9_]*$/, ~r/^Elixir\.MyApp\./]
  )

prepare/2 never decodes arbitrary atom-rich replies. It receives atom names as strings, applies the client policy, and only then calls String.to_atom/1 for accepted names.

Dynamic atoms

SafeRPC cannot infer atoms created only at runtime, for example atoms coming from a database row or user input. Prefer strings for unbounded values. If a runtime atom set is finite and intentional, declare it explicitly:

use SafeRPC, service: :my_app, atoms: [:queued, :running, :failed]

What this does not guarantee

Safe ETF protects the Erlang runtime from creating unbounded atoms while decoding. It does not validate application semantics. Servers should still authorize requests, and clients should still validate replies according to application rules.