From a6a0e075eb0b97003d4f90518ddbe1df81aeed2d Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 9 Mar 2026 17:37:55 -0700 Subject: [PATCH 1/5] Handle nullexnref on JS boundary Throw a JSException (modeling a TypeError) when passing nullexnref to JS. We had already handled throwing an error when passing a non-null exnref, but without handling nulls, SignatureRefining could optimize a exnref to a nullexnref on the boundary and change the throwing behavior. This was a bug in the interpreter rather than in the optimization pass. --- src/tools/execution-results.h | 9 +++-- test/lit/exec/fuzzing-api.wast | 65 ++++++++++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/src/tools/execution-results.h b/src/tools/execution-results.h index 56dc3d0af76..f38997c5f14 100644 --- a/src/tools/execution-results.h +++ b/src/tools/execution-results.h @@ -319,8 +319,13 @@ struct LoggingExternalInterface : public ShellExternalInterface { Literals arguments; for (const auto& param : sig.params) { // An i64 param can work from JS, but fuzz_shell provides 0, which errors - // on attempts to convert it to BigInt. v128 and exnref are disalloewd. - if (param == Type::i64 || param == Type::v128 || param.isExn()) { + // on attempts to convert it to BigInt. v128 is disallowed. + if (param == Type::i64 || param == Type::v128) { + throwJSException(); + } + // Exnref and nullexnref are also disallowed. + if (param.isRef() && + HeapType(param.getHeapType().getTop()).isMaybeShared(HeapType::exn)) { throwJSException(); } if (!param.isDefaultable()) { diff --git a/test/lit/exec/fuzzing-api.wast b/test/lit/exec/fuzzing-api.wast index 00db75a19be..73f43ed3397 100644 --- a/test/lit/exec/fuzzing-api.wast +++ b/test/lit/exec/fuzzing-api.wast @@ -357,7 +357,7 @@ ) ) - (func $illegal-result (result v128) + (func $illegal-v128-result (result v128) ;; Helper for the function below. The result is illegal for JS. (call $log-i32 (i32.const 910) @@ -365,15 +365,59 @@ (v128.const i32x4 1 2 3 4) ) - ;; CHECK: [fuzz-exec] calling ref.calling.illegal-result + ;; CHECK: [fuzz-exec] calling ref.calling.illegal-v128-result ;; CHECK-NEXT: [LoggingExternalInterface logging 1] - (func $ref.calling.illegal-result (export "ref.calling.illegal-result") + (func $ref.calling.illegal-v128-result (export "ref.calling.illegal-v128-result") ;; The v128 result causes an error here, so we will log 1 as an exception. The JS ;; semantics determine that we do that check *before* the call, so the logging ;; of 910 does not go through. (call $log-i32 (call $call.ref.catch - (ref.func $illegal-result) + (ref.func $illegal-v128-result) + ) + ) + ) + + (func $illegal-exnref-result (result exnref) + ;; Helper for the function below. The result is illegal for JS. + (call $log-i32 + (i32.const 911) + ) + (block $l (result exnref) + (try_table (catch_all_ref $l) + (call $throwing) + ) + (unreachable) + ) + ) + + ;; CHECK: [fuzz-exec] calling ref.calling.illegal-exnref-result + ;; CHECK-NEXT: [LoggingExternalInterface logging 1] + (func $ref.calling.illegal-exnref-result (export "ref.calling.illegal-exnref-result") + ;; As above with the v128, the exnref cannot be converted to a JS value. + (call $log-i32 + (call $call.ref.catch + (ref.func $illegal-exnref-result) + ) + ) + ) + + (func $illegal-nullexnref-result (result nullexnref) + ;; Helper for the function below. The result is illegal for JS. + (call $log-i32 + (i32.const 912) + ) + (ref.null noexn) + ) + + ;; CHECK: [fuzz-exec] calling ref.calling.illegal-nullexnref-result + ;; CHECK-NEXT: [LoggingExternalInterface logging 912] + ;; CHECK-NEXT: [LoggingExternalInterface logging 0] + (func $ref.calling.illegal-nullexnref-result (export "ref.calling.illegal-nullexnref-result") + ;; As above, the nullexnref cannot be converted to a JS value. + (call $log-i32 + (call $call.ref.catch + (ref.func $illegal-nullexnref-result) ) ) ) @@ -564,9 +608,16 @@ ;; CHECK: [fuzz-exec] calling ref.calling.illegal-exnref ;; CHECK-NEXT: [LoggingExternalInterface logging 1] -;; CHECK: [fuzz-exec] calling ref.calling.illegal-result +;; CHECK: [fuzz-exec] calling ref.calling.illegal-v128-result ;; CHECK-NEXT: [LoggingExternalInterface logging 1] +;; CHECK: [fuzz-exec] calling ref.calling.illegal-exnref-result +;; CHECK-NEXT: [LoggingExternalInterface logging 1] + +;; CHECK: [fuzz-exec] calling ref.calling.illegal-nullexnref-result +;; CHECK-NEXT: [LoggingExternalInterface logging 912] +;; CHECK-NEXT: [LoggingExternalInterface logging 0] + ;; CHECK: [fuzz-exec] calling ref.calling.legal-result ;; CHECK-NEXT: [LoggingExternalInterface logging 910] ;; CHECK-NEXT: [LoggingExternalInterface logging 0] @@ -592,8 +643,10 @@ ;; CHECK-NEXT: [fuzz-exec] comparing ref.calling.catching ;; CHECK-NEXT: [fuzz-exec] comparing ref.calling.illegal ;; CHECK-NEXT: [fuzz-exec] comparing ref.calling.illegal-exnref -;; CHECK-NEXT: [fuzz-exec] comparing ref.calling.illegal-result +;; CHECK-NEXT: [fuzz-exec] comparing ref.calling.illegal-exnref-result +;; CHECK-NEXT: [fuzz-exec] comparing ref.calling.illegal-nullexnref-result ;; CHECK-NEXT: [fuzz-exec] comparing ref.calling.illegal-v128 +;; CHECK-NEXT: [fuzz-exec] comparing ref.calling.illegal-v128-result ;; CHECK-NEXT: [fuzz-exec] comparing ref.calling.legal ;; CHECK-NEXT: [fuzz-exec] comparing ref.calling.legal-result ;; CHECK-NEXT: [fuzz-exec] comparing ref.calling.rethrow From a3c65f7e47fc04c87484680f41b1b7d7acd456b8 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 9 Mar 2026 19:57:31 -0700 Subject: [PATCH 2/5] results, too --- src/tools/execution-results.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tools/execution-results.h b/src/tools/execution-results.h index f38997c5f14..42775361a61 100644 --- a/src/tools/execution-results.h +++ b/src/tools/execution-results.h @@ -339,7 +339,9 @@ struct LoggingExternalInterface : public ShellExternalInterface { for (const auto& result : sig.results) { // An i64 result is fine: a BigInt will be provided. But v128 and exnref // still error. - if (result == Type::v128 || result.isExn()) { + if (result == Type::v128 || + (result.isRef() && HeapType(result.getHeapType().getTop()) + .isMaybeShared(HeapType::exn))) { throwJSException(); } } From 3af391f26512ef578ea00e775e0f8767980bbb58 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 9 Mar 2026 20:24:11 -0700 Subject: [PATCH 3/5] update test --- test/lit/exec/fuzzing-api.wast | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/lit/exec/fuzzing-api.wast b/test/lit/exec/fuzzing-api.wast index 73f43ed3397..e91231229eb 100644 --- a/test/lit/exec/fuzzing-api.wast +++ b/test/lit/exec/fuzzing-api.wast @@ -411,8 +411,7 @@ ) ;; CHECK: [fuzz-exec] calling ref.calling.illegal-nullexnref-result - ;; CHECK-NEXT: [LoggingExternalInterface logging 912] - ;; CHECK-NEXT: [LoggingExternalInterface logging 0] + ;; CHECK-NEXT: [LoggingExternalInterface logging 1] (func $ref.calling.illegal-nullexnref-result (export "ref.calling.illegal-nullexnref-result") ;; As above, the nullexnref cannot be converted to a JS value. (call $log-i32 @@ -615,8 +614,7 @@ ;; CHECK-NEXT: [LoggingExternalInterface logging 1] ;; CHECK: [fuzz-exec] calling ref.calling.illegal-nullexnref-result -;; CHECK-NEXT: [LoggingExternalInterface logging 912] -;; CHECK-NEXT: [LoggingExternalInterface logging 0] +;; CHECK-NEXT: [LoggingExternalInterface logging 1] ;; CHECK: [fuzz-exec] calling ref.calling.legal-result ;; CHECK-NEXT: [LoggingExternalInterface logging 910] From 0b48a8de6eaf3496ccddadf04149e34a4f195b89 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 10 Mar 2026 10:21:16 -0700 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Alon Zakai --- src/tools/execution-results.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/execution-results.h b/src/tools/execution-results.h index 42775361a61..a0b19390efc 100644 --- a/src/tools/execution-results.h +++ b/src/tools/execution-results.h @@ -337,7 +337,7 @@ struct LoggingExternalInterface : public ShellExternalInterface { // Error on illegal results. Note that this happens, as per JS semantics, // *before* the call. for (const auto& result : sig.results) { - // An i64 result is fine: a BigInt will be provided. But v128 and exnref + // An i64 result is fine: a BigInt will be provided. But v128 and [null]exnref // still error. if (result == Type::v128 || (result.isRef() && HeapType(result.getHeapType().getTop()) From 4c0ff1a4ac5e785cf90f27eee489e1ab4d8eaa91 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 10 Mar 2026 10:54:58 -0700 Subject: [PATCH 5/5] format --- src/tools/execution-results.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/execution-results.h b/src/tools/execution-results.h index a0b19390efc..f3b45c049d6 100644 --- a/src/tools/execution-results.h +++ b/src/tools/execution-results.h @@ -337,8 +337,8 @@ struct LoggingExternalInterface : public ShellExternalInterface { // Error on illegal results. Note that this happens, as per JS semantics, // *before* the call. for (const auto& result : sig.results) { - // An i64 result is fine: a BigInt will be provided. But v128 and [null]exnref - // still error. + // An i64 result is fine: a BigInt will be provided. But v128 and + // [null]exnref still error. if (result == Type::v128 || (result.isRef() && HeapType(result.getHeapType().getTop()) .isMaybeShared(HeapType::exn))) {