From 59eb221d7cd95a466af34fd558f8b496abc24a46 Mon Sep 17 00:00:00 2001 From: sideshowbarker Date: Mon, 25 May 2026 14:58:24 +0900 Subject: [PATCH] =?UTF-8?q?LibWasm:=20Fix=20=E2=80=9Creturn=E2=80=9D=20lea?= =?UTF-8?q?king=20intermediate=20value-stack=20entries=20to=20caller?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: Loading WebAssembly modules that use “return” mid-function can corrupt the heap and crash the browser. Cause: HANDLE_INSTRUCTION(return_) shrank the label stack but left any working values pushed before the “return” instruction on the shared value stack. Those residuals leaked into the caller’s frame and accumulated across calls — until they overflowed the value stack’s inline storage and corrupted adjacent allocator metadata. Fix: After shrinking the label stack down to the function-level label, also remove value-stack entries between that label’s recorded stack_height and the top .arity() result values — mirroring the cleanup that branch_to_label already performs for br/br_if. Fixes https://github.com/LadybirdBrowser/ladybird/issues/9614 --- .../AbstractMachine/BytecodeInterpreter.cpp | 7 +++ .../Executor/test-return-mid-function.js | 14 +++++ .../Fixtures/Modules/return-mid-function.wasm | Bin 0 -> 106 bytes .../Fixtures/Modules/return-mid-function.wat | 58 ++++++++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 Libraries/LibWasm/Tests/Executor/test-return-mid-function.js create mode 100644 Libraries/LibWasm/Tests/Fixtures/Modules/return-mid-function.wasm create mode 100644 Libraries/LibWasm/Tests/Fixtures/Modules/return-mid-function.wat diff --git a/Libraries/LibWasm/AbstractMachine/BytecodeInterpreter.cpp b/Libraries/LibWasm/AbstractMachine/BytecodeInterpreter.cpp index 0e87d5797a..374b7aaa2f 100644 --- a/Libraries/LibWasm/AbstractMachine/BytecodeInterpreter.cpp +++ b/Libraries/LibWasm/AbstractMachine/BytecodeInterpreter.cpp @@ -2131,6 +2131,13 @@ HANDLE_INSTRUCTION(return_) { LOG_INSN; configuration.label_stack().shrink(configuration.frame().label_index() + 1, true); + // Clear intermediate working values from the value stack, keeping only the top .arity() (the return values) above + // the function-level label's recorded stack_height. Without this, residual values pushed before the return are + // leaked to the caller’s value stack — and accumulate across nested calls until heap-buffer-overflow. + auto const& label = configuration.label_stack().unsafe_last(); + auto& vs = configuration.value_stack(); + if (vs.size() > label.stack_height() + label.arity()) + vs.remove(label.stack_height(), vs.size() - label.stack_height() - label.arity()); return Outcome::Return; } diff --git a/Libraries/LibWasm/Tests/Executor/test-return-mid-function.js b/Libraries/LibWasm/Tests/Executor/test-return-mid-function.js new file mode 100644 index 0000000000..a20275f796 --- /dev/null +++ b/Libraries/LibWasm/Tests/Executor/test-return-mid-function.js @@ -0,0 +1,14 @@ +test("return mid-function doesn't corrupt caller stack", () => { + const bin = readBinaryWasmFile("Fixtures/Modules/return-mid-function.wasm"); + const module = parseWebAssemblyModule(bin); + + // Deterministic check: without the fix, the residual 99 from $leaky sits between the caller’s 10 and the result 42, + // so i32.add yields 141, rather than the correct 52. + const test_add = module.getExport("test_add"); + expect(module.invoke(test_add)).toBe(52); + + // Stress check: 100 iterations accumulate residuals under the bug; under ASan this overflows the value stack’s + // inline storage. + const drive = module.getExport("drive"); + expect(module.invoke(drive)).toBe(100 * 42); +}); diff --git a/Libraries/LibWasm/Tests/Fixtures/Modules/return-mid-function.wasm b/Libraries/LibWasm/Tests/Fixtures/Modules/return-mid-function.wasm new file mode 100644 index 0000000000000000000000000000000000000000..acfd1a8a5541e529817087d481de789b888cc500 GIT binary patch literal 106 zcmZQbEY4+QU|?WmWlUgTtY>Cr0dg4FMVL5BQj1IC6H`)vQYl55WvL9zTsABm433W( z9JTnl*%=(U1Q@coxfvX