Repository: tetratelabs/wazero
Pull request #879 (2022-12-02 00:53:14)

fuzz: adds validation target for maybe invalid module compilation
Signed-off-by: Takeshi Yoneda <takeshi@tetrate.io>


Patch:
diff --git a/Makefile b/Makefile
index 0413695f..3cd82bae 100644
--- a/Makefile
+++ b/Makefile
@@ -229,3 +229,4 @@ fuzz_timeout_seconds ?= 10
 .PHONY: fuzz
 fuzz:
 	@cd internal/integration_test/fuzz && cargo fuzz run basic -- -max_total_time=$(fuzz_timeout_seconds)
+	@cd internal/integration_test/fuzz && cargo fuzz run validation -- -max_total_time=$(fuzz_timeout_seconds)
diff --git a/internal/wasm/func_validation.go b/internal/wasm/func_validation.go
index f2cdee21..4f1c3307 100644
--- a/internal/wasm/func_validation.go
+++ b/internal/wasm/func_validation.go
@@ -1464,6 +1464,9 @@ func (m *Module) validateFunctionWithMaxStackValues(
 				valueTypeStack.push(p)
 			}
 		} else if op == OpcodeEnd {
+			if len(controlBlockStack) == 0 {
+				return fmt.Errorf("redundant End instruction at %#x", pc)
+			}
 			bl := controlBlockStack[len(controlBlockStack)-1]
 			bl.endAt = pc
 			controlBlockStack = controlBlockStack[:len(controlBlockStack)-1]


Test patch:
diff --git a/imports/wasi_snapshot_preview1/testdata/zig/build.zig b/imports/wasi_snapshot_preview1/testdata/zig/build.zig
new file mode 100644
index 00000000..e69de29b
diff --git a/imports/wasi_snapshot_preview1/testdata/zig/ls.zig b/imports/wasi_snapshot_preview1/testdata/zig/ls.zig
new file mode 100644
index 00000000..e69de29b
diff --git a/internal/integration_test/fuzz/fuzz/Cargo.toml b/internal/integration_test/fuzz/fuzz/Cargo.toml
index d2bdb079..67936d45 100644
--- a/internal/integration_test/fuzz/fuzz/Cargo.toml
+++ b/internal/integration_test/fuzz/fuzz/Cargo.toml
@@ -13,6 +13,12 @@ libfuzzer-sys = "0.4.3"
 wasm-smith = "0.11.4"
 wasmprinter = "0.2.39"
 
+[[bin]]
+name = "validation"
+path = "fuzz_targets/validation.rs"
+test = false
+doc = false
+
 [[bin]]
 name = "basic"
 path = "fuzz_targets/basic.rs"
diff --git a/internal/integration_test/fuzz/fuzz/build.rs b/internal/integration_test/fuzz/fuzz/build.rs
index cb7d1996..908fe877 100644
--- a/internal/integration_test/fuzz/fuzz/build.rs
+++ b/internal/integration_test/fuzz/fuzz/build.rs
@@ -8,7 +8,7 @@ fn main() {
 
     let wazero_fuzz_lib_dir = format!("{}/wazerolib", wazero_fuzz_dir.as_str());
     let library_out_path = format!("{}/libwazero.a", wazero_fuzz_lib_dir);
-    let library_source_path = format!("{}/lib.go", wazero_fuzz_lib_dir);
+    let library_source_path = format!("{}/...", wazero_fuzz_lib_dir);
 
     // Parse the GOARCH from the --target argument passed to cargo.
     let goarch = var("TARGET")
diff --git a/internal/integration_test/fuzz/fuzz/fuzz_targets/validation.rs b/internal/integration_test/fuzz/fuzz/fuzz_targets/validation.rs
new file mode 100644
index 00000000..4e8c5277
--- /dev/null
+++ b/internal/integration_test/fuzz/fuzz/fuzz_targets/validation.rs
@@ -0,0 +1,29 @@
+#![no_main]
+
+use libfuzzer_sys::arbitrary::{Result, Unstructured};
+use libfuzzer_sys::fuzz_target;
+
+fuzz_target!(|data: &[u8]| {
+    drop(run(data));
+});
+
+fn run(data: &[u8]) -> Result<()> {
+    // Create the random source.
+    let mut u = Unstructured::new(data);
+
+    // Generate the random module via wasm-smith, but MaybeInvalidModule.
+    let module: wasm_smith::MaybeInvalidModule = u.arbitrary()?;
+    let module_bytes = module.to_bytes();
+
+    unsafe {
+        validate(module_bytes.as_ptr(), module_bytes.len());
+    }
+
+    // We always return Ok as inside validate, we cause panic if the binary is interesting.
+    Ok(())
+}
+
+extern "C" {
+    // validate is implemented in Go, and accepts the pointer to the binary and its size.
+    fn validate(binary_ptr: *const u8, binary_size: usize);
+}
diff --git a/internal/integration_test/fuzz/wazerolib/lib.go b/internal/integration_test/fuzz/wazerolib/lib.go
index e121d3a9..399ba7d1 100644
--- a/internal/integration_test/fuzz/wazerolib/lib.go
+++ b/internal/integration_test/fuzz/wazerolib/lib.go
@@ -2,224 +2,19 @@ package main
 
 import "C"
 import (
-	"context"
 	"crypto/sha256"
 	"encoding/hex"
 	"fmt"
 	"os"
 	"path"
-	"reflect"
-	"strings"
-	"unsafe"
-
-	"github.com/tetratelabs/wazero"
-	"github.com/tetratelabs/wazero/api"
 )
 
 func main() {}
 
-// require_no_diff ensures that the behavior is the same between the compiler and the interpreter for any given binary.
-// And if there's diff, this also saves the problematic binary and wat into testdata directory.
-//
-//export require_no_diff
-func require_no_diff(binaryPtr uintptr, binarySize int, watPtr uintptr, watSize int) {
-	wasmBin := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
-		Data: binaryPtr,
-		Len:  binarySize,
-		Cap:  binarySize,
-	}))
-
-	wat := *(*string)(unsafe.Pointer(&reflect.SliceHeader{
-		Data: watPtr,
-		Len:  watSize,
-		Cap:  watSize,
-	}))
-
-	// Create two runtimes.
-
-	failed := true
-	defer func() {
-		if failed {
-			// If the test fails, we save the binary and wat into testdata directory.
-			saveFailedBinary(wasmBin, wat)
-		}
-	}()
-
-	requireNoDiff(wasmBin, func(err error) {
-		if err != nil {
-			panic(err)
-		}
-	})
-
-	failed = false
-	return
-}
-
-// requireNoDiff ensures that the behavior is the same between the compiler and the interpreter for any given binary.
-func requireNoDiff(wasmBin []byte, requireNoError func(err error)) {
-	// Choose the context to use for function calls.
-	ctx := context.Background()
-
-	compiler := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigCompiler())
-	interpreter := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigInterpreter())
-
-	defer compiler.Close(ctx)
-	defer interpreter.Close(ctx)
-
-	compiledCompiled, err := compiler.CompileModule(ctx, wasmBin)
-	requireNoError(err)
-
-	interpreterCompiled, err := interpreter.CompileModule(ctx, wasmBin)
-	requireNoError(err)
-
-	// Instantiate module.
-	compilerMod, compilerInstErr := compiler.InstantiateModule(ctx, compiledCompiled, wazero.NewModuleConfig())
-	interpreterMod, interpreterInstErr := interpreter.InstantiateModule(ctx, interpreterCompiled, wazero.NewModuleConfig())
-
-	okToInvoke, err := ensureInstantiationError(compilerInstErr, interpreterInstErr)
-	requireNoError(err)
-
-	if okToInvoke {
-		err = ensureInvocationResultMatch(compilerMod, interpreterMod, interpreterCompiled.ExportedFunctions())
-		requireNoError(err)
-	}
-}
-
-const valueTypeVector = 0x7b
-
-// ensureInvocationResultMatch invokes all the exported functions from the module, and compare all the results between compiler vs interpreter.
-func ensureInvocationResultMatch(compiledMod, interpreterMod api.Module, exportedFunctions map[string]api.FunctionDefinition) (err error) {
-	ctx := context.Background()
-
-outer:
-	for name, def := range exportedFunctions {
-		resultTypes := def.ResultTypes()
-		for _, rt := range resultTypes {
-			switch rt {
-			case api.ValueTypeI32, api.ValueTypeI64, api.ValueTypeF32, api.ValueTypeF64, valueTypeVector:
-			default:
-				// For the sake of simplicity in the assertion, we only invoke the function with the basic types.
-				continue outer
-			}
-		}
-
-		cmpF := compiledMod.ExportedFunction(name)
-		intF := interpreterMod.ExportedFunction(name)
-
-		params := getDummyValues(def.ParamTypes())
-		cmpRes, cmpErr := cmpF.Call(ctx, params...)
-		intRes, intErr := intF.Call(ctx, params...)
-		if errMismatch := ensureInvocationError(cmpErr, intErr); errMismatch != nil {
-			panic(fmt.Sprintf("error mismatch on invoking %s: %v", name, errMismatch))
-		}
-
-		matched := true
-		var typesIndex int
-		for i := 0; i < len(cmpRes); i++ {
-			switch resultTypes[typesIndex] {
-			case api.ValueTypeI32, api.ValueTypeF32:
-				matched = matched && uint32(cmpRes[i]) == uint32(intRes[i])
-			case api.ValueTypeI64, api.ValueTypeF64:
-				matched = matched && cmpRes[i] == intRes[i]
-			case valueTypeVector:
-				matched = matched && cmpRes[i] == intRes[i] && cmpRes[i+1] == intRes[i+1]
-				i++ // We need to advance twice (lower and higher 64bits)
-			}
-			typesIndex++
-		}
-
-		if !matched {
-			err = fmt.Errorf("result mismatch on invoking '%s':\n\tinterpreter got: %v\n\tcompiler got: %v", name, intRes, cmpRes)
-		}
-	}
-	return
-}
-
-// getDummyValues returns a dummy input values for function invocations.
-func getDummyValues(valueTypes []api.ValueType) (ret []uint64) {
-	for _, vt := range valueTypes {
-		if vt != 0x7b { // v128
-			ret = append(ret, 0)
-		} else {
-			ret = append(ret, 0, 0)
-		}
-	}
-	return
-}
-
-// ensureInvocationError ensures that function invocation errors returned by interpreter and compiler match each other's.
-func ensureInvocationError(compilerErr, interpErr error) error {
-	if compilerErr == nil && interpErr == nil {
-		return nil
-	} else if compilerErr == nil && interpErr != nil {
-		return fmt.Errorf("compiler returned no error, but interpreter got: %w", interpErr)
-	} else if compilerErr != nil && interpErr == nil {
-		return fmt.Errorf("interpreter returned no error, but compiler got: %w", compilerErr)
-	}
-
-	compilerErrMsg, interpErrMsg := compilerErr.Error(), interpErr.Error()
-	if idx := strings.Index(compilerErrMsg, "\n"); idx >= 0 {
-		compilerErrMsg = compilerErrMsg[:strings.Index(compilerErrMsg, "\n")]
-	}
-	if idx := strings.Index(interpErrMsg, "\n"); idx >= 0 {
-		interpErrMsg = interpErrMsg[:strings.Index(interpErrMsg, "\n")]
-	}
-
-	if compilerErrMsg != interpErrMsg {
-		return fmt.Errorf("error mismatch:\n\tinterpreter: %v\n\tcompiler: %v", interpErr, compilerErr)
-	}
-	return nil
-}
-
-// ensureInstantiationError ensures that instantiation errors returned by interpreter and compiler match each other's.
-func ensureInstantiationError(compilerErr, interpErr error) (okToInvoke bool, err error) {
-	if compilerErr == nil && interpErr == nil {
-		return true, nil
-	} else if compilerErr == nil && interpErr != nil {
-		return false, fmt.Errorf("compiler returned no error, but interpreter got: %w", interpErr)
-	} else if compilerErr != nil && interpErr == nil {
-		return false, fmt.Errorf("interpreter returned no error, but compiler got: %w", compilerErr)
-	}
-
-	compilerErrMsg, interpErrMsg := compilerErr.Error(), interpErr.Error()
-	if idx := strings.Index(compilerErrMsg, "\n"); idx >= 0 {
-		compilerErrMsg = compilerErrMsg[:strings.Index(compilerErrMsg, "\n")]
-	}
-	if idx := strings.Index(interpErrMsg, "\n"); idx >= 0 {
-		interpErrMsg = interpErrMsg[:strings.Index(interpErrMsg, "\n")]
-	}
-
-	if !allowedErrorDuringInstantiation(compilerErrMsg) {
-		return false, fmt.Errorf("invalid error occur with compiler: %v", compilerErr)
-	} else if !allowedErrorDuringInstantiation(interpErrMsg) {
-		return false, fmt.Errorf("invalid error occur with interpreter: %v", interpErrMsg)
-	}
-
-	if compilerErrMsg != interpErrMsg {
-		return false, fmt.Errorf("error mismatch:\n\tinterpreter: %v\n\tcompiler: %v", interpErr, compilerErr)
-	}
-	return false, nil
-}
-
-// allowedErrorDuringInstantiation checks if the error message is considered sane.
-func allowedErrorDuringInstantiation(errMsg string) bool {
-	// This happens when data segment causes out of bound, but it is considered as runtime-error in WebAssembly 2.0
-	// which is fine.
-	if strings.HasPrefix(errMsg, "data[") && strings.HasSuffix(errMsg, "]: out of bounds memory access") {
-		return true
-	}
-
-	// Start function failure is neither instantiation nor compilation error, but rather a runtime error, so that is fine.
-	if strings.HasPrefix(errMsg, "start function[") && strings.Contains(errMsg, "failed: wasm error:") {
-		return true
-	}
-	return false
-}
-
 const failedCasesDir = "wazerolib/testdata"
 
 // saveFailedBinary writes binary and wat into failedCasesDir so that it is easy to reproduce the error.
-func saveFailedBinary(bin []byte, wat string) {
+func saveFailedBinary(bin []byte, wat string, reproduceTestName string) {
 	checksum := sha256.Sum256(bin)
 	checkSumStr := hex.EncodeToString(checksum[:])
 
@@ -242,27 +37,35 @@ func saveFailedBinary(bin []byte, wat string) {
 		panic(err)
 	}
 
-	watPath := path.Join(testDataDir, fmt.Sprintf("%s.wat", checkSumStr))
-	watF, err := os.Create(watPath)
-	if err != nil {
-		panic(err)
-	}
-
-	defer watF.Close()
+	var watPath string
+	if len(wat) != 0 {
+		watPath = path.Join(testDataDir, fmt.Sprintf("%s.wat", checkSumStr))
+		watF, err := os.Create(watPath)
+		if err != nil {
+			panic(err)
+		}
 
-	_, err = watF.Write([]byte(wat))
-	if err != nil {
-		panic(err)
-	}
+		defer watF.Close()
 
-	fmt.Printf(`
+		_, err = watF.Write([]byte(wat))
+		if err != nil {
+			panic(err)
+		}
+		fmt.Printf(`
 Failed WebAssembly Text:
 %s
 
 Failed Wasm binary has been written to %s
 Failed Wasm Text has been written to %s
-To reproduce the failure, execute: WASM_BINARY_PATH=%s go test ./wazerolib/...
+To reproduce the failure, execute: WASM_BINARY_PATH=%s go test -run=%s ./wazerolib/...
 
 
-`, wat, binaryPath, watPath, binaryPath)
+`, wat, binaryPath, watPath, binaryPath, reproduceTestName)
+	} else {
+		fmt.Printf(`
+Failed WebAssembly Binary in hex: %s
+Failed Wasm binary has been written to %s
+To reproduce the failure, execute: WASM_BINARY_PATH=%s go test -run=%s ./wazerolib/...
+`, hex.EncodeToString(bin), binaryPath, binaryPath, reproduceTestName)
+	}
 }
diff --git a/internal/integration_test/fuzz/wazerolib/nodiff.go b/internal/integration_test/fuzz/wazerolib/nodiff.go
new file mode 100644
index 00000000..684ce67f
--- /dev/null
+++ b/internal/integration_test/fuzz/wazerolib/nodiff.go
@@ -0,0 +1,209 @@
+package main
+
+import "C"
+import (
+	"context"
+	"fmt"
+	"reflect"
+	"strings"
+	"unsafe"
+
+	"github.com/tetratelabs/wazero"
+	"github.com/tetratelabs/wazero/api"
+)
+
+// require_no_diff ensures that the behavior is the same between the compiler and the interpreter for any given binary.
+// And if there's diff, this also saves the problematic binary and wat into testdata directory.
+//
+//export require_no_diff
+func require_no_diff(binaryPtr uintptr, binarySize int, watPtr uintptr, watSize int) {
+	wasmBin := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
+		Data: binaryPtr,
+		Len:  binarySize,
+		Cap:  binarySize,
+	}))
+
+	wat := *(*string)(unsafe.Pointer(&reflect.SliceHeader{
+		Data: watPtr,
+		Len:  watSize,
+		Cap:  watSize,
+	}))
+
+	failed := true
+	defer func() {
+		if failed {
+			// If the test fails, we save the binary and wat into testdata directory.
+			saveFailedBinary(wasmBin, wat, "TestReRunFailedRequireNoDiffCase")
+		}
+	}()
+
+	requireNoDiff(wasmBin, func(err error) {
+		if err != nil {
+			panic(err)
+		}
+	})
+
+	failed = false
+	return
+}
+
+// requireNoDiff ensures that the behavior is the same between the compiler and the interpreter for any given binary.
+func requireNoDiff(wasmBin []byte, requireNoError func(err error)) {
+	// Choose the context to use for function calls.
+	ctx := context.Background()
+
+	compiler := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigCompiler())
+	interpreter := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigInterpreter())
+
+	defer compiler.Close(ctx)
+	defer interpreter.Close(ctx)
+
+	compiledCompiled, err := compiler.CompileModule(ctx, wasmBin)
+	requireNoError(err)
+
+	interpreterCompiled, err := interpreter.CompileModule(ctx, wasmBin)
+	requireNoError(err)
+
+	// Instantiate module.
+	compilerMod, compilerInstErr := compiler.InstantiateModule(ctx, compiledCompiled, wazero.NewModuleConfig())
+	interpreterMod, interpreterInstErr := interpreter.InstantiateModule(ctx, interpreterCompiled, wazero.NewModuleConfig())
+
+	okToInvoke, err := ensureInstantiationError(compilerInstErr, interpreterInstErr)
+	requireNoError(err)
+
+	if okToInvoke {
+		err = ensureInvocationResultMatch(compilerMod, interpreterMod, interpreterCompiled.ExportedFunctions())
+		requireNoError(err)
+	}
+}
+
+const valueTypeVector = 0x7b
+
+// ensureInvocationResultMatch invokes all the exported functions from the module, and compare all the results between compiler vs interpreter.
+func ensureInvocationResultMatch(compiledMod, interpreterMod api.Module, exportedFunctions map[string]api.FunctionDefinition) (err error) {
+	ctx := context.Background()
+
+outer:
+	for name, def := range exportedFunctions {
+		resultTypes := def.ResultTypes()
+		for _, rt := range resultTypes {
+			switch rt {
+			case api.ValueTypeI32, api.ValueTypeI64, api.ValueTypeF32, api.ValueTypeF64, valueTypeVector:
+			default:
+				// For the sake of simplicity in the assertion, we only invoke the function with the basic types.
+				continue outer
+			}
+		}
+
+		cmpF := compiledMod.ExportedFunction(name)
+		intF := interpreterMod.ExportedFunction(name)
+
+		params := getDummyValues(def.ParamTypes())
+		cmpRes, cmpErr := cmpF.Call(ctx, params...)
+		intRes, intErr := intF.Call(ctx, params...)
+		if errMismatch := ensureInvocationError(cmpErr, intErr); errMismatch != nil {
+			panic(fmt.Sprintf("error mismatch on invoking %s: %v", name, errMismatch))
+		}
+
+		matched := true
+		var typesIndex int
+		for i := 0; i < len(cmpRes); i++ {
+			switch resultTypes[typesIndex] {
+			case api.ValueTypeI32, api.ValueTypeF32:
+				matched = matched && uint32(cmpRes[i]) == uint32(intRes[i])
+			case api.ValueTypeI64, api.ValueTypeF64:
+				matched = matched && cmpRes[i] == intRes[i]
+			case valueTypeVector:
+				matched = matched && cmpRes[i] == intRes[i] && cmpRes[i+1] == intRes[i+1]
+				i++ // We need to advance twice (lower and higher 64bits)
+			}
+			typesIndex++
+		}
+
+		if !matched {
+			err = fmt.Errorf("result mismatch on invoking '%s':\n\tinterpreter got: %v\n\tcompiler got: %v", name, intRes, cmpRes)
+		}
+	}
+	return
+}
+
+// getDummyValues returns a dummy input values for function invocations.
+func getDummyValues(valueTypes []api.ValueType) (ret []uint64) {
+	for _, vt := range valueTypes {
+		if vt != 0x7b { // v128
+			ret = append(ret, 0)
+		} else {
+			ret = append(ret, 0, 0)
+		}
+	}
+	return
+}
+
+// ensureInvocationError ensures that function invocation errors returned by interpreter and compiler match each other's.
+func ensureInvocationError(compilerErr, interpErr error) error {
+	if compilerErr == nil && interpErr == nil {
+		return nil
+	} else if compilerErr == nil && interpErr != nil {
+		return fmt.Errorf("compiler returned no error, but interpreter got: %w", interpErr)
+	} else if compilerErr != nil && interpErr == nil {
+		return fmt.Errorf("interpreter returned no error, but compiler got: %w", compilerErr)
+	}
+
+	compilerErrMsg, interpErrMsg := compilerErr.Error(), interpErr.Error()
+	if idx := strings.Index(compilerErrMsg, "\n"); idx >= 0 {
+		compilerErrMsg = compilerErrMsg[:strings.Index(compilerErrMsg, "\n")]
+	}
+	if idx := strings.Index(interpErrMsg, "\n"); idx >= 0 {
+		interpErrMsg = interpErrMsg[:strings.Index(interpErrMsg, "\n")]
+	}
+
+	if compilerErrMsg != interpErrMsg {
+		return fmt.Errorf("error mismatch:\n\tinterpreter: %v\n\tcompiler: %v", interpErr, compilerErr)
+	}
+	return nil
+}
+
+// ensureInstantiationError ensures that instantiation errors returned by interpreter and compiler match each other's.
+func ensureInstantiationError(compilerErr, interpErr error) (okToInvoke bool, err error) {
+	if compilerErr == nil && interpErr == nil {
+		return true, nil
+	} else if compilerErr == nil && interpErr != nil {
+		return false, fmt.Errorf("compiler returned no error, but interpreter got: %w", interpErr)
+	} else if compilerErr != nil && interpErr == nil {
+		return false, fmt.Errorf("interpreter returned no error, but compiler got: %w", compilerErr)
+	}
+
+	compilerErrMsg, interpErrMsg := compilerErr.Error(), interpErr.Error()
+	if idx := strings.Index(compilerErrMsg, "\n"); idx >= 0 {
+		compilerErrMsg = compilerErrMsg[:strings.Index(compilerErrMsg, "\n")]
+	}
+	if idx := strings.Index(interpErrMsg, "\n"); idx >= 0 {
+		interpErrMsg = interpErrMsg[:strings.Index(interpErrMsg, "\n")]
+	}
+
+	if !allowedErrorDuringInstantiation(compilerErrMsg) {
+		return false, fmt.Errorf("invalid error occur with compiler: %v", compilerErr)
+	} else if !allowedErrorDuringInstantiation(interpErrMsg) {
+		return false, fmt.Errorf("invalid error occur with interpreter: %v", interpErrMsg)
+	}
+
+	if compilerErrMsg != interpErrMsg {
+		return false, fmt.Errorf("error mismatch:\n\tinterpreter: %v\n\tcompiler: %v", interpErr, compilerErr)
+	}
+	return false, nil
+}
+
+// allowedErrorDuringInstantiation checks if the error message is considered sane.
+func allowedErrorDuringInstantiation(errMsg string) bool {
+	// This happens when data segment causes out of bound, but it is considered as runtime-error in WebAssembly 2.0
+	// which is fine.
+	if strings.HasPrefix(errMsg, "data[") && strings.HasSuffix(errMsg, "]: out of bounds memory access") {
+		return true
+	}
+
+	// Start function failure is neither instantiation nor compilation error, but rather a runtime error, so that is fine.
+	if strings.HasPrefix(errMsg, "start function[") && strings.Contains(errMsg, "failed: wasm error:") {
+		return true
+	}
+	return false
+}
diff --git a/internal/integration_test/fuzz/wazerolib/lib_test.go b/internal/integration_test/fuzz/wazerolib/nodiff_test.go
similarity index 60%
rename from internal/integration_test/fuzz/wazerolib/lib_test.go
rename to internal/integration_test/fuzz/wazerolib/nodiff_test.go
index 44ad2ea3..85ffcd04 100644
--- a/internal/integration_test/fuzz/wazerolib/lib_test.go
+++ b/internal/integration_test/fuzz/wazerolib/nodiff_test.go
@@ -7,13 +7,13 @@ import (
 	"github.com/tetratelabs/wazero/internal/testing/require"
 )
 
-// TestReRunFailedCase re-runs the failed case specified by WASM_BINARY_NAME in testdata directory.
-func TestReRunFailedCase(t *testing.T) {
+// TestReRunFailedRequireNoDiffCase re-runs the failed case specified by WASM_BINARY_NAME in testdata directory.
+func TestReRunFailedRequireNoDiffCase(t *testing.T) {
 	binaryPath := os.Getenv("WASM_BINARY_PATH")
 
 	wasmBin, err := os.ReadFile(binaryPath)
 	if err != nil {
-		t.Fatal(err)
+		t.Skip(err)
 	}
 
 	requireNoDiff(wasmBin, func(err error) { require.NoError(t, err) })
diff --git a/internal/integration_test/fuzz/wazerolib/validate.go b/internal/integration_test/fuzz/wazerolib/validate.go
new file mode 100644
index 00000000..e0237b85
--- /dev/null
+++ b/internal/integration_test/fuzz/wazerolib/validate.go
@@ -0,0 +1,40 @@
+package main
+
+import "C"
+import (
+	"context"
+	"reflect"
+	"unsafe"
+
+	"github.com/tetratelabs/wazero"
+)
+
+// validate accepts maybe-invalid Wasm module bytes and ensures that our validation phase works correctly
+// as well as the compiler doesn't panic during compilation!
+//
+//export validate
+func validate(binaryPtr uintptr, binarySize int) {
+	wasmBin := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
+		Data: binaryPtr,
+		Len:  binarySize,
+		Cap:  binarySize,
+	}))
+
+	failed := true
+	defer func() {
+		if failed {
+			// If the test fails, we save the binary and wat into testdata directory.
+			saveFailedBinary(wasmBin, "", "TestReRunFailedValidateCase")
+		}
+	}()
+
+	tryCompile(wasmBin)
+	failed = false
+}
+
+// Ensure that validation and compilation do not panic!
+func tryCompile(wasmBin []byte) {
+	ctx := context.Background()
+	compiler := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigCompiler())
+	_, _ = compiler.CompileModule(ctx, wasmBin)
+}
diff --git a/internal/integration_test/fuzz/wazerolib/validate_test.go b/internal/integration_test/fuzz/wazerolib/validate_test.go
new file mode 100644
index 00000000..69cc755f
--- /dev/null
+++ b/internal/integration_test/fuzz/wazerolib/validate_test.go
@@ -0,0 +1,18 @@
+package main
+
+import (
+	"os"
+	"testing"
+)
+
+// TestReRunFailedValidateCase re-runs the failed case specified by WASM_BINARY_NAME in testdata directory.
+func TestReRunFailedValidateCase(t *testing.T) {
+	binaryPath := os.Getenv("WASM_BINARY_PATH")
+
+	wasmBin, err := os.ReadFile(binaryPath)
+	if err != nil {
+		t.Skip(err)
+	}
+
+	tryCompile(wasmBin)
+}
diff --git a/internal/wasm/func_validation_test.go b/internal/wasm/func_validation_test.go
index a899e302..59564e7d 100644
--- a/internal/wasm/func_validation_test.go
+++ b/internal/wasm/func_validation_test.go
@@ -3542,3 +3542,14 @@ func TestModule_funcValidation_loopWithParams(t *testing.T) {
 		})
 	}
 }
+
+// TestFunctionValidation_redundantEnd is found in th validation fuzzing #879.
+func TestFunctionValidation_redundantEnd(t *testing.T) {
+	m := &Module{
+		TypeSection:     []*FunctionType{{}},
+		FunctionSection: []Index{0},
+		CodeSection:     []*Code{{Body: []byte{OpcodeEnd, OpcodeEnd}}},
+	}
+	err := m.validateFunction(api.CoreFeaturesV2, 0, nil, nil, nil, nil, nil)
+	require.EqualError(t, err, "redundant End instruction at 0x1")
+}