Skip to content

Wasm: accept subtypes in i31.get_s/u and any.convert_extern, survive a failed BBQ compile - #532

Open
robobun wants to merge 1 commit into
mainfrom
fix/bbq-i31-get-block-result
Open

Wasm: accept subtypes in i31.get_s/u and any.convert_extern, survive a failed BBQ compile#532
robobun wants to merge 1 commit into
mainfrom
fix/bbq-i31-get-block-result

Conversation

@robobun

@robobun robobun commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • The validator requires 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 spec types these operands as (ref null i31) and (ref null extern), so any subtype is valid, including the bottom types none and noextern. (i31.get_s (ref.null none)) fails with "expected I31ref". Other engines and the reference interpreter accept it.
  • BBQPlan::work dereferences the result of compileFunction without a null check. When a BBQ-tier compile fails, it segfaults at function->bbqLoopEntrypoints (address 0x28) on the compiler thread. Segfault in the BBQ wasm JIT on i31.get_s (br_on_null ...) of a block result bun#40770 hit this through the block-result typing bug fixed in 8f229fb: the BBQ pass kept (ref none) for a block result where the validating pass used the declared i31ref, so only the BBQ pass failed the exact-match check.

Fix

  • Use isSubtype for the three checks, like every other reference-typed operand check in the parser. Each JIT tier already emits a runtime null trap for a nullable operand, and the IPInt interpreter checks null unconditionally, so a bottom-typed (always null) operand traps correctly at runtime.
  • Return early from BBQPlan::work when compileFunction returns null. compileFunction already completed the plan through fail(). The function keeps running in the interpreter tier instead of crashing the process.
  • Verified: JSTests/wasm/stress/i31-get-bottom-type.js fails on main ("doesn't validate: i31.get_s ref to type (ref null none) expected I31ref") and passes with the fix. run-jsc-stress-tests JSTests/wasm.yaml --filter i31 and --filter extern pass on a debug ASAN build, except two pre-existing failures that fail identically on an unfixed shell (gc/i31.wast.js.wasm-collect-continuously, a JSPromise assertion flake, and externref-result-tuple.js, which needs shared memory enabled).

Background

  • The wasm GC type lattice has a bottom type per hierarchy: none for internal refs, noextern for external refs. A (ref null none) value is always null. Validation-level subtyping admits it anywhere an internal ref type is expected.
  • JSC validates each function once at module load (through the IPInt generation pass), and each JIT tier re-runs the same FunctionParser checks at compile time. A check that passes at load time must pass in every tier, or the tier-up compile fails on a module the embedder already accepted.
  • 8f229fb made block ends re-type results to the declared signature in every pass, which closed the type divergence between passes. The null check here makes any future divergence degrade to a graceful compile failure instead of a segfault.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and didn't find any bugs. Because it relaxes Wasm GC type validation (which gates what the JIT tiers will compile), a human familiar with the Wasm backends should confirm each tier's addI31GetS/U and addAnyConvertExtern handle a bottom-typed (always-null) operand safely.

What was reviewed

  • The three isSubtype checks in WasmFunctionParser.h match the existing pattern used for array.len (line 2700), ref.cast externref (line 2927), and extern.convert_any (line 3037).
  • The BBQPlan null-check: verified compileFunction() calls fail() before returning nullptr (line 174-177), and the early-return mirrors the existing didFailToAllocate() handling just below.
  • The new stress test follows JSTests/README.md conventions: wasmTestLoopCount for tier-up, throwing assertions, no stdout, correct directory.
Extended reasoning...

Overview

This PR touches three files: Source/JavaScriptCore/wasm/WasmFunctionParser.h (three one-line validator changes replacing exact-type checks with isSubtype checks for i31.get_s, i31.get_u, and any.convert_extern), Source/JavaScriptCore/wasm/WasmBBQPlan.cpp (a 4-line null-check after compileFunction() in BBQPlan::work()), and a new regression test JSTests/wasm/stress/i31-get-bottom-type.js. The parser change brings these three opcodes in line with the Wasm GC spec, which types their operands as (ref null i31) / (ref null extern) and therefore admits any subtype including the bottom types none / noextern. The BBQPlan change hardens against a null-pointer dereference when a BBQ-tier compile fails validation after the module already loaded.

Security risks

The validator change is the security-relevant piece: it widens the set of operand types the parser accepts for three GC opcodes. If any downstream JIT tier's addI31GetS/U or addAnyConvertExtern implementation assumed the operand type was exactly i31ref/externref and did something unsafe with a (ref none) or (ref noextern) operand, this could introduce type confusion. The PR description states that each JIT tier already emits a runtime null trap for nullable operands and IPInt checks null unconditionally, and the new test exercises the null-trap path across tiers via wasmTestLoopCount. The pattern is identical to how array.len, ref.cast, and extern.convert_any already validate their operands in the same file, which strongly suggests the tiers are built for subtype inputs. Still, validator relaxations in a JIT engine are the kind of change where a second pair of eyes from someone who knows the BBQ/OMG lowering paths is prudent. The BBQPlan null-check is purely defensive and reduces risk (crash → graceful degradation to interpreter tier).

Level of scrutiny

Medium. The diff is small (~15 lines of production code), mechanically follows established patterns in the same file, and comes with a targeted regression test that the author reports fails on main and passes with the fix under debug ASAN. However, per the JSC CLAUDE.md guidance that "a change in one tier usually needs changes in others," and because Wasm type validation gates what the JIT compiles, a reviewer familiar with the Wasm tier implementations should sign off rather than an automated approval.

Other factors

I confirmed BBQPlan::compileFunction() does call fail() before returning nullptr (WasmBBQPlan.cpp:174-177), so the new early-return correctly relies on the plan already being completed — matching the linkBuffer.didFailToAllocate() branch immediately below it. No CODEOWNERS file governs these paths. The test file adheres to JSTests conventions (uses wasmTestLoopCount, throwing assertions from ../assert.js, no logging, placed in wasm/stress/, includes a //@ runDefaultWasm directive to force tier-up thresholds). No outstanding reviewer objections in the timeline.

@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ed5bd545-cd81-4425-a7c3-466e9099888b

📥 Commits

Reviewing files that changed from the base of the PR and between d71031a and 8c9008b.

📒 Files selected for processing (3)
  • JSTests/wasm/stress/i31-get-bottom-type.js
  • Source/JavaScriptCore/wasm/WasmBBQPlan.cpp
  • Source/JavaScriptCore/wasm/WasmFunctionParser.h

Included review availability: Your plan provides up to 5 included reviews per hour; 2 remain after this review.


Walkthrough

The change updates WebAssembly subtype validation, prevents failed BBQ compilation from installing missing code, and adds stress tests for bottom-type references, null traps, externref conversion, and br_on_null.

Changes

WebAssembly bottom-type handling

Layer / File(s) Summary
Bottom-type instruction validation
Source/JavaScriptCore/wasm/WasmFunctionParser.h
i31.get_s, i31.get_u, and any.convert_extern now accept the relevant nullable reference subtypes through isSubtype.
Failed BBQ compilation fallback
Source/JavaScriptCore/wasm/WasmBBQPlan.cpp
BBQPlan::work() returns after compilation failure and leaves execution in the interpreter tier.
Bottom-type stress coverage
JSTests/wasm/stress/i31-get-bottom-type.js
Repeated tests cover null traps, (ref.null noextern) conversion to JavaScript null, and br_on_null with a precise bottom-typed result.

Merge Risk: ⚪ Minimal · up to 8c900

This PR broadens valid WebAssembly reference typing and prevents a failed tiered compile from crashing the process; no actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the problem, fix, background, affected behavior, and verification results. However, it does not follow the repository template because it omits the bug title, Bugzilla link, r… Add the associated Bugzilla URL and bug title, include the required “Reviewed by NOBODY (OOPS!).” line or actual review status, and list each changed file with relevant functions or classes.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes both primary changes: subtype acceptance for WebAssembly operations and safe handling of failed BBQ compilation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description explains the problem, fix, background, affected behavior, and verification results. However, it does not follow the repository template because it omits the bug title, Bugzilla link, review status, and changed-file/function list.

  • Fix all pre-merge checks with AI

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown

Preview Builds

Commit Release Date
8c9008b2 autobuild-preview-pr-532-8c9008b2 2026-08-29 05:47:08 UTC
966a3d77 autobuild-preview-pr-532-966a3d77 2026-08-28 13:26:16 UTC

robobun added a commit to oven-sh/bun that referenced this pull request Aug 28, 2026
…operands

i31.get_s, i31.get_u and any.convert_extern required their operand type
to be exactly i31ref or externref. The wasm GC spec accepts any subtype,
including the bottom types none and noextern. Modules that other engines
accept failed to validate with 'expected I31ref'.

The same exact-match check, combined with a block-result typing bug the
previous WebKit upgrade fixed, made the background BBQ tier-up compile
fail on modules that already validated, and BBQPlan::work dereferenced
the failed result: the segfault reported in issue #40770. The engine now
checks subtypes (oven-sh/WebKit#532) and a failed BBQ compile leaves the
function on the interpreter tier instead of crashing.

Fixes #40770
…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 8f229fb) 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.
@robobun
robobun force-pushed the fix/bbq-i31-get-block-result branch from 966a3d7 to 8c9008b Compare August 29, 2026 05:12
@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

robobun added a commit to oven-sh/bun that referenced this pull request Aug 29, 2026
…operands

i31.get_s, i31.get_u and any.convert_extern required their operand type
to be exactly i31ref or externref. The wasm GC spec accepts any subtype,
including the bottom types none and noextern. Modules that other engines
accept failed to validate with 'expected I31ref'.

The same exact-match check, combined with a block-result typing bug the
previous WebKit upgrade fixed, made the background BBQ tier-up compile
fail on modules that already validated, and BBQPlan::work dereferenced
the failed result: the segfault reported in issue #40770. The engine now
checks subtypes (oven-sh/WebKit#532) and a failed BBQ compile leaves the
function on the interpreter tier instead of crashing.

Fixes #40770
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant