Medium webkit Type Confusion 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactProcessing maliciously crafted web content may lead to arbitrary code execution
DescriptionA type confusion issue was addressed with improved state management.
ComponentJSC
Bug ClassType Confusion
Tracker308231
Fix commitaf957bf20474 (webkit) +52/-1
CISA KEVNot listed
CreditedNathaniel Oh (@calysteon), Kwak Kiyong / kakaogames
Disclosed2026-09-14

Background

B3 / `B3ReduceStrength`
JSC’s low-level optimizing IR and its strength-reduction pass (Source/JavaScriptCore/b3/B3ReduceStrength.cpp). It constant-folds and simplifies IR nodes, including WebAssembly GC cast operations lowered into B3.
WasmRefCast / WasmRefTest
WebAssembly GC reference cast operations. ref.test/br_on_cast* test whether a reference’s runtime type (RTT) is a subtype of a target type; ref.cast asserts it. The IR node carries a targetRTT(), a targetHeapType(), and a shouldNegate() flag.
`shouldNegate()`
Set for the negated form of the test (e.g. br_on_cast_fail, which branches when the cast would fail). It inverts the boolean meaning of the subtype check.
`isSubRTT` / RTT
rtt->isSubRTT(*targetRTT) returns whether the value’s runtime type is a subtype of the target — i.e., whether the cast succeeds.

Root Cause Analysis

When B3ReduceStrength could resolve a cast’s subtype relationship at compile time (the non-typeIndexIsType branch at B3ReduceStrength.cpp:~3671), it folded the result to a constant with replaceWithNewValue(m_proc.addIntConstant(m_value, !!rtt->isSubRTT(*targetRTT))). This used the raw subtype result and ignored cast->shouldNegate(). For a negated cast such as br_on_cast_fail, the correct folded value is the inverse of the subtype test; by folding to the un-negated isSubRTT result, the optimizer produced a constant with inverted polarity. The branch/select driven by that constant then takes the wrong path: a reference that actually fails the cast is treated as having passed (or vice versa), so downstream code uses the reference as the cast-target struct type when it is in fact a different, incompatible type — a type confusion between WebAssembly GC struct types.

The patch computes const bool isSubtype = rtt->isSubRTT(*targetRTT); and folds to cast->shouldNegate() ? !isSubtype : isSubtype, honoring the negation flag so the constant matches the operation’s true semantics. The companion hunk at ~3604 (the structNew identity-replacement branch for a succeeding subtype) adds ASSERT(!cast->shouldNegate()), documenting/enforcing that this success-fold path is only valid for the non-negated WasmRefTest form and that a negated br_on_cast_fail must not reach it. The regression test (JSTests/wasm/gc/br_on_cast_fail.js) exports fail()/pass() built on br_on_cast_fail and runs them 200000 times to force tier-up into B3, then checks both return the correct values — failing if the folded polarity is wrong.

Key insight
B3’s constant-folding of a WebAssembly cast used isSubRTT() directly and dropped the shouldNegate() flag, so negated casts (br_on_cast_fail) folded to the inverted boolean — turning a failed cast into an apparent success and confusing one GC struct type for another.

Attack Path

  1. Craft a wasm GC module Attacker builds a module with two struct types (e.g. a small struct {i32} and a larger struct {i32,i64,i64}) and a function using br_on_cast_fail to cast between them.
  2. Force optimization The function is called in a hot loop (the test uses 200000 iterations) so JSC tiers it up through B3, where the faulty fold applies.
  3. Fold inverts the test B3ReduceStrength constant-folds the negated cast to the un-negated isSubRTT result, so the “cast failed” branch is taken as if it succeeded.
  4. Type-confused access Code on the taken path treats the reference as the cast-target struct type, accessing fields/layout that do not match the object’s real type.
  5. Memory corruption → ACE Field reads/writes against the wrong struct layout give an out-of-bounds / type-confused access primitive; escalation to arbitrary code execution is the advisory’s stated impact (the controlled-write chain itself is inferred, not shown by the diff).

Impact Assessment

