Medium CVSS 4.3 webkit Type Confusion 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected Safari crash
ComponentJSC
Sub-componentJSC DFG
Bug ClassType Confusion
Tracker290834
Fix commit2a545562709a (WebKit/WebKit) +28/-6
CWECWE-843 (Type confusion)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L
CISA KEVNot listed
CreditedYuhao Hu, Yan Kang, Chenggang Wu, Xiaojie Wei
Disclosed2025-05-12

Background

DFG/FTL
JavaScriptCore’s optimizing JIT tiers that speculate on value types; wrong speculation must trigger an OSR exit rather than execute unsafe code.
Use kind / edge
A DFG edge’s use kind (KnownCellUse, ObjectUse, …) tells the backend what to assume and check about an operand; too weak a use kind omits a needed check.
Structure
JSC’s per-object shape descriptor; only JSObjects have one, and GetGlobalObject reads the global object pointer stored in it.
Type confusion
Treating memory of one type as another; here a non-object cell is treated as a JSObject with a Structure.

Root Cause Analysis

This fixes a type confusion in JavaScriptCore’s DFG/FTL compilation of the GetGlobalObject node. In DFGFixupPhase, GetGlobalObject was grouped with SkipScope/GetScope/GetGetter/GetSetter and given fixEdge<KnownCellUse> on its operand — a use kind that only guarantees the operand is a cell, not that it is a JSObject. compileGetGlobalObject then did emitLoadStructure(vm, object, result) and loadPtr(Address(result, Structure::globalObjectOffset())), i.e. it read the operand’s Structure and pulled the globalObject pointer out of it, which is only meaningful for a JSObject. Because only KnownCellUse was enforced, a non-object cell (for example a string or symbol) could flow into GetGlobalObject; loading a ‘structure’ from such a cell and dereferencing Structure::globalObjectOffset() reinterprets unrelated memory as a Structure and yields an attacker-influenced pointer — classic type confusion.

The fix splits GetGlobalObject out of the KnownCellUse group and gives it fixEdge<ObjectUse>, adds an explicit speculateObject(node->child1(), objectGPR) in the DFG backend before loading the structure, and changes the FTL lowering from loadStructure(lowCell(…)) to loadStructure(lowObject(…)).

The restored invariant is that GetGlobalObject only ever operates on a proven JSObject; a non-object operand now triggers a speculation failure / OSR exit instead of being treated as an object. The regression test opt() calls arg.test with an object then a regexp so the compiled GetGlobalObject sees a non-object shape, exercising the guard.

Key insight
GetGlobalObject was validated only as a cell (KnownCellUse) when it actually requires a JSObject; the missing ObjectUse speculation let a non-object cell be treated as an object and its ‘structure’ dereferenced.

Attack Path

  1. Warm up the JIT Repeatedly call a function so a GetGlobalObject node is compiled in DFG/FTL for a receiver the profiler believes is a cell.
  2. Feed a non-object cell Arrange for the operand to be a non-object cell (e.g. a string) that satisfies KnownCellUse but is not a JSObject.
  3. Force the structure load GetGlobalObject loads a ‘Structure’ from the non-object cell and reads Structure::globalObjectOffset(), reinterpreting controlled bytes as a Structure/global pointer.
  4. Escalate the type confusion Use the confused pointer as a primitive to build fake objects / arbitrary read-write within the WebContent process.

Impact Assessment

A JIT type-confusion primitive in the WebContent process: reading a Structure and its globalObject field from an attacker-chosen non-object cell yields a controlled pointer, the usual starting point for fake-object construction and arbitrary read/write in JSC. Although the advisory rates it a crash, type confusions in the DFG/FTL are historically reliable routes to full WebContent code execution.

Changed Functions

FunctionChangeNotes
for
JSTests/stress/dfg-get-global-object-should-use-object-edge.js
modified

Files Changed

  • JSTests/stress/dfg-get-global-object-should-use-object-edge.js
  • Source/JavaScriptCore/dfg/DFGFixupPhase.cpp
  • Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
  • Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp

Audit Directions

  • Same file: other cell-vs-object nodes
    Grep DFGFixupPhase.cpp for nodes grouped under KnownCellUse that later load a Structure or object field (emitLoadStructure, Structure::*Offset); confirm each truly accepts any cell, not just objects.
  • Backend structure loads
    In DFGSpeculativeJIT/FTL, audit emitLoadStructure / loadStructure call sites for a preceding speculateObject/lowObject; a lowCell feeding loadStructure is a red flag.
  • Scope/global accessors
    Review GetScope, SkipScope, UnwrapGlobalProxy and similar for correct use-kind assumptions about object vs proxy vs cell operands.
