Type confusion via incorrect GLB algorithm in [@ wasm::RefType::greatestLowerBound] when nullability and heap-type ordering are crossed
Categories
(Core :: JavaScript: WebAssembly, defect, P1)
Tracking
()
| Tracking | Status | |
|---|---|---|
| firefox-esr115 | --- | unaffected |
| firefox-esr140 | --- | unaffected |
| firefox149 | --- | wontfix |
| firefox150 | + | fixed |
| firefox151 | + | fixed |
People
(Reporter: bugmon, Assigned: bvisness)
References
(Blocks 1 open bug)
Details
(6 keywords, Whiteboard: [prefs-checked][adv-main150+r])
Attachments
(3 files, 3 external files, 3 obsolete files)
Summary
Vulnerability type: Type confusion (Wasm GC type system bypass)
Affected file: js/src/wasm/WasmValType.cpp, lines 146–173 (RefType::greatestLowerBound)
Root cause: RefType::greatestLowerBound(a, b) checks isSubTypeOf(a, b) and isSubTypeOf(b, a) as whole-type comparisons (heap type and nullability together). When a has a more-precise heap type but is nullable, and b has a less-precise heap type but is non-nullable, neither subtype check passes (nullability blocks one direction, heap hierarchy blocks the other). The function falls through to returning bottomType() — i.e., (ref none) — even though the true GLB is a real, inhabited type.
This bogus (ref none) propagates through GVN's ReplaceAllUsesWith, making MWasmRefCastConcrete::foldsTo treat every downstream cast as a trivially-valid upcast (since (ref none) ⊑ everything), eliding the runtime type check entirely.
Affected Code
File: js/src/wasm/WasmValType.cpp, lines 146–173
RefType RefType::greatestLowerBound(RefType a, RefType b) {
MOZ_RELEASE_ASSERT(a.hierarchy() == b.hierarchy());
// ...
bool nullable = a.isNullable() && b.isNullable();
// If one type is a subtype of the other, the lower type is the GLB.
if (RefType::isSubTypeOf(a, b)) { // <-- checks heap AND nullability
MOZ_RELEASE_ASSERT(a.isNullable() == nullable);
return a;
}
if (RefType::isSubTypeOf(b, a)) { // <-- checks heap AND nullability
MOZ_RELEASE_ASSERT(b.isNullable() == nullable);
return b;
}
// The only possible common type now is the hierarchy's bottom type.
return a.bottomType().withIsNullable(nullable); // <-- WRONG when heap types ARE related
}
Why this is wrong: Consider a = (ref null $sub) and b = (ref $super) where $sub <: $super:
| Check | Result | Reason |
|---|---|---|
isSubTypeOf((ref null $sub), (ref $super)) |
false | Line 1398: nullable subtype, non-nullable supertype → reject |
isSubTypeOf((ref $super), (ref null $sub)) |
false | Line 1443: TypeDef::isSubTypeOf($super, $sub) is false |
| Returned value | (ref none) |
Falls through to bottom |
| Correct GLB | (ref $sub) |
{null ∪ $sub-instances} ∩ {$super-instances} = {$sub-instances} |
The algorithm conflates the two axes. A correct GLB must compute the heap-type GLB independently of nullability.
Secondary enabling code:
js/src/jit/MIR-wasm.h, line 2182 — MWasmSelect::congruentTo only compares operand pointers, allowing two selects with different (stale) wasmRefType stamps to be merged:
bool congruentTo(const MDefinition* ins) const override {
return congruentIfOperandsEqual(ins); // does not compare wasmRefType_
}
js/src/jit/ValueNumbering.cpp, line 165 — GVN applies GLB when merging congruent nodes. The code's reasoning (lines 127–164) is actually sound — two congruent nodes do represent the same value, so GLB is the right operation. But the GLB implementation is broken:
to->setWasmRefType(wasm::MaybeRefType::greatestLowerBound(from->wasmRefType(),
to->wasmRefType()));
js/src/jit/MIR-wasm.cpp, lines 944–957 — FoldTrivialWasmCasts elides the cast because (ref none) is a subtype of everything:
static MDefinition* FoldTrivialWasmCasts(MDefinition* ref,
wasm::RefType sourceType,
wasm::RefType destType) {
// Upcasts are trivially valid.
if (wasm::RefType::isSubTypeOf(sourceType, destType)) {
return ref; // <-- elides the cast; returns raw input
}
return nullptr;
}
Correct Pattern
RefType RefType::greatestLowerBound(RefType a, RefType b) {
MOZ_RELEASE_ASSERT(a.hierarchy() == b.hierarchy());
// GLB of nullability: non-nullable wins.
bool nullable = a.isNullable() && b.isNullable();
// Compute the heap-type GLB independently of nullability.
// Normalize both to nullable so isSubTypeOf compares only heap types.
RefType ha = a.withIsNullable(true);
RefType hb = b.withIsNullable(true);
if (RefType::isSubTypeOf(ha, hb)) {
return a.withIsNullable(nullable);
}
if (RefType::isSubTypeOf(hb, ha)) {
return b.withIsNullable(nullable);
}
// Heap types are genuinely unrelated — bottom is correct.
return a.bottomType().withIsNullable(nullable);
}
With this fix, GLB((ref null $sub), (ref $super)) → (ref $sub), and the subsequent cast to $unrelated correctly does not fold (since $sub ⊄ $unrelated).
Exploit Chain
Given Wasm types: $sub <: $super, and $unrelated (no relationship). Parameters $x, $y : (ref $sub).
-
Construct Select A with operands
(ref.cast (ref $super) $x, $y, $cond). DuringTrackWasmRefTypes,computeWasmRefTypestamps it asLUB((ref $super), (ref $sub)) = (ref $super). -
Construct Select B with operands
($x, ref.cast (ref null $sub) $y, $cond).TrackWasmRefTypesstamps it asLUB((ref $sub), (ref null $sub)) = (ref null $sub). -
GVN folds cast A.
ref.cast (ref $super)on(ref $sub)input:isSubTypeOf((ref $sub), (ref $super))= true →FoldTrivialWasmCastsreturns$xdirectly. Select A's operand 0 is now the raw$xMParameter. -
GVN folds cast B.
ref.cast (ref null $sub)on(ref $sub)input:isSubTypeOf((ref $sub), (ref null $sub))= true → returns$ydirectly. Select B's operand 1 is now the raw$yMParameter. -
Both selects now have identical operands
($x, $y, $cond).MWasmSelect::congruentToreturns true. -
GVN merges Select B into Select A.
ReplaceAllUsesWithcomputesGLB((ref null $sub), (ref $super)). NeitherisSubTypeOfcheck passes (nullability blocks one, heap order blocks the other) → returns(ref none). Select A'swasmRefTypeis overwritten with(ref none). -
GVN processes
ref.cast (ref null $unrelated)on the merged select.ref()->wasmRefType() = (ref none).isSubTypeOf((ref none), (ref null $unrelated))= true (none is bottom of Any hierarchy). The cast folds away — no runtime check is emitted. -
struct.set $unrelated Nexecutes on a$subobject. Fields are read/written at$unrelated's offsets against$sub's memory layout → out-of-bounds access, ref/i64 confusion, or wild pointer dereference depending on field types.
Security Impact
Severity: Critical (sec-critical)
Attacker capability: Full Wasm GC type confusion. A web-content-controlled Wasm module compiled with Ion can:
- addrOf primitive (demonstrated): Confuse a struct holding
i64with one holdinganyrefat the same offset → write a JS object reference viastruct.set, read it back asi64viastruct.get. I reproduced this and leaked a JS object pointer:0x14c1a1863038. - fakeObj primitive: The reverse — write an attacker-chosen
i64where the engine later reads it as a GC reference. - OOB write (original PoC): Confuse a small inline-storage struct with a large outline-storage struct →
struct.seton a high-index field dereferences a bogus outline-data pointer and writes attacker-controlled data (rdx = 0xdeadbeefdeadbeef).
Preconditions:
- Wasm GC enabled (shipped by default in Firefox).
- Ion compilation of the Wasm module. In production Firefox, Baseline runs first and traps correctly; the attacker must wait for Ion tier-up (warm-up threshold) or the engine must select Ion as the sole compiler.
- No sandbox escape required — this is reachable from ordinary web content via
WebAssembly.Module/WebAssembly.Instance.
ASAN Report
AddressSanitizer:DEADLYSIGNAL
=================================================================
==372783==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000178 (pc 0x3d1e8b84607f bp 0x7ffebb508d90 sp 0x7ffebb508d90 T0)
==372783==The signal is caused by a WRITE memory access.
==372783==Hint: address points to the zero page.
#0 0x3d1e8b84607f (<unknown module>)
#1 0x3d1e8b8461d8 (<unknown module>)
#2 0x568788f57054 in js::wasm::Instance::callExport(JSContext*, unsigned int, JS::CallArgs const&, js::wasm::CoercionLevel) /firefox/js/src/wasm/WasmInstance.cpp:4067:10
#3 0x568788f55d3c in WasmCall(JSContext*, unsigned int, JS::Value*) /firefox/js/src/wasm/WasmInstance.cpp:3853:19
#4 0x5687887198aa in CallJSNative(JSContext*, bool (*)(JSContext*, unsigned int, JS::Value*), js::CallReason, JS::CallArgs const&) /firefox/js/src/vm/Interpreter.cpp:490:13
#5 0x5687887198aa in js::InternalCallOrConstruct(JSContext*, JS::CallArgs const&, js::MaybeConstruct, js::CallReason) /firefox/js/src/vm/Interpreter.cpp:586:12
#6 0x5687887360d1 in InternalCall(JSContext*, js::AnyInvokeArgs const&, js::CallReason) /firefox/js/src/vm/Interpreter.cpp:653:10
#7 0x5687887360d1 in js::CallFromStack(JSContext*, JS::CallArgs const&, js::CallReason) /firefox/js/src/vm/Interpreter.cpp:658:10
#8 0x5687887360d1 in js::Interpret(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:3272:16
#9 0x568788718796 in MaybeEnterInterpreterTrampoline(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:384:10
#10 0x568788718796 in js::RunScript(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:460:13
#11 0x56878871d3d7 in js::ExecuteKernel(JSContext*, JS::Handle<JSScript*>, JS::Handle<JSObject*>, js::AbstractFramePtr, JS::MutableHandle<JS::Value>) /firefox/js/src/vm/Interpreter.cpp:850:10
#12 0x568788a9fee2 in JS_ExecuteScript(JSContext*, JS::Handle<JSScript*>) /firefox/js/src/vm/CompilationAndEvaluation.cpp:572:10
#13 0x568786bd14c7 in RunFile(JSContext*, char const*, _IO_FILE*, CompileUtf8, bool, bool) /firefox/js/src/shell/js.cpp:1388:10
#14 0x568786bd0b90 in Process(JSContext*, char const*, bool, FileKind) /firefox/js/src/shell/js.cpp
#15 0x568786b7c665 in ProcessArgs(JSContext*, js::cli::OptionParser*) /firefox/js/src/shell/js.cpp:12107:10
#16 0x568786b7c665 in Shell(JSContext*, js::cli::OptionParser*) /firefox/js/src/shell/js.cpp:12360:12
#17 0x568786b69d98 in main /firefox/js/src/shell/js.cpp:12766:12
#18 0x78f1014611c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9)
#19 0x78f10146128a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a)
#20 0x568786a72b18 in _start (/firefox/obj-x86_64-pc-linux-gnu/dist/bin/js+0x2d7eb18)
==372783==Register values:
rax = 0x4141414141414141 rbx = 0x00007ffebb509000 rcx = 0x0000000000000000 rdx = 0xdeadbeefdeadbeef
rdi = 0x00003edbffb009f0 rsi = 0x00003edbffb00a10 rbp = 0x00007ffebb508d90 rsp = 0x00007ffebb508d90
r8 = 0x0000000000000050 r9 = 0x000000000000000a r10 = 0x00007ffebb509000 r11 = 0x00000edaa01142b8
r12 = 0x00007ffebb508e08 r13 = 0x00001000576991e0 r14 = 0x00007661008e7080 r15 = 0x0000000000000000
SUMMARY: AddressSanitizer: SEGV (<unknown module>)
==372783==ABORTING
Register forensics: rax = 0x4141414141414141 is the controlled i64 field value from $sub (the struct.get $super 0 result). rdx = 0xdeadbeefdeadbeef is the controlled struct.set write value. rcx = 0 is the outline-data pointer slot — zero because $sub is an inline-storage struct and has no such pointer; the write lands at [rcx + 0x178] (field 63's offset in the outline buffer). With different type layouts, rcx becomes attacker-controlled.
addrOf variant output (no crash, clean pointer leak):
Iteration 0: leaked value = 0x14c1a1863038
*** TYPE CONFUSION CONFIRMED: leaked a pointer as i64! ***
Suggested Fix
Primary fix — js/src/wasm/WasmValType.cpp: Compute the heap-type GLB independently of nullability.
RefType RefType::greatestLowerBound(RefType a, RefType b) {
MOZ_RELEASE_ASSERT(a.hierarchy() == b.hierarchy());
// GLB of nullability is independent of heap type.
bool nullable = a.isNullable() && b.isNullable();
// Compare heap types ignoring nullability: normalize both to nullable
// so only the heap-type relationship is tested.
RefType ha = a.withIsNullable(true);
RefType hb = b.withIsNullable(true);
if (RefType::isSubTypeOf(ha, hb)) {
// a's heap type is more precise (or equal).
return ha.withIsNullable(nullable);
}
if (RefType::isSubTypeOf(hb, ha)) {
// b's heap type is more precise.
return hb.withIsNullable(nullable);
}
// Heap types are genuinely unrelated in the tree — bottom is correct.
return a.bottomType().withIsNullable(nullable);
}
Defense in depth (optional) — js/src/jit/MIR-wasm.cpp: Reject cast folding when the source type is a non-nullable bottom type. (ref none) is uninhabited — if it appears as a source type, something has already gone wrong and folding should be conservative.
static MDefinition* FoldTrivialWasmCasts(MDefinition* ref,
wasm::RefType sourceType,
wasm::RefType destType) {
// (ref none), (ref nofunc), etc. are uninhabited. A live value with this
// type indicates an analysis bug — do not fold.
if (!sourceType.isNullable() && sourceType.isBottomType()) {
return nullptr;
}
if (wasm::RefType::isSubTypeOf(sourceType, destType)) {
return ref;
}
return nullptr;
}
| Reporter | ||
Comment 1•7 months ago
|
||
| Reporter | ||
Comment 2•7 months ago
|
||
| Reporter | ||
Comment 3•7 months ago
|
||
Updated•7 months ago
|
Updated•7 months ago
|
| Assignee | ||
Comment 5•7 months ago
|
||
Seems legit. Haven't verified the exploit yet but it does seem the logic is incorrect in the way it says, which would have a variety of knock-on effects.
| Assignee | ||
Comment 6•7 months ago
|
||
Updated•7 months ago
|
| Assignee | ||
Comment 7•7 months ago
|
||
Updated•7 months ago
|
Updated•7 months ago
|
Comment 8•7 months ago
|
||
--wasm-compiler=ion is a reproduction aid, not a precondition — Ion is reached via default tier-up in production, and Wasm GC ships by default. Tagging [prefs-checked]; sec-high looks right (type confusion with demonstrated addrOf, web-reachable). Not attaching a fix since D291228/D291229 are already up.
This is an automated analysis result. If this result is incorrect please add a needinfo and feel free to correct the error.
| Assignee | ||
Comment 9•7 months ago
|
||
| Assignee | ||
Comment 10•7 months ago
|
||
| Reporter | ||
Comment 11•7 months ago
|
||
Unable to reproduce bug 2028009 using build mozilla-central 20260331210827-1369941f425d. Without a baseline, bugmon is unable to analyze this bug.
Removing bugmon keyword as no further action possible. Please review the bug and re-add the keyword for further analysis.
Updated•7 months ago
|
Updated•7 months ago
|
| Assignee | ||
Comment 12•7 months ago
|
||
Comment on attachment 9562214 [details]
(secure)
Security Approval Request
- How easily could an exploit be constructed based on the patch?: With medium difficulty, I expect. The patch points directly at the source of a problem (a bad GLB that would produce (ref none), and then an incorrectly-removed cast) but that's only part of a larger exploit chain.
- Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: No
- Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: beta, release
- If not all supported branches, which bug introduced the flaw?: Bug 2000677
- Do you have backports for the affected branches?: No
- If not, how different, hard to create, and risky will they be?: Easy to create, should be no significant differences. Risk will be unchanged.
- How likely is this patch to cause regressions; how much testing does it need?: Unlikely to cause regressions as every change is just checking conditions and bailing out early.
- Is the patch ready to land after security approval is given?: Yes
- Is Android affected?: Unknown
Comment 13•7 months ago
|
||
Comment on attachment 9562214 [details]
(secure)
Approved to land and request uplift
| Assignee | ||
Comment 14•7 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D291228
Updated•7 months ago
|
Comment 15•7 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: Type confusion bug due to mis-optimization of wasm reference types.
- Code covered by automated testing?: yes
- Fix verified in Nightly?: no
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: All modifications are bailing out early from an optimization.
- String changes made/needed?: none
- Is Android affected?: unknown
Comment 16•7 months ago
|
||
firefox-release Uplift Approval Request
- User impact if declined/Reason for urgency: Type confusion bug due to mis-optimization of wasm reference types.
- Code covered by automated testing?: yes
- Fix verified in Nightly?: no
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: All modifications are bailing out early from an optimization.
- String changes made/needed?: none
- Is Android affected?: unknown
| Assignee | ||
Comment 17•7 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D291228
Comment 18•7 months ago
|
||
Comment 19•7 months ago
|
||
Comment 20•7 months ago
|
||
[task 2026-04-02T17:37:59.565+00:00] testWasmRefType_GLB
[task 2026-04-02T17:37:59.576+00:00] /builds/worker/checkouts/gecko/js/src/jsapi-tests/testWasmRefSubtypes.cpp:478:bad GLB of (ref null 0) and (ref any): expected (ref none), got (ref 0)
[task 2026-04-02T17:37:59.576+00:00] TEST-UNEXPECTED-FAIL | testWasmRefType_GLB | /builds/worker/checkouts/gecko/js/src/jsapi-tests/testWasmRefSubtypes.cpp:478:bad GLB of (ref null 0) and (ref any): expected (ref none), got (ref 0)
Comment 21•7 months ago
|
||
Comment on attachment 9563785 [details]
(secure)
Not ready for our planned dot release.
Updated•7 months ago
|
Comment 22•7 months ago
|
||
| Assignee | ||
Comment 23•7 months ago
|
||
Comment on attachment 9563775 [details]
(secure)
This patch has been updated with the required test changes that have also just landed on Nightly. Assuming it's good on Nightly now, all the info in the uplift request should still be valid and a review would be appreciated.
| Assignee | ||
Comment 24•7 months ago
|
||
Comment on attachment 9563785 [details]
(secure)
For good measure, this patch has also been updated. I don't know what the process allows here, but it would be good for this modified patch to be uplifted to release as well if possible.
Comment 25•7 months ago
|
||
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Comment 26•7 months ago
|
||
| uplift | ||
Updated•7 months ago
|
Updated•6 months ago
|
Comment 27•2 months ago
|
||
Comment 28•2 months ago
|
||
Updated•2 months ago
|
Description
•