Repository: tetratelabs/wazero (go)

Issue:
Looking up and calling a non existing function will SIGILL on some platforms
**Describe the bug**
This is a result of the issue we found when developing #1611, that code would try to call the exported function `stackRestore`, but that function was not defined in our WASM, because we didn't expect the current tests to reach that code. On some platforms this will `SIGILL: illegal instruction` some of the times, at least on arm64 and riscv64. 

**To Reproduce**
First make sure you can run arm64 in Docker by install qemu:

```
docker run --privileged --rm ghcr.io/tetratelabs/wazero/internal-binfmt --install arm64
```

Create a Dockerfile with the following:
```
FROM scratch
CMD ["/test"]
```

Build the docker image:
```
docker buildx build -t wazero:test --platform linux/arm64 .
```

Create the following main.go:
```
package main

import (
	"context"
	_ "embed"
	"os"

	"github.com/tetratelabs/wazero"
	"github.com/tetratelabs/wazero/experimental"
	"github.com/tetratelabs/wazero/experimental/logging"
	"github.com/tetratelabs/wazero/imports/emscripten"
)

// This is the invoke.wasm from imports/emscripten/testdata.
//
//go:embed invoke.wasm
var invokeWasm []byte

func main() {
	ctx := context.WithValue(context.Background(), experimental.FunctionListenerFactoryKey{}, logging.NewLoggingListenerFactory(os.Stdout))

	r := wazero.NewRuntime(ctx)
	defer r.Close(ctx)

	compiled, err := r.CompileModule(ctx, invokeWasm)
	if err != nil {
		panic(err)
	}

	_, err = emscripten.InstantiateForModule(ctx, r, compiled)
	if err != nil {
		panic(err)
	}

	mod, err := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig())
	if err != nil {
		panic(err)
	}

	mod.ExportedFunction("iDontExist").Call(ctx)
}
```

Build the binary and run it in Docker:

```
export GOARCH=arm64
export CGO_ENABLED=0
go build -o reproduce main.go
docker run --platform linux/arm64 -v $(pwd)/reproduce:/test --tmpfs /tmp --rm -t wazero:test
```

You might have to run it a few times to trigger it. If you first check the exported function for nil it doesn't have this behaviour.

**Expected behavior**
`ExportedFunction` should always return nil when the function doesn't exist.

**Environment (please complete the relevant information):**
- Go version: 1.20.2
- wazero Version: v1.4.0
- Host architecture: amd64, but compiled to arm64 with GOARCH and ran in arm64 qemu
- Runtime mode: both

**Additional context**
```
SIGILL: illegal instruction
PC=0x0 m=0 sigcode=2
instruction bytes: 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0

goroutine 1 [running]:
main.main()
	/home/jeroen/Projects/OpenSource/wazero/reproduce/main.go:42 +0x338 fp=0x4000173f20 sp=0x4000173e30 pc=0x1b34b8
runtime: g 1: unexpected return pc for main.main called from 0x400000e018
stack: frame={sp:0x4000173f20, fp:0x4000174010} stack=[0x400016c000,0x4000174000)
0x0000004000173e20:  0x000000400014a500  0x000000400005de78 
0x0000004000173e30:  0x00000000001b34b8 <main.main+0x0000000000000338>  0x000000400014a500 
0x0000004000173e40:  0x0000000000220af2  0x000000400005de78 
0x0000004000173e50:  0x00000000001b3444 <main.main+0x00000000000002c4>  0x0000000000000010 
0x0000004000173e60:  0x00000000001db540  0x0100000000000001 
0x0000004000173e70:  0x0000000000230ad8  0x000000400004c768 
0x0000004000173e80:  0x00000000000454a0 <runtime.main+0x0000000000000200>  0x0000000000268148 
0x0000004000173e90:  0x000000400014a500  0x0000000000266160 
0x0000004000173ea0:  0x000000400007da40  0x0000004000013440 
0x0000004000173eb0:  0x0000000000268a00  0x000000400018a000 
0x0000004000173ec0:  0x0100000000014904  0x0000000000265ce8 
0x0000004000173ed0:  0x0000000000000001  0x0000004000013300 
0x0000004000173ee0:  0x000000400007db30  0x000000400014a500 
0x0000004000173ef0:  0x000000400007da40  0x0000004000013440 
0x0000004000173f00:  0x0000000000268148  0x0000000000266160 
0x0000004000173f10:  0x0000004000129000  0x0000000000267078 
0x0000004000173f20: <0x000000400000e018  0x0000004000014030 
0x0000004000173f30:  0x000000400018a000  0x00000000001b3540 <main.main.func1+0x0000000000000000> 
0x0000004000173f40:  0x0000000000267078  0x000000400007db30 
0x0000004000173f50:  0x0000000000266160  0x000000400007da40 
0x0000004000173f60:  0x0000004000173f38  0x0000000000000000 
0x0000004000173f70:  0x0000000000072024 <runtime.goexit+0x0000000000000004>  0x000000400007a000 
0x0000004000173f80:  0x0000000000000000  0x0000000000000000 
0x0000004000173f90:  0x0000000000000000  0x0100000000000000 
0x0000004000173fa0:  0x0000000000000000  0x00000000003a3b20 
0x0000004000173fb0:  0x00000000000455d0 <runtime.main.func2+0x0000000000000000>  0x0000004000173f9e 
0x0000004000173fc0:  0x0000004000173fb0  0x0000000000000000 
0x0000004000173fd0:  0x0000000000000000  0x0000000000000000 
0x0000004000173fe0:  0x0000000000000000  0x0000000000000000 
0x0000004000173ff0:  0x0000000000000000  0x0000000000000000 
main.main()
	/home/jeroen/Projects/OpenSource/wazero/reproduce/main.go:43 +0x338 fp=0x4000174010 sp=0x4000173f20 pc=0x1b34b8

goroutine 2 [force gc (idle)]:
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
	/usr/local/go/src/runtime/proc.go:381 +0xe0 fp=0x400004cfa0 sp=0x400004cf80 pc=0x458b0
runtime.goparkunlock(...)
	/usr/local/go/src/runtime/proc.go:387
runtime.forcegchelper()
	/usr/local/go/src/runtime/proc.go:305 +0xb0 fp=0x400004cfd0 sp=0x400004cfa0 pc=0x45700
runtime.goexit()
	/usr/local/go/src/runtime/asm_arm64.s:1172 +0x4 fp=0x400004cfd0 sp=0x400004cfd0 pc=0x72024
created by runtime.init.6
	/usr/local/go/src/runtime/proc.go:293 +0x24

goroutine 3 [GC sweep wait]:
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
	/usr/local/go/src/runtime/proc.go:381 +0xe0 fp=0x400004d760 sp=0x400004d740 pc=0x458b0
runtime.goparkunlock(...)
	/usr/local/go/src/runtime/proc.go:387
runtime.bgsweep(0x0?)
	/usr/local/go/src/runtime/mgcsweep.go:278 +0x98 fp=0x400004d7b0 sp=0x400004d760 pc=0x32328
runtime.gcenable.func1()
	/usr/local/go/src/runtime/mgc.go:178 +0x28 fp=0x400004d7d0 sp=0x400004d7b0 pc=0x273e8
runtime.goexit()
	/usr/local/go/src/runtime/asm_arm64.s:1172 +0x4 fp=0x400004d7d0 sp=0x400004d7d0 pc=0x72024
created by runtime.gcenable
	/usr/local/go/src/runtime/mgc.go:178 +0x6c

goroutine 4 [GC scavenge wait]:
runtime.gopark(0x4000072000?, 0x262fd0?, 0x1?, 0x0?, 0x0?)
	/usr/local/go/src/runtime/proc.go:381 +0xe0 fp=0x400004df50 sp=0x400004df30 pc=0x458b0
runtime.goparkunlock(...)
	/usr/local/go/src/runtime/proc.go:387
runtime.(*scavengerState).park(0x3a35c0)
	/usr/local/go/src/runtime/mgcscavenge.go:400 +0x5c fp=0x400004df80 sp=0x400004df50 pc=0x302bc
runtime.bgscavenge(0x0?)
	/usr/local/go/src/runtime/mgcscavenge.go:628 +0x44 fp=0x400004dfb0 sp=0x400004df80 pc=0x307f4
runtime.gcenable.func2()
	/usr/local/go/src/runtime/mgc.go:179 +0x28 fp=0x400004dfd0 sp=0x400004dfb0 pc=0x27388
runtime.goexit()
	/usr/local/go/src/runtime/asm_arm64.s:1172 +0x4 fp=0x400004dfd0 sp=0x400004dfd0 pc=0x72024
created by runtime.gcenable
	/usr/local/go/src/runtime/mgc.go:179 +0xac

goroutine 5 [finalizer wait]:
runtime.gopark(0x400004c5a8?, 0x60000000024e68?, 0x28?, 0xd7?, 0x1?)
	/usr/local/go/src/runtime/proc.go:381 +0xe0 fp=0x400004c580 sp=0x400004c560 pc=0x458b0
runtime.runfinq()
	/usr/local/go/src/runtime/mfinal.go:193 +0x100 fp=0x400004c7d0 sp=0x400004c580 pc=0x264b0
runtime.goexit()
	/usr/local/go/src/runtime/asm_arm64.s:1172 +0x4 fp=0x400004c7d0 sp=0x400004c7d0 pc=0x72024
created by runtime.createfing
	/usr/local/go/src/runtime/mfinal.go:163 +0x80

r0      0x0
r1      0x266160
r2      0x400007da40
r3      0x0
r4      0x0
r5      0x0
r6      0x0
r7      0x0
r8      0x10
r9      0x4000011d60
r10     0x0
r11     0x0
r12     0x1d6
r13     0x200
r14     0x1d7
r15     0x18
r16     0x400016c3a0
r17     0x4000173b80
r18     0x0
r19     0x0
r20     0x4000173b50
r21     0x0
r22     0x0
r23     0x0
r24     0x0
r25     0x0
r26     0x230eb0
r27     0x3d7000
r28     0x40000021a0
r29     0x4000173e28
lr      0x1b3630
sp      0x4000173e30
pc      0x0
fault   0x0
```

