Released May 12, 2025

CVE-2025-31206

CVSS 4.3 severity medium CWE-843 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L

Overview

Impact:

Description: Processing maliciously crafted web content may lead to an unexpected Safari crash

Researchers: Yuhao Hu, Yan Kang, Chenggang Wu, Xiaojie Wei

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.

Patch Details

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 });
+}
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));
     }
 

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2025-31206
Bugzilla 290834
Component JSC
Bug Class TypeConfusion
Severity medium
Commit 2a545562709ac7a6…
Advisory Apple Advisory

Terminology

  • 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.

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.

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.

Changed Functions

Function File Change Note
fixupNode (GetGlobalObject case) Source/JavaScriptCore/dfg/DFGFixupPhase.cpp modified Moves GetGlobalObject out of the KnownCellUse group and fixes its child edge as ObjectUse so a non-object operand is rejected.
SpeculativeJIT::compileGetGlobalObject Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp modified Adds speculateObject(node->child1(), objectGPR) before emitLoadStructure so the operand is proven to be an object at runtime.
FTL compileGetGlobalObject Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp modified Changes loadStructure(lowCell(…)) to loadStructure(lowObject(…)), enforcing the object type in the FTL tier.

Files Changed

Source Files

  • Source/JavaScriptCore/dfg/DFGFixupPhase.cpp
  • Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
  • Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp

Test Files

  • JSTests/stress/dfg-get-global-object-should-use-object-edge.js

Regression Test

The fix ships this proof-of-concept / regression test:

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 });
+}