From 8c9008b262023f01b39ce37568aae5ff5858927b Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 28 Aug 2026 12:55:06 +0000 Subject: [PATCH] Wasm: accept subtypes in i31.get_s/u and any.convert_extern, survive a failed BBQ compile The validator required the operand of i31.get_s and i31.get_u to be exactly I31ref, and the operand of any.convert_extern to be exactly Externref. The wasm GC spec types these operands as (ref null i31) and (ref null extern), so any subtype is valid, including the bottom types none and noextern. A module with (i31.get_s (ref.null none)) was rejected with 'expected I31ref' even though other engines accept it. Use isSubtype, like every other reference-typed operand check in the parser. Each tier already handles a nullable operand with a runtime null trap, and the IPInt interpreter checks null unconditionally. BBQPlan::work also dereferenced the result of compileFunction without a null check. A BBQ-tier validation failure cannot happen for a module that already validated, but bugs that broke that invariant (such as the block-result typing fixed in 8f229fb72961) turned into a segfault on the compiler thread, reported in oven-sh/bun#40770 as a crash in computeExceptionHandlerAndLoopEntrypointLocations. Return early instead; compileFunction already completed the plan through fail(), and the function keeps running in the interpreter tier. JSTests/wasm/stress/i31-get-bottom-type.js covers the accepted subtypes, the runtime trap, and the br_on_null block-result pattern from the bun issue across the BBQ and OMG tiers. --- JSTests/wasm/stress/i31-get-bottom-type.js | 55 +++++++++++++++++++ Source/JavaScriptCore/wasm/WasmBBQPlan.cpp | 5 ++ .../JavaScriptCore/wasm/WasmFunctionParser.h | 6 +- 3 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 JSTests/wasm/stress/i31-get-bottom-type.js diff --git a/JSTests/wasm/stress/i31-get-bottom-type.js b/JSTests/wasm/stress/i31-get-bottom-type.js new file mode 100644 index 000000000000..aa86784f3ced --- /dev/null +++ b/JSTests/wasm/stress/i31-get-bottom-type.js @@ -0,0 +1,55 @@ +//@ runDefaultWasm("-m", "--useConcurrentJIT=0", "--thresholdForBBQOptimizeAfterWarmUp=10", "--thresholdForOMGOptimizeAfterWarmUp=20") + +import { instantiate } from "../gc/wast-wrapper.js"; +import * as assert from "../assert.js"; + +// i31.get_s and i31.get_u accept any subtype of i31ref, including the +// bottom type none. A null operand traps at runtime. +function testI31GetBottom() { + for (const op of ["i31.get_s", "i31.get_u"]) { + const m = instantiate(` + (module + (func (export "f") (result i32) + (${op} (ref.null none)))) + `); + for (let i = 0; i < wasmTestLoopCount; i++) + assert.throws(() => m.exports.f(), WebAssembly.RuntimeError, "i31.get_ to a null reference"); + } +} + +// any.convert_extern accepts any subtype of externref, including the +// bottom type noextern. +function testAnyConvertExternBottom() { + const m = instantiate(` + (module + (func (export "f") (result anyref) + (any.convert_extern (ref.null noextern)))) + `); + for (let i = 0; i < wasmTestLoopCount; i++) + assert.eq(m.exports.f(), null); +} + +// After a reachable block end, the BBQ pass keeps the precise type of the +// block result, here (ref null none), while the validating pass used the +// declared i31ref. The i31.get_s type check then failed only in the BBQ +// pass, and BBQPlan::work dereferenced the failed compilation result +// (https://github.com/oven-sh/bun/issues/40770). +function testI31GetBrOnNullBlockResult() { + const m = instantiate(` + (module + (func (export "f") (result i32) + (block $l1 + (return + (i31.get_s + (br_on_null $l1 + (block $l2 (result i31ref) + (ref.null none)))))) + (i32.const -1))) + `); + for (let i = 0; i < wasmTestLoopCount; i++) + assert.eq(m.exports.f(), -1); +} + +testI31GetBottom(); +testAnyConvertExternBottom(); +testI31GetBrOnNullBlockResult(); diff --git a/Source/JavaScriptCore/wasm/WasmBBQPlan.cpp b/Source/JavaScriptCore/wasm/WasmBBQPlan.cpp index 158107aae7a6..ed9dbffef349 100644 --- a/Source/JavaScriptCore/wasm/WasmBBQPlan.cpp +++ b/Source/JavaScriptCore/wasm/WasmBBQPlan.cpp @@ -98,6 +98,11 @@ void BBQPlan::work() Ref callee = BBQCallee::create(functionIndexSpace, m_moduleInformation->nameSection().get(functionIndexSpace), Ref { m_profiledCallee }); std::unique_ptr function = compileFunction(m_functionIndex, callee.get(), context, unlinkedWasmToWasmCalls); + if (!function) [[unlikely]] { + // compileFunction already called fail(), which completed the plan. + // The function keeps running in the interpreter tier. + return; + } // The finished code is patched further (wasm call-site linking in installOptimizedCallee) and // flushed once afterward, so skip LinkBuffer's finalize instruction-cache flush. diff --git a/Source/JavaScriptCore/wasm/WasmFunctionParser.h b/Source/JavaScriptCore/wasm/WasmFunctionParser.h index 4f4cf92ad41f..cc225aa7aea1 100644 --- a/Source/JavaScriptCore/wasm/WasmFunctionParser.h +++ b/Source/JavaScriptCore/wasm/WasmFunctionParser.h @@ -2463,7 +2463,7 @@ FOR_EACH_WASM_MEMORY_STORE_OP(CREATE_CASE) case ExtGCOpType::I31GetS: { TypedExpression ref; WASM_TRY_POP_EXPRESSION_STACK_INTO(ref, "i31.get_s"); - WASM_VALIDATOR_FAIL_IF(!isI31ref(ref.type()), "i31.get_s ref to type ", ref.type(), " expected ", TypeKind::I31ref); + WASM_VALIDATOR_FAIL_IF(!isSubtype(ref.type(), Type { TypeKind::RefNull, typeIndexFromTypeKind(TypeKind::I31ref) }), "i31.get_s ref to type ", ref.type(), " expected ", TypeKind::I31ref); ExpressionType result; WASM_TRY_ADD_TO_CONTEXT(addI31GetS(ref, result)); @@ -2474,7 +2474,7 @@ FOR_EACH_WASM_MEMORY_STORE_OP(CREATE_CASE) case ExtGCOpType::I31GetU: { TypedExpression ref; WASM_TRY_POP_EXPRESSION_STACK_INTO(ref, "i31.get_u"); - WASM_VALIDATOR_FAIL_IF(!isI31ref(ref.type()), "i31.get_u ref to type ", ref.type(), " expected ", TypeKind::I31ref); + WASM_VALIDATOR_FAIL_IF(!isSubtype(ref.type(), Type { TypeKind::RefNull, typeIndexFromTypeKind(TypeKind::I31ref) }), "i31.get_u ref to type ", ref.type(), " expected ", TypeKind::I31ref); ExpressionType result; WASM_TRY_ADD_TO_CONTEXT(addI31GetU(ref, result)); @@ -3024,7 +3024,7 @@ FOR_EACH_WASM_MEMORY_STORE_OP(CREATE_CASE) case ExtGCOpType::AnyConvertExtern: { TypedExpression reference; WASM_TRY_POP_EXPRESSION_STACK_INTO(reference, "any.convert_extern"_s); - WASM_VALIDATOR_FAIL_IF(!isExternref(reference.type()), "any.convert_extern reference to type "_s, reference.type(), " expected "_s, TypeKind::Externref); + WASM_VALIDATOR_FAIL_IF(!isSubtype(reference.type(), externrefType()), "any.convert_extern reference to type "_s, reference.type(), " expected "_s, TypeKind::Externref); ExpressionType result; WASM_TRY_ADD_TO_CONTEXT(addAnyConvertExtern(reference, result));