Skip to content

WASM runtime: per-request instantiation, engine-global plugin limits, and a seven-function WASI surface #138

Description

@ndreno

Three properties of the WASM plugin runtime that cost something on every request today, and that also gate any future heavyweight plugin. Surfaced while assessing WAF integration options in ADR-0031, where they were prerequisites for the sidecar and TinyGo routes. The route that ADR chose is a native pipeline stage, so none of this blocks the WAF work. Filing it separately so it can be scheduled on its own merits.

They are independent of each other and can land in any order.


P1. A fresh WASM instance is created and init()-ed on every request, per middleware

InstancePool::get_instance builds a new PluginInstance and calls init(config) each time it is invoked:

  • crates/barbacane-wasm/src/pool.rs:223 (get_instance)
  • crates/barbacane-wasm/src/pool.rs:260 (instance.init(&config_json))

and it is invoked per request, per middleware:

  • crates/barbacane/src/main.rs:1576 (middleware chain construction)
  • crates/barbacane/src/main.rs:1866 (dispatch)

Modules are AOT-compiled and cached, so this is instantiation plus guest-side init, not compilation. For the current plugin set that is cheap but not free: a request through five middlewares pays five instantiations and five init calls, and init runs guest code that parses the plugin's config JSON.

The name InstancePool suggests instances are pooled. They are not; the type caches modules and configs.

What to do: reuse instances keyed on (plugin, config), which is what InstanceKey already models, so init runs once per key rather than once per request. Needs a decision on guest state: a reused instance carries whatever the guest left in its linear memory, so either instances are reset between requests or plugins gain a documented contract that on_request must not depend on prior state. That contract question is the real work here, not the pooling.

Unmeasured. I have not benchmarked this, so I am not claiming a number. A benchmark comparing the current path against a reused instance across the existing plugin set would size it, and is worth doing before the change rather than after.

P2. Plugin resource limits are engine-global, so every plugin gets the largest plugin's ceiling

PluginLimits defaults to 16 MB of linear memory, 100 ms wall clock and 100 M fuel (crates/barbacane-wasm/src/limits.rs:28), and the data plane installs one set for the whole engine:

  • crates/barbacane/src/main.rs:734

The floor is then scaled by max_body_size, so a deployment that accepts large uploads raises the ceiling for all 33 plugins at once, including the ones that only read a header.

What to do: per-plugin [limits] in plugin.toml, carried through PluginCapabilities into the artifact manifest and enforced per instance. This is also what would let a genuinely memory-hungry plugin exist without handing the same budget to everything else.

P3. The WASI surface is seven hand-written stubs, and a missing import fails at instantiation

crates/barbacane-wasm/src/instance.rs:2213-2371 defines stubs for random_get, clock_time_get, fd_write, sched_yield, environ_get, environ_sizes_get and proc_exit. Nothing calls Linker::define_unknown_imports_as_traps, so a plugin importing anything else fails to instantiate, with an error that points at the missing symbol rather than at the cause.

A wasm32-wasip1 guest from most toolchains also imports args_get, args_sizes_get, fd_close, fd_fdstat_get, fd_seek and poll_oneoff.

wasmtime-wasi is already declared as a workspace dependency (Cargo.toml:105) and is used nowhere: the only reference in the tree is the comment at instance.rs:2217 saying it could replace the stubs.

What to do: either adopt wasmtime-wasi with a capability-gated view, or keep the stubs and call Linker::define_unknown_imports_as_traps(&module) (plural, taking the module; verified against wasmtime 47.0.3). That gives every unresolved import a trapping stub, so instantiation succeeds and the trap fires only if the guest actually calls one. The second is smaller and arguably better for a sandbox: it keeps the import surface explicit, denies by default, and turns "this plugin will not load" into "this plugin trapped on a call it should not have made", which is easier to diagnose.

Corrected: an earlier version of this issue named the method in the singular, which does not exist.


Why this is worth doing independently of the WAF

P1 is the only one with a per-request cost today, and it applies to every plugin in every deployment. P2 and P3 are latent: they cost nothing until someone writes a plugin that needs more memory than its neighbours, or one built with a toolchain whose WASI imports differ, at which point they present as confusing failures rather than as limits.

None of the three is on the critical path for ADR-0031, which compiles rules at build time and evaluates them in a native pipeline stage next to the existing per-operation validators (crates/barbacane/src/main.rs:654-655).

Refs: ADR-0031, section "Runtime prerequisites (P1-P3)".

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions