Closed Bug 2045432 Opened 2 months ago Closed 2 months ago

Differential behavior: ToRefType (WasmValType.cpp) accepts "contref"/"nullcontref" without a StackSwitchingAvailable runtime gate, letting content JS mint disabled stack-switching types

Categories

(Core :: JavaScript: WebAssembly, defect, P2)

x86_64
Linux
defect

Tracking

()

RESOLVED FIXED
153 Branch
Tracking Status
firefox153 --- fixed

People

(Reporter: decoder, Assigned: rhunt)

References

(Blocks 1 open bug)

Details

(Keywords: ai-involved, testcase)

Attachments

(3 files)

Summary

The JS-API string-to-wasm-type parser ToRefType in js/src/wasm/WasmValType.cpp accepts the typed-continuation (stack-switching) reference type strings "contref" and "nullcontref" guarded only by the compile-time ENABLE_WASM_JSPI flag (js/src/wasm/WasmValType.cpp:256-265, added by the stack-switching type-system patch from bug 2023217). It is not guarded by the runtime feature flag (javascript.options.wasm_stack_switching, default false for all users on all channels, modules/libpref/init/StaticPrefList.yaml:9749).

As a result, while wasm validation in the same runtime hard-rejects continuation types ("stack switching not enabled"), the JS API manufactures live engine objects whose types belong to the disabled feature's type hierarchy — with no flags and no pref changes, under --fuzzing-safe defaults.

This breaks the file's own convention: wasm::ToValType in the same file gates "v128" on SimdAvailable(cx) (WasmValType.cpp:298), and "exnref" was gated on ExnRefAvailable(cx) in this exact function until exception handling shipped (gate removed in bug 1967231).

What content JS can mint while the feature is disabled

Verified on the shells below (--fuzzing-safe, no extra flags):

  1. new WebAssembly.Table({element: "nullcontref", initial: N}) — a live, growable table of element type (ref null nocont); set(i, null) / grow(n) / type() work. The Table constructor path goes through CheckRefType, which accepts NoCont via CheckNullContRefValue.
  2. new WebAssembly.Tag({parameters: ["contref", "nullcontref"]}) — a TagType whose argument layout contains cont-hierarchy reference slots.
  3. new WebAssembly.Function({parameters:["contref"], results:["nullcontref"]}, f)WasmFunctionCreate (WasmJS.cpp) synthesizes, compiles and instantiates a real wasm module with no binary validation; the cont-bearing FuncType is canonicalized into the process-global type context and visible via f.type().
  4. WebAssembly.promising(f) for such an fWasmPI.cpp builds a builtin module with features.stackSwitching force-enabled (options.isBuiltinModule = true, WasmCompile.cpp), clones the rogue type context and Ion-compiles GC struct types with cont-typed fields and code passing cont-typed values — all while the feature pref is off.

Why this is security-relevant

