Skip to content

fix: Safely reset wasmtime memory per instance reset#157

Merged
AndrewSisley merged 5 commits into
sourcenetwork:mainfrom
edjroz:fix/store-per-instance-reset
Jun 3, 2026
Merged

fix: Safely reset wasmtime memory per instance reset#157
AndrewSisley merged 5 commits into
sourcenetwork:mainfrom
edjroz:fix/store-per-instance-reset

Conversation

@edjroz

@edjroz edjroz commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Relevant issue(s)

Resolves #155

Description

The wasmtime runtime leaks wasm linear memory on every Reset() call, eventually crashing the host process under a large set of inputs.

Fix (store-per-instance + re-instantiate on Reset):

  • wRuntime now holds only the shared wasmtime.Engine (so module compilation is still reused); each module.Instance owns its own wasmtime.Store.
  • Reset() creates a fresh store, re-instantiates the module, re-applies set_param, and swaps it in behind a shared *instanceHandles pointer so the Alloc/Transform/Memory closures see the new store transparently.
  • The displaced store is explicitly Close()d. Relying on Go GC finalizers is not enough: the store's linear memory is C-allocated and invisible to Go's GC pressure heuristics, so dropped stores pile up faster than they are collected. Measured host RSS growth without the explicit close was ~127 KiB per Reset cycle; with it, RSS stays flat.
  • module.Memory gained a Size() method. Note this is a compile-time breaking change for any external implementers of that interface; all in-repo implementations (wasmtime, wazero, js, BytesMemory) are updated.