Runs in the WebContent (renderer) process inside the JSC JIT (B3), reachable from any page that can instantiate a WebAssembly module with GC cast instructions — no elevated privilege. The diff substantiates a type-confusion miscompilation: an optimizer fold that produces the wrong boolean for negated casts, letting a failed cast be treated as successful and the reference be used at an incompatible struct type. That is a strong memory-safety primitive (type confusion between GC structs of different layout); the advisory rates it arbitrary code execution. The step from type confusion to a full RCE chain is inferred. Gating: the function must be hot enough to tier into B3 and use a negated cast (br_on_cast_fail / negated ref.test).

Changed Functions

FunctionChangeNotes
for
JSTests/wasm/gc/br_on_cast_fail.js
modified

Files Changed

  • JSTests/wasm/gc/br_on_cast_fail.js
  • Source/JavaScriptCore/b3/B3ReduceStrength.cpp

Audit Directions

  • Other `shouldNegate()`-carrying nodes
    Audit every B3/Air and DFG/FTL fold or lowering of cast/test nodes to confirm the negation flag is applied on all paths (constant-fold, identity, select lowering), not just some.
  • Wasm GC cast lowering across tiers
    Compare BBQ/OMG and B3 handling of ref.test/ref.cast/br_on_cast(_fail) for consistent polarity, especially where a subtype result is cached or reused.
  • Constant-folding of boolean predicates
    Look for !!predicate folds elsewhere that drop a modifier flag (negate/strict/invert) carried on the IR node.
diff --git a/Source/JavaScriptCore/b3/B3ReduceStrength.cpp b/Source/JavaScriptCore/b3/B3ReduceStrength.cpp
index 0395c30e8395..65201a94a1fc 100644
--- a/Source/JavaScriptCore/b3/B3ReduceStrength.cpp
+++ b/Source/JavaScriptCore/b3/B3ReduceStrength.cpp
@@ -3604,6 +3604,8 @@ private:
                 SUPPRESS_UNCOUNTED_LOCAL const Wasm::RTT* targetRTT = cast->targetRTT();
                 if (!Wasm::typeIndexIsType(static_cast<Wasm::TypeIndex>(toHeapType))) {
                     if (rtt->isSubRTT(*targetRTT)) {
+                        // shouldNegate can only be set on WasmRefTest.
+                        ASSERT(!cast->shouldNegate());
                         replaceWithIdentity(structNew);
                         break;
                     }
@@ -3669,7 +3671,8 @@ private:
                 int32_t toHeapType = cast->targetHeapType();
                 SUPPRESS_UNCOUNTED_LOCAL const Wasm::RTT* targetRTT = cast->targetRTT();
                 if (!Wasm::typeIndexIsType(static_cast<Wasm::TypeIndex>(toHeapType))) {
-                    replaceWithNewValue(m_proc.addIntConstant(m_value, !!rtt->isSubRTT(*targetRTT)));
+                    const bool isSubtype = rtt->isSubRTT(*targetRTT);
+                    replaceWithNewValue(m_proc.addIntConstant(m_value, cast->shouldNegate() ? !isSubtype : isSubtype));
                     break;
                 }
             }