The engine outside this parser assumes "validation prevents continuation types from existing" while the feature is disabled. The minted objects sit directly on top of known-immature machinery with latent type-confusion hazards:

  • wasm::CheckTypeRefValue (WasmValue.cpp:306) deliberately skips the type check for continuation objects (// TODO: skipping type check to get JS-PI working) — accepts any ContObject for any cont type.
  • ValType::isExposable() (WasmValType.h:853) only rejects the abstract contref/nullcontref; a concrete (ref $cont) type-ref is considered exposable, so the hasUnexposableArgOrRet gates (WasmInstance.cpp:259, :4017) do not cover it.
  • RefType::isRefBottom() (WasmValType.h:460) omits nocont, so non-nullable (ref nocont) is wrongly considered inhabitable by isInhabitable()/castPossible().
  • MacroAssembler::branchWasmRefIsSubtype (MacroAssembler.cpp:7146) MOZ_CRASHes on the cont hierarchy.

This is not a shell-only issue: WebAssembly.Table/Tag/Function are web APIs, ENABLE_WASM_JSPI is enabled in Firefox Nightly x64 builds, and the pref javascript.options.wasm_stack_switching is false for all users (so the gate that should protect them is missing). Nightly-only exposure via the compile-time flag.

Reproduction

Testcases are attached:

  • poc.js — differential testcase. Run js --fuzzing-safe poc.js (SpiderMonkey) vs d8 poc.js (V8).
  • reach.js — supplementary SpiderMonkey-only demonstration of propagation depth (live table mutation/growth, tag layouts, an unvalidated compiled module with a cont-bearing canonical signature, WebAssembly.promising Ion-compiling cont-field structs).

SpiderMonkey output (debug ASan shell, identical on the opt ASan shell):

Table {element:'nullcontref'}: ACCEPTED
Tag {parameters:['contref']}: ACCEPTED
Tag {parameters:['nullcontref']}: ACCEPTED
module with (cont $f) type: REJECTED

V8 output:

Table {element:'nullcontref'}: REJECTED
Tag {parameters:['contref']}: REJECTED
Tag {parameters:['nullcontref']}: REJECTED
module with (cont $f) type: REJECTED

The first three lines diverge; with correct gating SpiderMonkey would print REJECTED for all four (as it does for every other unshipped type string, and as V8 does). The last (control) line shows wasm validation in the same SpiderMonkey shell rejecting the feature.

Tested on mozilla-central 20260603-5cffec5992d3 (asan-debug and fuzzing-asan-opt JS shells, Linux x86_64).

Suggested fix

In ToRefType (js/src/wasm/WasmValType.cpp), gate the two literals on the runtime feature, mirroring the SimdAvailable(cx) gate used for "v128" in wasm::ToValType in the same file:

#ifdef ENABLE_WASM_JSPI
  if (StackSwitchingAvailable(cx)) {
    if (StringEqualsLiteral(typeLinearStr, "contref")) { ... }
    if (StringEqualsLiteral(typeLinearStr, "nullcontref")) { ... }
  }
#endif

Defense-in-depth follow-ups found while auditing the blast radius:

  • add isNoCont() to RefType::isRefBottom() (WasmValType.h:460);
  • make ValType::isExposable() treat concrete cont type-refs (isTypeRef() && typeDef()->isContType()) as unexposable;
  • replace the CheckTypeRefValue cont "TODO: skipping type check" with a real subtype check before stack switching ships.

We should try to fix this before shipping JS-PI. I don't think this is exploitable. The worst case are some assertions, but still not great.

Assignee: nobody → rhunt
Severity: -- → S2
Priority: -- → P2

ToRefType accepted "contref" and "nullcontref" strings when only the
compile-time ENABLE_WASM_JSPI flag was set, without checking the runtime
pref. This allowed JS content to mint continuation-typed objects
(WebAssembly.Table, Tag, Function) while stack switching was disabled.

Gate the two literals on StackSwitchingAvailable(cx), mirroring how
ToValType gates "v128" on SimdAvailable(cx).

Additionally:

  • Add isNoCont() to RefType::isRefBottom() so that non-nullable (ref nocont)
    is correctly treated as uninhabitable by isInhabitable()/castPossible().
  • Simplify PackedType::isExposable() using isVector(), hierarchy(), and an
    early return for non-ref types, replacing the per-type checks for cont
    and exn types.

This is not exploitable. User code can trigger assertions, but no way to violate the sandbox. The only possible weird values to 'mint' are null values, which are safe. There's no way to chain it further into the prototype stack-switching implementation.

Group: javascript-core-security
Keywords: sec-audit
Attachment #9595693 - Attachment description: (secure) → Bug 2045432 - Gate contref/nullcontref JS API on StackSwitchingAvailable runtime pref. r?yury
Pushed by rhunt@eqrion.net: https://github.com/mozilla-firefox/firefox/commit/8984929ad021 https://hg.mozilla.org/integration/autoland/rev/1fe2a6c630da Gate contref/nullcontref JS API on StackSwitchingAvailable runtime pref. r=yury
Pushed by agoloman@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/b264de7f0d86 https://hg.mozilla.org/integration/autoland/rev/14d651352449 Revert "Bug 2045432 - Gate contref/nullcontref JS API on StackSwitchingAvailable runtime pref. r=yury" for causing xpc failures @test_wasm_jspi_profiler.js.

Backed out for causing multiple wasm failures.

Flags: needinfo?(rhunt)
Pushed by rhunt@eqrion.net: https://github.com/mozilla-firefox/firefox/commit/61c9eab6351a https://hg.mozilla.org/integration/autoland/rev/4cf62ab68922 Gate contref/nullcontref JS API on StackSwitchingAvailable runtime pref. r=yury
Status: NEW → RESOLVED
Closed: 2 months ago
Resolution: --- → FIXED
Target Milestone: --- → 153 Branch
Flags: needinfo?(rhunt)
QA Whiteboard: [qa-triage-done-c154/b153]
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: