Skip to content

Instantly share code, notes, and snippets.

@rikatz
Last active September 28, 2026 16:38
Show Gist options
  • Select an option

  • Save rikatz/415e3d25f3770b8eed0991fdf9b7d37c to your computer and use it in GitHub Desktop.

Select an option

Save rikatz/415e3d25f3770b8eed0991fdf9b7d37c to your computer and use it in GitHub Desktop.
Envoy WASM - reload and memory

Coraza WASM memory growth: fix plan

Scope and source finding

The operator writes each renewed service account token into WasmPlugin.spec.pluginConfig.cache_token (coraza-kubernetes-operator/internal/controller/engine_controller_wasm_driver.go). Envoy includes the plugin configuration in the plugin key (envoy/source/extensions/common/wasm/plugin.cc), so a new token creates a new plugin root. Istio 1.30.3 strips its changing resource-version VM variable before forwarding the configuration, and Envoy keys the VM by its VM configuration and WASM bytes. With unchanged code and VM settings, successive roots share one worker VM.

The Coraza guest fixes are already deployed. The remaining fixes must release root-owned state without destroying state still used by another root on that VM. This plan is based on source inspection only; no process was run or measured.

1. Release Envoy timers when a root is deleted

Owner: envoy/source/extensions/common/wasm/wasm.{h,cc} and context.cc.

  • Add a root cleanup method on Envoy's Wasm that disables and erases timer_[root_id] and erases the inherited timer_period_[root_id]. Call it when an Envoy Context representing a root is destroyed, after the guest's on_done/on_delete lifecycle. Do not clean up a stream context or the VM context.
  • Make setTimerPeriod(root_id, 0) erase both entries as well. It currently leaves map entries behind even when the guest disables its timer.
  • Guard tickHandler against a deleted root so a late callback cannot reschedule it. This is a backstop for shutdown ordering; the primary release happens at root destruction.

Why here: WasmBase::startShutdown(plugin_key) in the pinned proxy-wasm-cpp-host removes the root, but Envoy owns the dispatcher timer. The current callback reschedules even if getContext(root_id) is null. The operator's ruleset poll interval defaults to 15 seconds, so this path is normally active.

2. Give Envoy metric IDs the same root lifetime

Owner: envoy/source/extensions/common/wasm/context.cc and wasm.{h,cc}.

  • When Context::defineMetric allocates an ID, record that ID under the defining context's root ID. On that root's destruction, erase its IDs from Wasm::counters_, gauges_, and histograms_, then erase the root's ID list.
  • Keep Envoy's underlying named stats intact: a later root may use the same counter name, and clearing an ID-to-stat pointer is different from deleting a shared stat. This bounds bookkeeping created by token refreshes while preserving metric values.
  • Cover metrics defined from a child context by resolving its root ID, rather than assuming all definitions happen directly on the root.

Why here: coraza-proxy-wasm/wasmplugin/metrics.go creates a new per-root metric registry. Envoy currently assigns new IDs for the same metric names on each root and retains their VM-level lookup entries.

3. Bound the proxy-wasm host's canary cache

Owner: proxy-wasm-cpp-host/include/proxy-wasm/wasm.h and src/wasm.cc; then update Envoy's pinned host dependency in envoy/MODULE.bazel and its dependency metadata.

  • Bound WasmHandleBase::plugin_canary_cache_ or evict a key when its final plugin handle is released. Prefer the smallest change that preserves canary validation for active configurations. Cache misses may revalidate a configuration; stale token-derived keys must not remain forever.
  • Land the host fix upstream or in a pinned fork. Changing a local copy of host source has no effect until Envoy consumes the new pinned revision.

Why here: the base WASM handle survives while worker clones use it, and canary() adds one result for every distinct token-derived plugin key without eviction. This is a smaller retention path than timers or guest WAF state, but it is unbounded.

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