diff --git a/JSTests/wasm/gc/br_on_cast_fail.js b/JSTests/wasm/gc/br_on_cast_fail.js
new file mode 100644
index 000000000000..7f764136334f
--- /dev/null
+++ b/JSTests/wasm/gc/br_on_cast_fail.js
@@ -0,0 +1,48 @@
+// (module
+//  (rec
+//    (type (;0;) (struct (field (mut i32))))
+//    (type (;1;) (struct (field (mut i32)) (field (mut i64)) (field (mut i64))))
+//  )
+//  (type (;2;) (func (result (ref struct))))
+//  (type (;3;) (func (result i32)))
+//  (export "fail" (func 0))
+//  (export "pass" (func 1))
+//  (func (;0;) (type 3) (result i32)
+//    block (type 2) (result (ref struct)) ;; label = @1
+//      i32.const 42
+//      struct.new 0
+//      br_on_cast_fail 0 (;@1;) (ref struct) (ref 1)
+//      drop
+//      i32.const 0
+//      return
+//    end
+//    drop
+//    i32.const 1
+//  )
+//  (func (;1;) (type 3) (result i32)
+//    block (type 2) (result (ref struct)) ;; label = @1
+//      i32.const 99
+//      i64.const 0
+//      i64.const 0
+//      struct.new 1
+//      br_on_cast_fail 0 (;@1;) (ref struct) (ref 1)
+//      drop
+//      i32.const 1
+//      return
+//    end
+//    drop
+//    i32.const 0
+//  )
+// )
+
+const bytes = new Uint8Array([0,97,115,109,1,0,0,0,1,28,3,78,2,79,0,95,1,127,1,79,0,95,3,127,1,126,1,126,1,96,0,1,100,107,96,0,1,127,3,3,2,3,3,7,15,2,4,102,97,105,108,0,0,4,112,97,115,115,0,1,10,54,2,23,0,2,2,65,42,251,0,0,251,25,0,0,107,1,26,65,0,15,11,26,65,1,11,28,0,2,2,65,227,0,66,0,66,0,251,0,1,251,25,0,0,107,1,26,65,1,15,11,26,65,0,11]);
+const mod = new WebAssembly.Module(bytes);
+const { pass, fail } = new WebAssembly.Instance(mod).exports;
+
+for (let i = 0; i < 200000; i++) {
+    fail();
+    pass();
+}
+
+if (fail() === 0 || pass() === 0)
+    throw new Error("br_cast_on_fail incorrect");
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/JSTests/wasm/gc/br_on_cast_fail.js b/JSTests/wasm/gc/br_on_cast_fail.js
new file mode 100644
index 000000000000..7f764136334f
--- /dev/null
+++ b/JSTests/wasm/gc/br_on_cast_fail.js
@@ -0,0 +1,48 @@
+// (module
+//  (rec
+//    (type (;0;) (struct (field (mut i32))))
+//    (type (;1;) (struct (field (mut i32)) (field (mut i64)) (field (mut i64))))
+//  )
+//  (type (;2;) (func (result (ref struct))))
+//  (type (;3;) (func (result i32)))
+//  (export "fail" (func 0))
+//  (export "pass" (func 1))
+//  (func (;0;) (type 3) (result i32)
+//    block (type 2) (result (ref struct)) ;; label = @1
+//      i32.const 42
+//      struct.new 0
+//      br_on_cast_fail 0 (;@1;) (ref struct) (ref 1)
+//      drop
+//      i32.const 0
+//      return
+//    end
+//    drop
+//    i32.const 1
+//  )
+//  (func (;1;) (type 3) (result i32)
+//    block (type 2) (result (ref struct)) ;; label = @1
+//      i32.const 99
+//      i64.const 0
+//      i64.const 0
+//      struct.new 1
+//      br_on_cast_fail 0 (;@1;) (ref struct) (ref 1)
+//      drop
+//      i32.const 1
+//      return
+//    end
+//    drop
+//    i32.const 0
+//  )
+// )
+
+const bytes = new Uint8Array([0,97,115,109,1,0,0,0,1,28,3,78,2,79,0,95,1,127,1,79,0,95,3,127,1,126,1,126,1,96,0,1,100,107,96,0,1,127,3,3,2,3,3,7,15,2,4,102,97,105,108,0,0,4,112,97,115,115,0,1,10,54,2,23,0,2,2,65,42,251,0,0,251,25,0,0,107,1,26,65,0,15,11,26,65,1,11,28,0,2,2,65,227,0,66,0,66,0,251,0,1,251,25,0,0,107,1,26,65,1,15,11,26,65,0,11]);
+const mod = new WebAssembly.Module(bytes);
+const { pass, fail } = new WebAssembly.Instance(mod).exports;
+
+for (let i = 0; i < 200000; i++) {
+    fail();
+    pass();
+}
+
+if (fail() === 0 || pass() === 0)
+    throw new Error("br_cast_on_fail incorrect");
Loading diff…