CVE-2025-31206
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.cppSource/JavaScriptCore/dfg/DFGSpeculativeJIT.cppSource/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 });
+}