Skip to content

Linear Memory

Core WebAssembly memory is a contiguous byte buffer owned by the instance. The guest sees offsets inside that buffer; the host must translate every pointer and length before touching memory.

Common patterns:

  • exported functions return scalar values directly,
  • strings and byte arrays are represented as pointer plus length,
  • the guest exports allocation and deallocation helpers,
  • the host writes input data into guest memory, calls an exported function, then reads the result range back.

Validate bounds on both reads and writes. Never trust a pointer just because it came from the guest. A valid pointer can still point to a value with the wrong shape for the protocol you expected.

For structured plugin contracts, use WIT and the Component Model. The canonical ABI still uses linear memory underneath, but the generated bindings own the lowering and lifting rules.

Instance Memory Policy

WebAssembly memory is measured in 64 KiB pages. MemoryLimits stores the initial and maximum page counts, with the WebAssembly maximum capped at 65536 pages.

Use WasmMemoryPolicy when instantiating a module. The policy bounds every imported and defined memory, limits the number of memories, and reserves no more than the aggregate growth budget:

val memoryPolicy =
    WasmMemoryPolicy(
        maxBytesPerMemory = 64L * 1024L * 1024L,
        maxTotalBytes = 96L * 1024L * 1024L,
        maxMemories = 2,
    )

val instance =
    Instance.builder(module)
        .withMemoryPolicy(memoryPolicy)
        .build()

Both byte limits must be positive multiples of 64 KiB. At build time, imported memories are rejected when their current or declared maximum size exceeds the per-memory or aggregate budget. Defined memories receive effective maximums that respect both budgets, so subsequent memory.grow operations cannot exceed them. The same policy is applied by the selected platform engine; it is not necessary to duplicate it with WasmtimeExecutionConfig.maxMemoryBytes.

withMemoryLimits(...) remains as a compatibility API for a module with at most one defined memory. It does not account for imported memories or aggregate growth and rejects modules with multiple defined memories. Prefer withMemoryPolicy(...) for untrusted or multi-memory modules.

Default Limits

Without withMemoryPolicy(...) or withMemoryLimits(...), defined memories still receive default bounds. Each defined memory is capped at WasmtimeExecutionConfig.maxMemoryBytes (256 MiB unless configured) and the instance may declare at most WasmtimeExecutionConfig.maxMemories memories (16 unless configured). A module whose initial memory size exceeds the per-memory cap fails with UninstantiableException before any memory is allocated, and a larger declared maximum is reduced to the cap. Imported memories are host-owned and are not re-checked. Defined tables are checked the same way against WasmtimeExecutionConfig.maxTableElements (1,000,000 elements unless configured; WasmtimeUnlimitedResourceLimit disables the check), and array.new / array.new_default in constant expressions accept at most 10,000,000 elements.

These defaults exist so that a hostile module cannot make the host allocate gigabytes during Instance.builder(module).build(). They are not a substitute for an explicit policy: set WasmMemoryPolicy to budget memory for untrusted plugins, including the aggregate across memories.

The host materializes instance memories and tables while building the instance, within the limits above, before handing the module to the engine. The memories reachable through Instance.memory(...) after a successful build are owned by the active platform engine. On JVM, Android, and iOS this means the linked Wasmtime backend; on wasmJs this means the browser or Node WebAssembly engine.