diff --git a/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp b/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp
index f394063c4549..a13dcaf41580 100644
--- a/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp
@@ -2115,12 +2115,16 @@ private:
         case SkipScope:
         case GetScope:
         case GetGetter:
-        case GetSetter:
-        case GetGlobalObject: {
+        case GetSetter: {
             fixEdge<KnownCellUse>(node->child1());
             break;
         }
 
+        case GetGlobalObject: {
+            fixEdge<ObjectUse>(node->child1());
+            break;
+        }
+
         case UnwrapGlobalProxy: {
             fixEdge<GlobalProxyUse>(node->child1());
             break;
diff --git a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
index e9b92d24abe5..8080a2ec33e9 100644
--- a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
+++ b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
@@ -7966,9 +7966,15 @@ void SpeculativeJIT::compileGetGlobalObject(Node* node)
 {
     SpeculateCellOperand object(this, node->child1());
     GPRTemporary result(this);
-    emitLoadStructure(vm(), object.gpr(), result.gpr());
-    loadPtr(Address(result.gpr(), Structure::globalObjectOffset()), result.gpr());
-    cellResult(result.gpr(), node);
+
+    GPRReg objectGPR = object.gpr();
+    GPRReg resultGPR = result.gpr();
+
+    speculateObject(node->child1(), objectGPR);
+
+    emitLoadStructure(vm(), objectGPR, resultGPR);
+    loadPtr(Address(resultGPR, Structure::globalObjectOffset()), resultGPR);
+    cellResult(resultGPR, node);
 }
 
 void SpeculativeJIT::compileGetGlobalThis(Node* node)
diff --git a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
index 2899e69ebb78..a4db4022121c 100644
--- a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
+++ b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
@@ -11749,7 +11749,7 @@ IGNORE_CLANG_WARNINGS_END
 
     void compileGetGlobalObject()
     {
-        LValue structure = loadStructure(lowCell(m_node->child1()));
+        LValue structure = loadStructure(lowObject(m_node->child1()));
         setJSValue(m_out.loadPtr(structure, m_heaps.Structure_globalObject));
     }
 
diff --git a/JSTests/stress/dfg-get-global-object-should-use-object-edge.js b/JSTests/stress/dfg-get-global-object-should-use-object-edge.js
new file mode 100644
index 000000000000..70262b4304ce
--- /dev/null
+++ b/JSTests/stress/dfg-get-global-object-should-use-object-edge.js
@@ -0,0 +1,12 @@
+//@ runDefault("--validateOptions=true", "--thresholdForJITSoon=10", "--thresholdForJITAfterWarmUp=10", "--thresholdForOptimizeAfterWarmUp=100", "--thresholdForOptimizeAfterLongWarmUp=100", "--thresholdForOptimizeSoon=100", "--thresholdForFTLOptimizeAfterWarmUp=1000", "--thresholdForFTLOptimizeSoon=1000", "--validateBCE=true", "--useConcurrentJIT=0")
+
+function opt(arg) {
+    try {
+        arg.test({ test: "padEnd" });
+    } catch (e) { }
+    arg.test(/123/);
+}
+
+for (let i = 0; i < 50; i++) {
+    opt({ test: opt });
+}
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/JSTests/stress/dfg-get-global-object-should-use-object-edge.js b/JSTests/stress/dfg-get-global-object-should-use-object-edge.js
new file mode 100644
index 000000000000..70262b4304ce
--- /dev/null
+++ b/JSTests/stress/dfg-get-global-object-should-use-object-edge.js
@@ -0,0 +1,12 @@
+//@ runDefault("--validateOptions=true", "--thresholdForJITSoon=10", "--thresholdForJITAfterWarmUp=10", "--thresholdForOptimizeAfterWarmUp=100", "--thresholdForOptimizeAfterLongWarmUp=100", "--thresholdForOptimizeSoon=100", "--thresholdForFTLOptimizeAfterWarmUp=1000", "--thresholdForFTLOptimizeSoon=1000", "--validateBCE=true", "--useConcurrentJIT=0")
+
+function opt(arg) {
+    try {
+        arg.test({ test: "padEnd" });
+    } catch (e) { }
+    arg.test(/123/);
+}
+
+for (let i = 0; i < 50; i++) {
+    opt({ test: opt });
+}
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker.