wasm_snapshot_file (wasm v0.3.0)

View Source

The on-disk form of an initialized runtime snapshot.

Encoding and decoding only: this module opens no file, resolves no module and calls nothing that could reach the facade, so it stays outside the cycle wasm_architecture_SUITE asserts on. What it takes and answers is a plain map of an image's contents, which wasm_snapshot knows how to take apart and put back together.

Why not term_to_binary/1

For the reason the reaper's journal records: binary_to_term/1 on a planted file materialises atoms before any structural check can reject the record, and the atom table is node-wide and never reclaimed. A snapshot lives in a directory an embedder names, which is exactly where a planted file would go.

So the format is explicit, every field is validated on the way in, and an atom is decoded through binary_to_existing_atom/2, which cannot create one.

The shape

"WASMIMG\\0" | u16 format | u32 image ABI | u32 payload length
sha256 of the payload, checked before a byte of it is used
payload: u8 codec | u32 uncompressed length | body
body: sections, each u8 tag, u32 length, contents

The digest covers the payload as stored, so a corrupt file is refused before it is decompressed rather than after. The uncompressed length is checked against a caller-supplied ceiling first, because zlib:uncompress/1 on a crafted three-megabyte stream is otherwise a multi-gigabyte allocation.

What a reader must still not assume

A digest proves the bytes are the bytes that were written. It proves nothing about who wrote them, and a snapshot is not code but guest state injected into a live runtime, which is worse rather than better: a table slot holds a function index, so a planted image is a call to a function the module never exposed. wasm_snapshot checks every index against the module's own function count on the way back in, and the directory is documented as being as trusted as the release, in the same words wasm_code_cache uses.

Summary

Functions

Read an image, or say why it is not one.

The format this build writes. An older one is a miss, never an error.

The value-representation version, bumped by hand.

The atoms the runtime's own values are made of, listed so that loading this module interns them.

Types

parts()

-type parts() ::
          #{hash := binary(),
            version := binary(),
            key := term(),
            shape := term(),
            globals := [term()],
            tables := [[term()]],
            mems := [map()],
            dropped := {map(), map()},
            hooks := map()}.

Functions

decode/2

-spec decode(binary(), non_neg_integer()) -> {ok, parts()} | {error, wasm_error:error()}.

Read an image, or say why it is not one.

Max bounds the decompressed size and must come from the module, not from the file: a length a planted file supplies is not a bound on anything.

Every failure is the same shape, and a caller is expected to treat all of them as a miss rather than an error, which is what wasm_code_cache promises for its own reads and does not deliver.

encode(Parts)

-spec encode(parts()) -> binary().

format_version()

-spec format_version() -> pos_integer().

The format this build writes. An older one is a miss, never an error.

image_abi()

-spec image_abi() -> pos_integer().

The value-representation version, bumped by hand.

own_atoms()

-spec own_atoms() -> [atom()].

The atoms the runtime's own values are made of, listed so that loading this module interns them.

unterm/1 decodes a name through binary_to_existing_atom/2, which is right: nothing in a file may mint an atom. But "existing" is a property of the emulator at that moment, and Erlang loads modules lazily, so without this the answer depends on whether some unrelated module carrying the same literal happened to have been loaded first.

That is not hypothetical. A CPython image holds funcref in its tables, and on a node that had only started the application binary_to_existing_atom( <<"funcref">>, utf8) raised badarg: the image was refused, lookup/2 turned the refusal into a miss as it must, and the worker spent 104 seconds capturing a snapshot it already had on disk. test/audit/ATTEMPTS.md has that run.

Every name here is a literal in this module's source, so it is in this module's atom table and exists from the moment the module is loaded -- which is before it can decode anything. The set is exactly what wasm_snapshot's admissible admits, and the guarantee it restores is only about these names: an atom a hook kept is still subject to existing already, because that one really does come from outside.