Tasks

  • I made sure the code is well commented, particularly hard-to-understand areas.
  • I made sure the repository-held documentation is changed accordingly.
  • I made sure the pull request title adheres to the conventional commit style (the subset used in the project can be found in tools/configs/validate-conventional-style.sh.
  • I made sure to discuss its limitations such as threats to validity, vulnerability to mistake and misuse, robustness to invalidation of assumptions, resource requirements, ...

How has this been tested?

  • New regression test TestWasm32ResetDoesNotLeakMemory (host-go/engine/tests/wasm32_pipeline_reset_test.go): drives a single instance through 2,000 Append/Reset cycles and asserts wasm linear memory growth stays within 2 pages after a 50-cycle warm-up.
  • Host RSS measurement (out-of-tree harness driving the same Reset loop and sampling process RSS via ps), since the in-tree test cannot observe memory held by dropped stores:
    • store-per-instance without explicit Close(): ~127 KiB/cycle RSS growth (linear).
    • store-per-instance with explicit Close(): flat (~5 KiB/cycle, amortizing to zero).
  • Full host-go test suite (make test), go vet, gofmt, and cross-compilation for GOOS=js GOARCH=wasm and GOOS=windows all pass.

Specify the platform(s) on which this was tested:

  • MacOS

edjroz added 5 commits June 2, 2026 21:12
Drives 2000 Reset() cycles; asserts linear memory growth <= 2 pages.
…work#155)

Drops and recreates wasmtime.Store on Reset(); gives dlmalloc a clean heap.
Keeps per-developer design/drawback notes out of the repo.
Dropped stores hold C memory invisible to Go GC; explicit Close keeps host RSS flat (~127KB/cycle -> ~5KB/cycle).
wazero/js runtimes still leak on Reset; the test only covers wasmtime.
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR fixes memory exhaustion in the Wasmtime runtime by switching from a single long-lived shared Store to creating a fresh Store per module instance. A Memory interface extension enables size reporting across all runtimes, and a regression test validates that repeated pipe.Reset() cycles no longer leak memory.

Changes

Wasmtime Memory Leak Fix via Per-Instance Store Management

Layer / File(s) Summary
Memory size tracking interface and BytesMemory implementation
host-go/engine/module/memory.go
Memory interface now declares Size() uint32 to report linear memory size. BytesMemory implements it by returning the underlying byte slice length cast to uint32.
Runtime memory size implementations
host-go/runtimes/js/memory.go, host-go/runtimes/wazero/memory.go
JavaScript and wazero memory wrappers each implement Size(): JS reads from Uint8Array length; wazero converts page count to bytes using the 64 KiB page size constant.
Wasmtime per-instance Store refactoring
host-go/runtimes/wasmtime/runtime.go
wRuntime now holds an Engine instead of a shared Store. Each instance creation calls newInstanceHandles to create a fresh Store per instance, wire imports/exports, and handle parameter marshaling. Reset now recreates the full instance and swaps handles in-place (closing the old Store) instead of restoring from a memory snapshot. A resetErr field gates subsequent Alloc/Transform calls if reset fails.
Memory leak regression test
host-go/engine/tests/wasm32_pipeline_reset_test.go
New test (build-restricted to non-Windows/non-JS) cycles a WASM instance through many pipe.Reset() iterations, records baseline memory after warm-up, and asserts net memory growth stays within bounds to catch leaks during repeated resets.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant wRuntime
  participant Engine
  participant InstanceHandles
  participant Store
  participant Module
  
  Client->>wRuntime: NewModule(moduleBytes)
  wRuntime->>Engine: NewModule(engine, moduleBytes)
  Engine-->>wRuntime: Compiled Module
  
  Client->>wRuntime: NewInstance()
  wRuntime->>InstanceHandles: newInstanceHandles(module)
  InstanceHandles->>Store: Create fresh Store
  Store-->>InstanceHandles: Store ready
  InstanceHandles->>Module: Instantiate with Store
  Module-->>InstanceHandles: Instance + Exports (memory, alloc, transform)
  InstanceHandles-->>wRuntime: handles (Store, memory, alloc, fn, instance)
  wRuntime-->>Client: module.Instance (backed by handles)
  
  Client->>Client: Use instance.Transform(), instance.Alloc(), etc.
  
  Client->>Client: Call instance.Reset()
  Client->>InstanceHandles: Reset
  InstanceHandles->>InstanceHandles: newInstanceHandles(module) again
  InstanceHandles->>Store: Create new Store
  InstanceHandles->>InstanceHandles: Swap handles in-place
  InstanceHandles->>Store: Close old Store
  Store-->>InstanceHandles: Old Store freed
  InstanceHandles-->>Client: Reset complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
Fix Wasmtime runtime memory exhaustion during large input processing [#155]

Out-of-scope changes

Code Change Explanation
Updated .gitignore to ignore docs/ directory (.gitignore line 29) This configuration change is orthogonal to the memory leak fix and issue #155. It appears to be a maintenance or project structure change unrelated to Wasmtime memory management.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain modules listed in go.work or their selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@host-go/engine/module/memory.go`:
- Around line 14-15: The Memory.Size() API currently returns uint32 which
overflows at the wasm32 maximum (4,294,967,296 bytes); change the Memory.Size()
signature to return uint64 and update all implementations (e.g.,
BytesMemory.Size(), the js implementation returning m.array.Length(), and the
wazero adapter that uses m.memory.Size() * 65536) to return uint64 values (e.g.,
uint64(len(s.data)), uint64(m.array.Length()), and uint64(m.memory.Size()) *
65536) so the full 4GiB range is representable without wraparound; update any
callers to expect uint64 as well.

In `@host-go/engine/tests/wasm32_pipeline_reset_test.go`:
- Around line 69-70: The subtraction "growth := final - baseline" can underflow
when final < baseline; update the calculation (using the variables final,
baseline, growth and the call instance.Memory().Size()) to compute the delta in
a signed type or clamp negative results to zero so that any shrink becomes 0
rather than a large wraparound value; modify the test to compute a signed
difference or use a saturating subtraction and assign the non-negative result to
growth.

In `@host-go/runtimes/wasmtime/runtime.go`:
- Around line 71-154: newInstanceHandles currently calls wasmtime.NewStore(eng)
to create store but returns early on many errors without closing it; ensure the
created store is closed on all error paths but remains open when returning the
successful *instanceHandles. Modify newInstanceHandles to arrange cleanup (e.g.,
set a defer that calls store.Close() conditionally or use a small local cleanup
function/flag) so any early return (errors from export/function lookups,
json.Marshal, alloc.Call, pipes.WriteItem, setParam.Call, pipes.ReadItem, etc.)
invokes store.Close(), and ensure the defer/flag is disabled before the final
return that constructs and returns the instanceHandles (store, memory, alloc,
transform).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a102183c-89cb-47e5-bfff-b360b88f5da1

📥 Commits

Reviewing files that changed from the base of the PR and between fadb7f1 and e99908e.

📒 Files selected for processing (6)
  • .gitignore
  • host-go/engine/module/memory.go
  • host-go/engine/tests/wasm32_pipeline_reset_test.go
  • host-go/runtimes/js/memory.go
  • host-go/runtimes/wasmtime/runtime.go
  • host-go/runtimes/wazero/memory.go
📜 Review details
🔇 Additional comments (1)
.gitignore (1)

29-29: LGTM!

Comment thread host-go/engine/module/memory.go
Comment thread host-go/engine/tests/wasm32_pipeline_reset_test.go
Comment thread host-go/runtimes/wasmtime/runtime.go
@AndrewSisley AndrewSisley changed the title Fix/store per instance reset fix: Safely reset wasmtime memory per instance reset Jun 3, 2026

@AndrewSisley AndrewSisley left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me! Thank you very much, this is very handy. Let me know if you also wish to resolve this for the other runtimes (no pressure at all) #159

// The GC finalizer would do this eventually, but wasm memory lives outside the Go heap
// and exerts no pressure on the Go GC, so dropped stores pile up faster than they are
// collected (issue #155).
oldStore.Close()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Thank you for this!

return r.(module.MemSize), err
},
Transform: func(next func() module.MemSize) (module.MemSize, error) {
if resetErr != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Thanks for working around the lack of error return from Reset and still bubbling the error back up to the user. Reset can be reformed later.

//
// Without the store-per-instance fix, dlmalloc-rs bookkeeping is overwritten on each Reset,
// causing memory.grow to be called every cycle and leak ~64 KiB per call (issue #155).
func TestWasm32ResetDoesNotLeakMemory(t *testing.T) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Thank you very much for including this in the PR.

@AndrewSisley AndrewSisley merged commit c87090b into sourcenetwork:main Jun 3, 2026
8 of 9 checks passed
@edjroz

edjroz commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

Yeah I can get on that

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wasmtime runtime appears to run out of memory

2 participants