wasm_snapshot (wasm v0.3.0)

View Source

Initialized runtime snapshots: an immutable copy of an already-started guest.

Captured after a runtime's init() has returned and while no call is running, and restored into a fresh instance at that point, so interpreter startup is skipped and one-instance-per-request isolation is kept exactly as it was.

Call it an initialized runtime snapshot or an instance snapshot, and not a heap snapshot: wasm_heap here already means the garbage-collected object store, which is a narrower thing and is in fact one of the states a snapshot refuses to carry.

What it holds

Every linear memory's bytes, size and limits; mutable globals; table contents; and the dropped data and element segment sets. Nothing else, and the omissions are the design:

Ownership metadata is exactly what must not be captured. Keeper resource ids, holder tokens, owner pids and live handles are identities of this instance in this node at this moment, and an image carrying them would restore into references to something already destroyed. Restore mints fresh ones, reserving through wasm_keeper as instantiation does. What enters the image is only what can be reconstructed from it.

Host and operating-system resources are reconstructed per request, never serialised: open files and directories, sockets, clocks, random providers, Erlang closures and processes, and whatever state an arbitrary host import maintains. The caller builds those fresh for each restore.

What it refuses

Loudly, and never half-applied:

why
an instance with no retained module handleprovenance cannot be proved
an imported memory, table or globalthe aliasing an image cannot represent
a shared memorythe same, plus another thread may be writing it
a non-empty wasm_heapreferences are ids into a store, not values
an external funcref or any externrefit names an instance that is not this one

The first is the one that is easy to get wrong. An identity is a name and wasm:compile/2 lets a caller supply one, so loading module A under hash H and compiling a different B with identity => {sha256, H} gives B a claim on H that succeeds. A claim proves the cache holds something under that name, never that this instance came from it, so #inst.module_handle is retained at instantiation and capture requires it.

Restore does not run the start function

Ordinary instantiation always does, and a start function is arbitrary guest code with arbitrary host effects: running it during a restore would repeat, against fresh imports, work the captured image already contains. This is the single most important sentence here.

Summary

Types

What a hook may keep: no pid, port, reference or fun.

An immutable image. Copyable between processes; see wasm:acquire/1.

Functions

Capture, given the module the instance was built from.

An image from what a file held, bound to a module the caller resolved.

Size, module and version, so a size has a way to be read.

The address space an image covers, as against the bytes it retains.

The process holding this image, or undefined before one is attached.

Build a fresh instance at the captured point.

An image's contents, in the plain shape wasm_snapshot_file encodes.

Attach the process that holds this image's claim and its charge.

Types

annotated()

-type annotated() ::
          {Height :: non_neg_integer(), instr()} |
          {Height :: non_neg_integer(), Base :: non_neg_integer(), instr()}.

captured_mem()

-type captured_mem() :: #{pages := non_neg_integer(), runs := [{non_neg_integer(), binary()}]}.

externtype()

-type externtype() ::
          {func, typeidx()} |
          {table,
           #tabletype{limits ::
                          #limits{min :: non_neg_integer(),
                                  max :: undefined | non_neg_integer(),
                                  shared :: boolean(),
                                  index_type :: i32 | i64},
                      elemtype :: reftype(),
                      init :: undefined | [instr()]}} |
          {mem,
           #memtype{limits ::
                        #limits{min :: non_neg_integer(),
                                max :: undefined | non_neg_integer(),
                                shared :: boolean(),
                                index_type :: i32 | i64}}} |
          {global, #globaltype{valtype :: valtype(), mut :: mut()}} |
          {tag, #tagtype{type :: typeidx()}}.

funcidx()

-type funcidx() :: non_neg_integer().

heaptype()

-type heaptype() ::
          func | extern | exn | any | eq | i31 | struct | array | nofunc | noextern | noexn | none |
          {type, typeidx()}.

instr()

-type instr() :: atom() | tuple().

memidx()

-type memidx() :: non_neg_integer().

mut()

-type mut() :: const | var.

numtype()

-type numtype() :: i32 | i64 | f32 | f64.

portable()

-type portable() :: binary() | number() | atom() | [portable()] | tuple() | #{portable() => portable()}.

What a hook may keep: no pid, port, reference or fun.

Restricted by construction and checked at capture, because a hook free to return any term could quietly contradict the promise that host resources are never captured.

reftype()

-type reftype() :: {ref, null | nonull, heaptype()}.

snapshot()

-nominal snapshot() ::
             #snapshot{id :: reference(),
                       handle :: wasm_module_cache:handle(),
                       version :: binary(),
                       key :: term(),
                       source :: reference(),
                       globals :: tuple(),
                       tables :: [[term()]],
                       mems :: [captured_mem()],
                       dropped_elems :: map(),
                       dropped_datas :: map(),
                       hooks :: #{binary() => portable()},
                       bytes :: non_neg_integer(),
                       owner :: undefined | pid()}.

An immutable image. Copyable between processes; see wasm:acquire/1.

tableidx()

-type tableidx() :: non_neg_integer().

typeidx()

-type typeidx() :: non_neg_integer().

valtype()

-type valtype() :: numtype() | vectype() | reftype().

vectype()

-type vectype() :: v128.

Functions

bytes/1

-spec bytes(snapshot()) -> non_neg_integer().

capture/3

-spec capture(#inst{id :: reference(),
                    ckpt :: reference(),
                    entry_key :: undefined | reference(),
                    types :: tuple(),
                    funcs :: tuple(),
                    exports :: #{binary() => {func | table | mem | global, non_neg_integer()}},
                    elems :: tuple(),
                    datas :: tuple(),
                    globaltypes :: tuple(),
                    tags :: tuple(),
                    canon :: tuple(),
                    fields :: tuple(),
                    kinds :: tuple(),
                    supers :: tuple(),
                    heap :: undefined | wasm_heap:heap(),
                    identity :: undefined | {sha256, binary()} | reference(),
                    module_handle :: undefined | wasm_module_cache:handle(),
                    leases :: undefined | atomics:atomics_ref(),
                    store :: term(),
                    version :: term(),
                    limits :: map(),
                    ctx :: term()},
              wasm_module_cache:handle(),
              map()) ->
                 {ok, snapshot()} | {error, wasm_error:error()}.

Capture, given the module the instance was built from.

The caller resolves the handle rather than this module doing it, which keeps wasm_snapshot out of the wasm and wasm_module_cache cycle: wasm_architecture_SUITE documents exactly three, and a fourth member is a change to the architecture rather than a detail.

from_parts/2

-spec from_parts(wasm_snapshot_file:parts(), wasm_module_cache:handle()) ->
                    {ok, snapshot()} | {error, wasm_error:error()}.

An image from what a file held, bound to a module the caller resolved.

The caller supplies the handle rather than the file naming one it trusts: a planted image would otherwise pick whichever resident module suits it, which is the forgery restore/3 closes on the live path by taking the module from the image. Off disk the direction inverts, so the caller decides which module an image is for and this refuses one that does not match.

info/1

-spec info(snapshot()) ->
              #{bytes := non_neg_integer(), module := wasm_module_cache:handle(), version := binary()}.

Size, module and version, so a size has a way to be read.

logical_bytes/1

-spec logical_bytes(snapshot()) -> non_neg_integer().

The address space an image covers, as against the bytes it retains.

bytes is what the budget charges and what an image holds; this is what a restore will write into. They differ by about 6x on a started CPython, and a decompression ceiling has to come from the second.

module_of/1

-spec module_of(snapshot()) -> wasm_module_cache:handle().

owner/1

-spec owner(snapshot()) -> undefined | pid().

The process holding this image, or undefined before one is attached.

restore/4

