diff --git a/lib/wasm/wasm_exec.js b/lib/wasm/wasm_exec.js index d71af9e97e8ca9..15eb3ab34be2f1 100644 --- a/lib/wasm/wasm_exec.js +++ b/lib/wasm/wasm_exec.js @@ -545,18 +545,39 @@ throw new Error("total length of command line and environment variables exceeds limit"); } - this._inst.exports.run(argc, argv); + this._runCatch(() => this._inst.exports.run(argc, argv)); if (this.exited) { this._resolveExitPromise(); } await this._exitPromise; } + _runCatch(fn) { + try { + fn(); + } catch (err) { + // mostly for debugging + try { + // insert message to mem + const bytes = encoder.encode(err.message + "\0"); + const ptr = this._inst.exports.allocErrorString(BigInt(bytes.length)) + new Uint8Array(this.mem.buffer, ptr, bytes.length).set(bytes); + // runtime.throw + this._inst.exports.throw(ptr, bytes.length); + } catch (err) {} + this.exited = true; + this._resolveExitPromise(); + throw err; + } + } + _resume() { if (this.exited) { throw new Error("Go program has already exited"); } - this._inst.exports.resume(); + + this._runCatch(() => this._inst.exports.resume()); + if (this.exited) { this._resolveExitPromise(); } diff --git a/src/runtime/mem_js.go b/src/runtime/mem_js.go index 080b1abc6701cc..b390e140c377c3 100644 --- a/src/runtime/mem_js.go +++ b/src/runtime/mem_js.go @@ -6,8 +6,18 @@ package runtime +import "unsafe" + // resetMemoryDataView signals the JS front-end that WebAssembly's memory.grow instruction has been used. // This allows the front-end to replace the old DataView object with a new one. // //go:wasmimport gojs runtime.resetMemoryDataView func resetMemoryDataView() + +// allocErrorString allocates a space to store the error string passed from the JS front-end. +// +//go:wasmexport allocErrorString +func allocErrorString(s int64) uintptr { + tmp := make([]byte, s) + return uintptr(unsafe.Pointer(&tmp[0])) +} diff --git a/src/runtime/panic.go b/src/runtime/panic.go index dc7a7fe357e091..e14c5e50917596 100644 --- a/src/runtime/panic.go +++ b/src/runtime/panic.go @@ -1086,6 +1086,7 @@ func internal_sync_fatal(s string) { // See go.dev/issue/67401. // //go:linkname throw +//go:wasmexport throw //go:nosplit func throw(s string) { // Everything throw does should be recursively nosplit so it diff --git a/test/wasmstackoverflow.go b/test/wasmstackoverflow.go new file mode 100644 index 00000000000000..9278f1d1465a79 --- /dev/null +++ b/test/wasmstackoverflow.go @@ -0,0 +1,73 @@ +// run + +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This test ensure stackoverflow was caught correctly + +package main + +import ( + "bytes" + "os" + "os/exec" + "path/filepath" +) + +const errorCode = ` +package main + +func growStack(n int64) { + if n > 0 { + growStack(n - 1) + } +} + +func main() { + growStack(998244353) +} +` + +func main() { + tmpDir, err := os.MkdirTemp("", "wasm-stackoverflow") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpDir) + + if err := os.WriteFile(tmpDir+"/test.go", []byte(errorCode), 0666); err != nil { + panic(err) + } + + cmd := exec.Command("go", "build", "-o", tmpDir+"/test.wasm", tmpDir+"/test.go") + cmd.Dir = tmpDir + cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm") + if err := cmd.Run(); err != nil { + panic(err) + } + + node, err := exec.LookPath("node") + if err != nil { + // skip wasm stackoverflow test because node is not found + return + } + + p := "../lib/wasm/wasm_exec_node.js" + p, err = filepath.Abs(p) + if err != nil { + panic(err) + } + + out := bytes.NewBuffer(nil) + cmd = exec.Command(node, p, tmpDir+"/test.wasm") + cmd.Stdout = out + cmd.Stderr = out + if err := cmd.Run(); err == nil { + panic("expected stackoverflow error, got nil") + } + + if !bytes.Contains(out.Bytes(), []byte("Maximum call stack size exceeded")) { + panic("expected stackoverflow error") + } +}