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)
Tracking
()
| 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):
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 throughCheckRefType, which acceptsNoContviaCheckNullContRefValue.new WebAssembly.Tag({parameters: ["contref", "nullcontref"]})— aTagTypewhose argument layout contains cont-hierarchy reference slots.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-bearingFuncTypeis canonicalized into the process-global type context and visible viaf.type().WebAssembly.promising(f)for such anf—WasmPI.cppbuilds a builtin module withfeatures.stackSwitchingforce-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 anyContObjectfor any cont type.ValType::isExposable()(WasmValType.h:853) only rejects the abstractcontref/nullcontref; a concrete(ref $cont)type-ref is considered exposable, so thehasUnexposableArgOrRetgates (WasmInstance.cpp:259,:4017) do not cover it.RefType::isRefBottom()(WasmValType.h:460) omitsnocont, so non-nullable(ref nocont)is wrongly considered inhabitable byisInhabitable()/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. Runjs --fuzzing-safe poc.js(SpiderMonkey) vsd8 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.promisingIon-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()toRefType::isRefBottom()(WasmValType.h:460); - make
ValType::isExposable()treat concrete cont type-refs (isTypeRef() && typeDef()->isContType()) as unexposable; - replace the
CheckTypeRefValuecont "TODO: skipping type check" with a real subtype check before stack switching ships.
| Reporter | ||
Comment 1•2 months ago
|
||
| Reporter | ||
Comment 2•2 months ago
|
||
| Assignee | ||
Comment 3•2 months ago
|
||
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 | ||
Comment 4•2 months ago
|
||
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.
| Assignee | ||
Comment 5•2 months ago
|
||
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.
Updated•2 months ago
|
Backed out for causing multiple wasm failures.
- Backout link
- Push with failures
- Failure Log sm build
- Failure Log jit
- Failure Log xpc
Comment 10•2 months ago
|
||
| bugherder | ||
| Assignee | ||
Updated•2 months ago
|
Updated•2 months ago
|
Description
•