CVE-2025-31206
Overview
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.
Attack Path
- 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.
- 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.
- 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.
- Escalate the type confusion Use the confused pointer as a primitive to build fake objects / arbitrary read-write within the WebContent process.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
forJSTests/stress/dfg-get-global-object-should-use-object-edge.js |
modified |
Files Changed
JSTests/stress/dfg-get-global-object-should-use-object-edge.jsSource/JavaScriptCore/dfg/DFGFixupPhase.cppSource/JavaScriptCore/dfg/DFGSpeculativeJIT.cppSource/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
Audit Directions
- Same file: other cell-vs-object nodesGrep 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 loadsIn DFGSpeculativeJIT/FTL, audit emitLoadStructure / loadStructure call sites for a preceding speculateObject/lowObject; a lowCell feeding loadStructure is a red flag.
- Scope/global accessorsReview GetScope, SkipScope, UnwrapGlobalProxy and similar for correct use-kind assumptions about object vs proxy vs cell operands.
Patch
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 });
+}
Regression Test / PoC
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 });
+}