-spec restore(snapshot(),
              #module{identity :: undefined | {sha256, binary()} | reference(),
                      types ::
                          [#subtype{final :: boolean(),
                                    supers :: [typeidx()],
                                    body ::
                                        #functype{params :: [valtype()], results :: [valtype()]} |
                                        #structtype{fields ::
                                                        [#fieldtype{type :: valtype() | i8 | i16,
                                                                    mut :: mut()}]} |
                                        #arraytype{field ::
                                                       #fieldtype{type :: valtype() | i8 | i16,
                                                                  mut :: mut()}}}],
                      rec_groups :: [{non_neg_integer(), non_neg_integer()}],
                      imports :: [#import{module :: binary(), name :: binary(), desc :: externtype()}],
                      funcs ::
                          [#func{type :: typeidx(),
                                 locals :: [valtype()],
                                 body :: [instr()] | {validated, [annotated()]}}],
                      tables ::
                          [#tabletype{limits ::
                                          #limits{min :: non_neg_integer(),
                                                  max :: undefined | non_neg_integer(),
                                                  shared :: boolean(),
                                                  index_type :: i32 | i64},
                                      elemtype :: reftype(),
                                      init :: undefined | [instr()]}],
                      mems ::
                          [#memtype{limits ::
                                        #limits{min :: non_neg_integer(),
                                                max :: undefined | non_neg_integer(),
                                                shared :: boolean(),
                                                index_type :: i32 | i64}}],
                      tags :: [#tagtype{type :: typeidx()}],
                      globals ::
                          [#global{type :: #globaltype{valtype :: valtype(), mut :: mut()},
                                   init :: [instr()]}],
                      exports ::
                          [#export{name :: binary(),
                                   desc :: {func | table | mem | global | tag, non_neg_integer()}}],
                      start :: undefined | funcidx(),
                      elems ::
                          [#elem{type :: reftype(),
                                 init :: [[instr()]],
                                 mode :: passive | declarative | {active, tableidx(), [instr()]}}],
                      datas ::
                          [#data{init :: binary(), mode :: passive | {active, memidx(), [instr()]}}],
                      data_count :: undefined | non_neg_integer(),
                      customs :: [{binary(), binary()}]},
              map(),
              map()) ->
                 {ok,
                  #inst{id :: reference(),
                        ckpt :: reference(),
                        entry_key :: undefined | reference(),
                        types :: tuple(),
                        funcs :: tuple(),
                        exports :: #{binary() => {func | table | mem | global, non_neg_integer()}},
                        elems :: tuple(),
                        datas :: tuple(),
                        globaltypes :: tuple(),
                        tags :: tuple(),
                        canon :: tuple(),
                        fields :: tuple(),
                        kinds :: tuple(),
                        supers :: tuple(),
                        heap :: undefined | wasm_heap:heap(),
                        identity :: undefined | {sha256, binary()} | reference(),
                        module_handle :: undefined | wasm_module_cache:handle(),
                        leases :: undefined | atomics:atomics_ref(),
                        store :: term(),
                        version :: term(),
                        limits :: map(),
                        ctx :: term()}} |
                 {error, wasm_error:error()} |
                 {error,
                  wasm_error:error(),
                  #inst{id :: reference(),
                        ckpt :: reference(),
                        entry_key :: undefined | reference(),
                        types :: tuple(),
                        funcs :: tuple(),
                        exports :: #{binary() => {func | table | mem | global, non_neg_integer()}},
                        elems :: tuple(),
                        datas :: tuple(),
                        globaltypes :: tuple(),
                        tags :: tuple(),
                        canon :: tuple(),
                        fields :: tuple(),
                        kinds :: tuple(),
                        supers :: tuple(),
                        heap :: undefined | wasm_heap:heap(),
                        identity :: undefined | {sha256, binary()} | reference(),
                        module_handle :: undefined | wasm_module_cache:handle(),
                        leases :: undefined | atomics:atomics_ref(),
                        store :: term(),
                        version :: term(),
                        limits :: map(),
                        ctx :: term()}}.

Build a fresh instance at the captured point.

The caller does not supply the module. It comes from the handle the image retained, which is why this takes a snapshot rather than being an option to instantiate/3. That option reopened the forgery on the other side: capture module A under H, then restore with an inline module B built with identity => {sha256, H} and a matching key, and the image is laid over a different module's layout. Taking the module from the image closes it by construction, because there is no argument left to forge.

to_parts/1

-spec to_parts(snapshot()) -> wasm_snapshot_file:parts().

An image's contents, in the plain shape wasm_snapshot_file encodes.

Self-references are normalised. A funcref names its defining instance by a reference(), which means nothing outside this node and nothing after a restart, so every one is rewritten to the atom self on the way out and back to a fresh identity on the way in. That is the same relocation reloc/3 does between two live instances, carried across a file.

with_owner/2

-spec with_owner(snapshot(), pid()) -> snapshot().

Attach the process that holds this image's claim and its charge.