From 4619f6206c19f041c64be296355ac5bd964c0f1b Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 16 Oct 2026 02:17:15 -0700 Subject: [PATCH 1/5] [Interpreter] Use 1 GiB host memory limit in both init and growMemory Previously, `ShellExternalInterface::init` only checked for 64-bit byte-size shift overflow and allocation failure, allowing modules with initial memories larger than the interpreter's 1 GiB cap (`MaxMemorySize = 1024 * 1024 * 1024`) to instantiate, while `ShellExternalInterface::growMemory` checked `newSize > 1024 * 1024 * 1024` and returned `false` (producing a Wasm `-1` result even for `memory.grow(0)`). When a module with an initial memory exceeding 1 GiB (or growing past 1 GiB while remaining within its declared maximum) was executed under differential fuzzing, the Binaryen interpreter returned `-1` from `memory.grow` while other VMs succeeded. Check `bytes > MaxMemorySize` in `ShellExternalInterface::init` and call `hostLimit("memory too large")` in both `init` and `growMemory` when the interpreter's 1 GiB allocation cap is exceeded so that `ExecutionResults` and differential fuzzing treat it as a host limit rather than a semantic divergence. --- src/shell-interface.h | 23 +++++++----------- test/lit/exec/memory64-max-size.wast | 35 +++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/shell-interface.h b/src/shell-interface.h index 1d8cf5e9d9e..48e4d9c65ea 100644 --- a/src/shell-interface.h +++ b/src/shell-interface.h @@ -88,6 +88,10 @@ struct ShellExternalInterface : ModuleRunner::ExternalInterface { return ret; } + // Apply a reasonable limit on memory size, 1GB, to avoid DOS on the + // interpreter. + static constexpr uint64_t MaxMemorySize = 1024 * 1024 * 1024; + void init(Module& wasm, ModuleRunner& instance) override { ModuleUtils::iterDefinedMemories(wasm, [&](wasm::Memory* memory) { // Compute the initial size in bytes carefully: the byte size of a @@ -95,19 +99,12 @@ struct ShellExternalInterface : ModuleRunner::ExternalInterface { // we let it wrap, instantiation would "succeed" with an empty buffer // and every later access would spuriously trap as out of bounds. uint64_t bytes = uint64_t(memory->initial) << memory->pageSizeLog2; - if ((bytes >> memory->pageSizeLog2) != uint64_t(memory->initial)) { - // The byte size is not even representable; we certainly cannot - // allocate it, just like a real VM could not. + if ((bytes >> memory->pageSizeLog2) != uint64_t(memory->initial) || + bytes > MaxMemorySize) { hostLimit("memory too large"); } auto shellMemory = Memory(); - try { - shellMemory.resize(bytes); - } catch (const std::exception&) { - // bad_alloc or length_error from the underlying vector: the memory - // is representable but too large to actually allocate. - hostLimit("memory too large"); - } + shellMemory.resize(bytes); memories[memory->name] = shellMemory; }); } @@ -236,10 +233,8 @@ struct ShellExternalInterface : ModuleRunner::ExternalInterface { } bool growMemory(Name memoryName, Address /*oldSize*/, Address newSize) override { - // Apply a reasonable limit on memory size, 1GB, to avoid DOS on the - // interpreter. - if (newSize > 1024 * 1024 * 1024) { - return false; + if (newSize > MaxMemorySize) { + hostLimit("memory too large"); } auto it = memories.find(memoryName); if (it == memories.end()) { diff --git a/test/lit/exec/memory64-max-size.wast b/test/lit/exec/memory64-max-size.wast index f1b5dc82dd7..a580e8b29be 100644 --- a/test/lit/exec/memory64-max-size.wast +++ b/test/lit/exec/memory64-max-size.wast @@ -1,6 +1,6 @@ ;; NOTE: Assertions have been generated by update_lit_checks.py --output=fuzz-exec and should not be edited. -;; RUN: wasm-opt %s -all --fuzz-exec-before -q -o /dev/null 2>&1 | filecheck %s +;; RUN: foreach %s %t wasm-opt -all --fuzz-exec-before -q -o /dev/null 2>&1 | filecheck %s ;; A maximal memory64 declares 2^48 pages, exactly 2^64 bytes, which cannot be ;; allocated (and whose byte size does not even fit in 64 bits). Instantiation @@ -12,3 +12,36 @@ (memory $0 i64 281474976710656 281474976710656 shared) (data $0 (i64.const 0) "\00") ) + +;; An initial memory larger than the interpreter's 1 GiB (16384 64KiB pages) +;; allocation cap must also fail instantiation as a host limit rather than +;; succeeding and then failing subsequent memory.grow(0) calls. +(module + ;; CHECK: [host limit memory too large] + (memory $0 i64 32769 32769) + (func $test (export "test") (result i64) + (memory.grow + (i64.const 0) + ) + ) +) + +;; Growing a memory past the interpreter's 1 GiB cap while remaining within the +;; declared maximum size must also signal a host limit rather than returning -1. +(module + (memory $0 i64 1 32769) + ;; CHECK: [fuzz-exec] export grow-0 + ;; CHECK-NEXT: [fuzz-exec] note result: grow-0 => 1 + (func $grow-0 (export "grow-0") (result i64) + (memory.grow + (i64.const 0) + ) + ) + ;; CHECK: [fuzz-exec] export grow-past-host-limit + ;; CHECK-NEXT: [host limit memory too large] + (func $grow-past-host-limit (export "grow-past-host-limit") (result i64) + (memory.grow + (i64.const 32768) + ) + ) +) From 4c43c540b7d9d9e338ddf3a2b1a07d854b8d4cac Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 16 Oct 2026 14:46:52 -0700 Subject: [PATCH 2/5] restore try-catch --- src/shell-interface.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/shell-interface.h b/src/shell-interface.h index 48e4d9c65ea..dec15e6c17a 100644 --- a/src/shell-interface.h +++ b/src/shell-interface.h @@ -104,7 +104,13 @@ struct ShellExternalInterface : ModuleRunner::ExternalInterface { hostLimit("memory too large"); } auto shellMemory = Memory(); - shellMemory.resize(bytes); + try { + shellMemory.resize(bytes); + } catch (const std::exception&) { + // bad_alloc or length_error from the underlying vector: the memory + // is representable but too large to actually allocate. + hostLimit("memory too large"); + } memories[memory->name] = shellMemory; }); } From e3d0fa31238bfe94926abf7cc3cdaf8f98e02ff1 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 16 Oct 2026 14:47:25 -0700 Subject: [PATCH 3/5] comment --- src/shell-interface.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/shell-interface.h b/src/shell-interface.h index dec15e6c17a..e56ebd91d85 100644 --- a/src/shell-interface.h +++ b/src/shell-interface.h @@ -240,6 +240,14 @@ struct ShellExternalInterface : ModuleRunner::ExternalInterface { bool growMemory(Name memoryName, Address /*oldSize*/, Address newSize) override { if (newSize > MaxMemorySize) { + // According to the spec, this should return -1. However, that kind of + // non-deterministic graceful failure causes false positives when doing + // differential fuzzing between implementations that vary in their ability + // to do the grow. To avoid those false positives in the fuzzer, we fail + // with an exceeded host limit here. This will cause fuzzer executions + // with large memory growths to be ignored. This limit is implemented here + // instead of in the main interpreter so other users (e.g. Precompute) get + // the standard behavior. hostLimit("memory too large"); } auto it = memories.find(memoryName); From e5ba80b9e675e96fdb0530b1def44da5d0dff980 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 16 Oct 2026 14:57:50 -0700 Subject: [PATCH 4/5] test --- test/lit/passes/precompute_all-features.wast | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/test/lit/passes/precompute_all-features.wast b/test/lit/passes/precompute_all-features.wast index 5ea442357db..add0e65503f 100644 --- a/test/lit/passes/precompute_all-features.wast +++ b/test/lit/passes/precompute_all-features.wast @@ -22,8 +22,8 @@ ;; CHECK: (global $global-mut (mut i32) (i32.const 2)) - ;; CHECK: (memory $m 512 512) - (memory $m 512 512) + ;; CHECK: (memory $m 512 65536) + (memory $m 512 65536) (data (memory $m) (i32.const 0) "hello!") (type $0 (func (param i32))) (global $global i32 (i32.const 1)) @@ -635,6 +635,18 @@ (i32.const 12) ) ) + ;; CHECK: (func $no-memory-grow-precompute (type $2) (result i32) + ;; CHECK-NEXT: (memory.grow + ;; CHECK-NEXT: (i32.const 32768) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $no-memory-grow-precompute (result i32) + ;; A very large memory.grow should not be precomputed and should not hit the + ;; shell interpreter's host memory limit. + (memory.grow + (i32.const 32768) + ) + ) ;; CHECK: (func $tuple-precompute (type $6) (result i32 i64) ;; CHECK-NEXT: (tuple.make 2 ;; CHECK-NEXT: (i32.const 42) From c4f031e8f5b5bf7730bbbdacaf4fe42cf7aaa48d Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 16 Oct 2026 17:23:15 -0700 Subject: [PATCH 5/5] improvements --- test/lit/exec/memory64-max-size.wast | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/lit/exec/memory64-max-size.wast b/test/lit/exec/memory64-max-size.wast index a580e8b29be..dbcafdb8f63 100644 --- a/test/lit/exec/memory64-max-size.wast +++ b/test/lit/exec/memory64-max-size.wast @@ -18,7 +18,7 @@ ;; succeeding and then failing subsequent memory.grow(0) calls. (module ;; CHECK: [host limit memory too large] - (memory $0 i64 32769 32769) + (memory $0 i64 16385 16385) (func $test (export "test") (result i64) (memory.grow (i64.const 0) @@ -27,9 +27,11 @@ ) ;; Growing a memory past the interpreter's 1 GiB cap while remaining within the -;; declared maximum size must also signal a host limit rather than returning -1. +;; declared maximum size must also signal a host limit rather than returning -1 +;; so differential fuzzing ignores runs that hit the cap instead of reporting a +;; false positive when other VMs succeed. (module - (memory $0 i64 1 32769) + (memory $0 i64 1 16385) ;; CHECK: [fuzz-exec] export grow-0 ;; CHECK-NEXT: [fuzz-exec] note result: grow-0 => 1 (func $grow-0 (export "grow-0") (result i64) @@ -41,7 +43,7 @@ ;; CHECK-NEXT: [host limit memory too large] (func $grow-past-host-limit (export "grow-past-host-limit") (result i64) (memory.grow - (i64.const 32768) + (i64.const 16384) ) ) )