Pull request:
emscripten: explicitly checks if an api.Function is not nil
Fixes #1621 (at least for us in terms of flaky tests)

Patch:
diff --git a/internal/emscripten/emscripten.go b/internal/emscripten/emscripten.go
index f880a0c5..3262e670 100644
--- a/internal/emscripten/emscripten.go
+++ b/internal/emscripten/emscripten.go
@@ -3,6 +3,7 @@ package emscripten
 import (
 	"context"
 	"errors"
+	"fmt"
 	"strconv"
 
 	"github.com/tetratelabs/wazero/api"
@@ -129,10 +130,7 @@ func (v *InvokeFunc) Call(ctx context.Context, mod api.Module, stack []uint64) {
 	// We reuse savedStack to save allocations. We allocate with a size of 2
 	// here to accommodate for the input and output of setThrew.
 	var savedStack [2]uint64
-	err = mod.ExportedFunction("stackSave").CallWithStack(ctx, savedStack[:])
-	if err != nil {
-		panic(err)
-	}
+	callOrPanic(ctx, mod, "stackSave", savedStack[:])
 
 	err = f.CallWithStack(ctx, stack)
 	if err != nil {
@@ -143,9 +141,7 @@ func (v *InvokeFunc) Call(ctx context.Context, mod api.Module, stack []uint64) {
 
 		// This is the equivalent of "stackRestore(sp);".
 		// Do not overwrite err here to preserve the original error.
-		if err := mod.ExportedFunction("stackRestore").CallWithStack(ctx, savedStack[:]); err != nil {
-			panic(err)
-		}
+		callOrPanic(ctx, mod, "stackRestore", savedStack[:])
 
 		// If we encounter ThrowLongjmpError, this means that the C code did a
 		// longjmp, which in turn called _emscripten_throw_longjmp and that is
@@ -163,9 +159,23 @@ func (v *InvokeFunc) Call(ctx context.Context, mod api.Module, stack []uint64) {
 		// This is the equivalent of "_setThrew(1, 0);".
 		savedStack[0] = 1
 		savedStack[1] = 0
-		err = mod.ExportedFunction("setThrew").CallWithStack(ctx, savedStack[:])
+		callOrPanic(ctx, mod, "setThrew", savedStack[:])
+	}
+}
+
+// maybeCallOrPanic calls a given function if it is exported, otherwise panics.
+//
+// This ensures if the given name is exported before calling it. In other words, this explicitly checks if an api.Function
+// returned by api.Module.ExportedFunction is not nil. This is necessary because directly calling a method which is
+// potentially nil interface can be fatal on some platforms due to a bug? in Go/QEMU.
+// See https://github.com/tetratelabs/wazero/issues/1621
+func callOrPanic(ctx context.Context, m api.Module, name string, stack []uint64) {
+	if f := m.ExportedFunction(name); f != nil {
+		err := f.CallWithStack(ctx, stack)
 		if err != nil {
-			panic(err) // setThrew failed
+			panic(err)
 		}
+	} else {
+		panic(fmt.Sprintf("%s not exported", name))
 	}
 }


Test patch:
diff --git a/internal/emscripten/emscripten_test.go b/internal/emscripten/emscripten_test.go
new file mode 100644
index 00000000..daea7b37
--- /dev/null
+++ b/internal/emscripten/emscripten_test.go
@@ -0,0 +1,26 @@
+package emscripten
+
+import (
+	"context"
+	"testing"
+
+	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/experimental/wazerotest"
+	"github.com/tetratelabs/wazero/internal/testing/require"
+)
+
+func Test_callOnPanic(t *testing.T) {
+	const exists = "f"
+	var called bool
+	f := wazerotest.NewFunction(func(context.Context, api.Module) { called = true })
+	f.ExportNames = []string{exists}
+	m := wazerotest.NewModule(nil, f)
+	t.Run("exists", func(t *testing.T) {
+		callOrPanic(context.Background(), m, exists, nil)
+		require.True(t, called)
+	})
+	t.Run("not exist", func(t *testing.T) {
+		err := require.CapturePanic(func() { callOrPanic(context.Background(), m, "not-exist", nil) })
+		require.EqualError(t, err, "not-exist not exported")
+	})
+}