Skip to content

Add maxMemoryBytes: a hard cap on WebAssembly linear memory - #283

Open
jackkav wants to merge 1 commit into
justjake:mainfrom
jackkav:max-memory-bytes
Open

jackkav wants to merge 1 commit into
justjake:mainfrom
jackkav:max-memory-bytes

Conversation

@jackkav

@jackkav jackkav commented Oct 16, 2026

Copy link
Copy Markdown

Layer: TypeScript wrapper (newVariant option). No C or build change.
Review just this PR: it is standalone on main, ~30 lines of source plus tests and a README note.
Related: #275 (under-enforced setMemoryLimit), #207 (memory accounting). Independent of the #248 stack.

What
newVariant(variant, { maxMemoryBytes }) instantiates the module with a WebAssembly.Memory whose maximum is the cap (the variants already build with IMPORTED_MEMORY=1, so wasmMemory could always do this by hand; this just makes it a one-liner and tests it).

setMemoryLimit goes through QuickJS's own allocation accounting, which undercounts on Emscripten builds: with a 32 MiB limit a guest still grew the heap to 63 MiB. The cap does not depend on that accounting, and it also bounds host-side buffers and the 5 MiB wasm stack. It doesn't replace fixing the accounting (#207's malloc_usable_size direction); it is the backstop.

Test: memory-cap.test.ts, on quickjs, quickjs-ng, and both asyncify builds (release). With a 64 MiB cap a guest allocating ~1 MiB strings: (1) gets a catchable InternalError; (2) peak memory.buffer.byteLength (sampled from a host callback each iteration) never exceeds the cap; (3) the module still evaluates code and the runtime/context dispose without the list_empty(gc_obj_list) assertion; (4) the same holds with setMemoryLimit(1 GiB). Plus validation (<16 MiB, or combined with wasmMemory, rejected). All 14 fail without the change (heap peaked at 449 MB vs the 64 MiB cap).
Verified on: full suite on this branch: 765 passed, 0 failed.
Not covered: debug (sanitizer) variants abort the whole module when the heap can't grow, so they are excluded and the option's docs say so; no leak-sanitizer check under exhaustion for the same reason. Assumes Emscripten's default 16 MiB initial memory, which no variant overrides (the tests would catch a change).
Behavior change: none unless the option is passed.

🤖 Generated with Claude Code

newVariant(variant, { maxMemoryBytes }) instantiates the module with a WebAssembly.Memory whose
maximum is the cap. setMemoryLimit depends on QuickJS's allocation accounting, which undercounts
on Emscripten builds; the cap does not. At the cap, allocation fails inside the module and guest
code gets a catchable InternalError, the runtime disposes cleanly, and the heap never exceeds it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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.

1 participant