CVE-2024-23222
Overview
Background
- Concurrent DFG/FTL JIT
- JavaScriptCore compiles hot code on a background thread while the mutator keeps running and can mutate objects the compiler is reasoning about.
- Structure / structure transition
- A JSObject’s shape; adding/changing properties transitions it S1->S2->S3, each a distinct Structure.
- CheckStructure + constant folding
- The JIT guards a property load with CheckStructure and may replace the load with the constant value it observed, assuming the guard makes that value safe.
- Transition / property-replacement watchpoint
- Watchpoints that fire when a watched structure transitions or a property is replaced, invalidating compiled code; an unwatched intermediate structure escapes them.
Root Cause Analysis
This fixes a concurrent-compiler type confusion in JavaScriptCore’s DFG constant-property folding — the bug behind an exploited-in-the-wild attack (bug 267134), shipped as two commits (64714692 adds the safety check; 66f60dea simplifies it). DFG/FTL compile on a background thread while the mutator keeps running. Graph::tryGetConstantProperty tries to constant-fold a property load guarded by a CheckStructure, e.g. CheckStructure O, S1|S3; GetByOffset O, offset. Pre-patch it simply took the object’s cellLock, read object->structure(), checked structureSet.contains(structure), and returned object->getDirectConcurrently(cellLock, structure, offset) — i.e. it folded whatever value the property held at the moment the background compiler observed it. The problem: with a live structure transition S1 -> S2 -> S3, the compiler can observe the object at an intermediate structure and fold the property value seen there, while WebKit only registered transition watchpoints for the structures in the CheckStructure set (S1 and S3), not the intermediate S2. An attacker mutating the object concurrently can make the compiler bake in the S2-era value, then transition O to S3 before the code runs; CheckStructure(S1|S3) then passes for S3, but the code uses the stale value folded from S2 — a type confusion (the JIT treats attacker-chosen bytes as a trusted constant, e.g. a pointer).
The fix reworks tryGetConstantProperty: it reads the value under the cell lock, then only keeps the constant fold if EITHER every structure in the CheckStructure set is watched (structure->dfgShouldWatch() for all — any transition then invalidates the compilation), OR the set contains exactly one structure (structureSet.size()==1), because with a single required structure there is no way to change the property while keeping that structure without firing the property-replacement watchpoint that invalidates the code; otherwise it bails (returns JSValue()). The first commit additionally recorded the object/offset/value/structure in a new DesiredObjectProperties and re-validated it on the main thread at end of compilation; the second commit removes DesiredObjectProperties, since the single-structure case is already covered by the property-replacement watchpoint.
The restored invariant is that the DFG only constant-folds a property when a watchpoint guarantees the folded value cannot go stale before the code runs.
Attack Path
- Set up a foldable property load Run JS with a hot property access on an object so the DFG/FTL compiles a CheckStructure + GetByOffset it wants to constant-fold.
- Race the background compiler While compilation runs on another thread, drive the object through a structure transition chain (S1 -> S2 -> S3) so the compiler observes and folds the property value at an intermediate structure S2 that is not watched.
- Reach a checked structure Finish transitioning the object to S3 (a member of the CheckStructure set) before the compiled code executes.
- Use the stale value CheckStructure(S1|S3) passes for S3, but the code uses the constant folded from S2 — a type confusion where attacker-controlled bytes are treated as a trusted constant.
- Escalate to code execution Leverage the confused value (e.g. a fake object/pointer) into arbitrary read/write and code execution in the WebContent process.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
forSource/JavaScriptCore/dfg/DFGDesiredObjectProperties.cpp |
modified | |
CodeBlockSource/JavaScriptCore/dfg/DFGDesiredObjectProperties.h |
modified | |
VMSource/JavaScriptCore/dfg/DFGDesiredObjectProperties.h |
modified | |
CommonDataSource/JavaScriptCore/dfg/DFGDesiredObjectProperties.h |
modified | |
WatchpointCollectorSource/JavaScriptCore/dfg/DFGDesiredObjectProperties.h |
modified | |
DesiredObjectPropertiesSource/JavaScriptCore/dfg/DFGDesiredObjectProperties.h |
modified |
Files Changed
Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxprojSource/JavaScriptCore/Sources.txtSource/JavaScriptCore/dfg/DFGDesiredObjectProperties.cppSource/JavaScriptCore/dfg/DFGDesiredObjectProperties.hSource/JavaScriptCore/dfg/DFGGraph.cppSource/JavaScriptCore/dfg/DFGPlan.cppSource/JavaScriptCore/dfg/DFGPlan.h
Audit Directions
- Same function: concurrent readsAudit tryGetConstantProperty and other getDirectConcurrently / concurrent object reads in DFGGraph for folding values without proving every relevant structure is watched or uniquely constrained.
- Watchpoint coverage vs CheckStructure setsReview DFG/FTL optimizations that assume a CheckStructure set makes an observed value safe; confirm intermediate/transitional structures are watched (dfgShouldWatch) or the set is size 1.
- Concurrent-compiler invariantsGrep for cellLock()/getDirectConcurrently and main-thread revalidation (isStillValidOnMainThread) to find places relying on background-observed state that the mutator can change before execution.
Patch
diff --git a/Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj b/Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj
index 452ea0632129..23b5a9afa800 100644
--- a/Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj
+++ b/Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj
@@ -1970,8 +1970,6 @@
E33F50811B8429A400413856 /* JSInternalPromise.h in Headers */ = {isa = PBXBuildFile; fileRef = E33F507F1B8429A400413856 /* JSInternalPromise.h */; settings = {ATTRIBUTES = (Private, ); }; };
E3400EC122A1CC7B009DED54 /* FunctionExecutableInlines.h in Headers */ = {isa = PBXBuildFile; fileRef = E3400EC022A1CC78009DED54 /* FunctionExecutableInlines.h */; };
E3443E3128E9711700C0020C /* ArrayPrototypeInlines.h in Headers */ = {isa = PBXBuildFile; fileRef = E3443E3028E9711700C0020C /* ArrayPrototypeInlines.h */; settings = {ATTRIBUTES = (Private, ); }; };
- E34565A72B4753D800ED9B78 /* DFGDesiredObjectProperties.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E34565A42B4753D700ED9B78 /* DFGDesiredObjectProperties.cpp */; };
- E34565A82B4753D800ED9B78 /* DFGDesiredObjectProperties.h in Headers */ = {isa = PBXBuildFile; fileRef = E34565A52B4753D800ED9B78 /* DFGDesiredObjectProperties.h */; };
E348876228F516C600527E5E /* ObjectPrototypeInlines.h in Headers */ = {isa = PBXBuildFile; fileRef = E348876128F516C600527E5E /* ObjectPrototypeInlines.h */; };
E349A7812491F161001BA336 /* DFGCodeOriginPool.h in Headers */ = {isa = PBXBuildFile; fileRef = E349A7802491F15A001BA336 /* DFGCodeOriginPool.h */; settings = {ATTRIBUTES = (Private, ); }; };
E34D4FAA2984F25B00CE5F9F /* WasmCallsiteCollection.h in Headers */ = {isa = PBXBuildFile; fileRef = E34D4FA92984F25A00CE5F9F /* WasmCallsiteCollection.h */; settings = {ATTRIBUTES = (Private, ); }; };
@@ -5624,8 +5622,6 @@
E33F50881B844A1A00413856 /* InternalPromiseConstructor.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = InternalPromiseConstructor.js; sourceTree = "<group>"; };
E3400EC022A1CC78009DED54 /* FunctionExecutableInlines.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FunctionExecutableInlines.h; sourceTree = "<group>"; };
E3443E3028E9711700C0020C /* ArrayPrototypeInlines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ArrayPrototypeInlines.h; sourceTree = "<group>"; };
- E34565A42B4753D700ED9B78 /* DFGDesiredObjectProperties.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = DFGDesiredObjectProperties.cpp; path = dfg/DFGDesiredObjectProperties.cpp; sourceTree = "<group>"; };
- E34565A52B4753D800ED9B78 /* DFGDesiredObjectProperties.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DFGDesiredObjectProperties.h; path = dfg/DFGDesiredObjectProperties.h; sourceTree = "<group>"; };
E348876128F516C600527E5E /* ObjectPrototypeInlines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ObjectPrototypeInlines.h; sourceTree = "<group>"; };
E349A77F2491F159001BA336 /* DFGCodeOriginPool.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = DFGCodeOriginPool.cpp; path = dfg/DFGCodeOriginPool.cpp; sourceTree = "<group>"; };
E349A7802491F15A001BA336 /* DFGCodeOriginPool.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = DFGCodeOriginPool.h; path = dfg/DFGCodeOriginPool.h; sourceTree = "<group>"; };
@@ -9034,8 +9030,6 @@
E3BFA5CD21E853A1009C0EBA /* DFGDesiredGlobalProperty.h */,
0F8F2B97172F04FD007DBDA5 /* DFGDesiredIdentifiers.cpp */,
0F8F2B98172F04FD007DBDA5 /* DFGDesiredIdentifiers.h */,
- E34565A42B4753D700ED9B78 /* DFGDesiredObjectProperties.cpp */,
- E34565A52B4753D800ED9B78 /* DFGDesiredObjectProperties.h */,
C2C0F7CB17BBFC5B00464FE4 /* DFGDesiredTransitions.cpp */,
C2C0F7CC17BBFC5B00464FE4 /* DFGDesiredTransitions.h */,
0FE853491723CDA500B618F5 /* DFGDesiredWatchpoints.cpp */,
@@ -10630,7 +10624,6 @@
0F2FC77316E12F740038D976 /* DFGDCEPhase.h in Headers */,
E3BFA5D021E853A1009C0EBA /* DFGDesiredGlobalProperty.h in Headers */,
0F8F2B9A172F0501007DBDA5 /* DFGDesiredIdentifiers.h in Headers */,
- E34565A82B4753D800ED9B78 /* DFGDesiredObjectProperties.h in Headers */,
C2C0F7CE17BBFC5B00464FE4 /* DFGDesiredTransitions.h in Headers */,
0FE8534C1723CDA500B618F5 /* DFGDesiredWatchpoints.h in Headers */,
C2981FD917BAEE4B00A3BC98 /* DFGDesiredWeakReferences.h in Headers */,
@@ -13155,7 +13148,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- E34565A72B4753D800ED9B78 /* DFGDesiredObjectProperties.cpp in Sources */,
52CD0F682242F71C004A18A5 /* testdfg.cpp in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
diff --git a/Source/JavaScriptCore/Sources.txt b/Source/JavaScriptCore/Sources.txt
index 8c27317e7292..961adec4a7e4 100644
--- a/Source/JavaScriptCore/Sources.txt
+++ b/Source/JavaScriptCore/Sources.txt
@@ -353,7 +353,6 @@ dfg/DFGCriticalEdgeBreakingPhase.cpp
dfg/DFGDCEPhase.cpp
dfg/DFGDesiredGlobalProperties.cpp
dfg/DFGDesiredIdentifiers.cpp
-dfg/DFGDesiredObjectProperties.cpp
dfg/DFGDesiredTransitions.cpp
dfg/DFGDesiredWatchpoints.cpp
dfg/DFGDesiredWeakReferences.cpp
diff --git a/Source/JavaScriptCore/dfg/DFGDesiredObjectProperties.cpp b/Source/JavaScriptCore/dfg/DFGDesiredObjectProperties.cpp
deleted file mode 100644
index 294cc24fb710..000000000000
--- a/Source/JavaScriptCore/dfg/DFGDesiredObjectProperties.cpp
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2023 Apple Inc. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
- * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
- * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
- * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
- * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include "config.h"
-#include "DFGDesiredObjectProperties.h"
-
-#if ENABLE(DFG_JIT)
-
-#include "JSObjectInlines.h"
-
-namespace JSC { namespace DFG {
-
-bool DesiredObjectProperties::addLazily(JSObject* object, PropertyOffset offset, JSValue value, Structure* structure)
-{
- auto result = m_properties.add(std::tuple { object, offset }, std::tuple { value, structure });
- if (result.isNewEntry)
- return true;
- auto [expectedValue, expectedStructure] = result.iterator->value;
- return expectedValue == value && expectedStructure == structure;
-}
-
-// Ultimately, we can use the code when
-// 1. the main thread's object is holding an expected structure.
-// 2. the main thread's object's property is expected one.
-bool DesiredObjectProperties::areStillValidOnMainThread(VM&)
-{
- for (auto& [key, values] : m_properties) {
- auto [object, offset] = key;
- auto [expectedValue, expectedStructure] = values;
-
- Structure* structure = object->structure();
-
- if (UNLIKELY(structure != expectedStructure))
- return false;
-
- if (UNLIKELY(!structure->isValidOffset(offset)))
- return false;
-
- JSValue value = object->getDirect(offset);
- if (UNLIKELY(value != expectedValue))
- return false;
- }
- return true;
-}
-
-} } // namespace JSC::DFG
-
-#endif // ENABLE(DFG_JIT)
-
diff --git a/Source/JavaScriptCore/dfg/DFGDesiredObjectProperties.h b/Source/JavaScriptCore/dfg/DFGDesiredObjectProperties.h
deleted file mode 100644
index 1c4c4f5fe4a6..000000000000
--- a/Source/JavaScriptCore/dfg/DFGDesiredObjectProperties.h
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2023 Apple Inc. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
- * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
- * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
- * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
- * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-#pragma once
-
-#if ENABLE(DFG_JIT)
-
-#include "DFGFrozenValue.h"
-#include "DFGStructureAbstractValue.h"
-#include <wtf/HashMap.h>
-
-namespace JSC {
-
-class CodeBlock;
-class VM;
-
-namespace DFG {
-
-class CommonData;
-class WatchpointCollector;
-
-class DesiredObjectProperties {
-public:
- bool addLazily(JSObject*, PropertyOffset, JSValue, Structure*);
- bool areStillValidOnMainThread(VM&);
-
-private:
- HashMap<std::tuple<JSObject*, PropertyOffset>, std::tuple<JSValue, Structure*>> m_properties;
-};
-
-} } // namespace JSC::DFG
-
-#endif // ENABLE(DFG_JIT)
diff --git a/Source/JavaScriptCore/dfg/DFGGraph.cpp b/Source/JavaScriptCore/dfg/DFGGraph.cpp
index 9fc513a86749..5a400ad64d2f 100644
--- a/Source/JavaScriptCore/dfg/DFGGraph.cpp
+++ b/Source/JavaScriptCore/dfg/DFGGraph.cpp
@@ -1354,8 +1354,8 @@ JSValue Graph::tryGetConstantProperty(
if (!result)
return JSValue();
- // If all structures are watched, we don't need to check them in the main thread because they already registered watchpoints
- // for transitions. If the object gets transition while compiling, then it invalidates the code.
+ // If all structures are watched, we don't need to consider whether object transitions and changes the value.
+ // If the object gets transition while compiling, then it invalidates the code.
bool allAreWatched = true;
for (unsigned i = structureSet.size(); i--;) {
RegisteredStructure structure = structureSet[i];
@@ -1373,11 +1373,6 @@ JSValue Graph::tryGetConstantProperty(
if (structureSet.size() != 1)
return JSValue();
- m_plan.weakReferences().addLazily(object);
- m_plan.weakReferences().addLazily(result);
- if (!m_plan.objectProperties().addLazily(object, offset, result, set.onlyStructure()))
- return JSValue();
-
return result;
}
diff --git a/Source/JavaScriptCore/dfg/DFGPlan.cpp b/Source/JavaScriptCore/dfg/DFGPlan.cpp
index 2eff8f0ac451..7ef80267ead2 100644
--- a/Source/JavaScriptCore/dfg/DFGPlan.cpp
+++ b/Source/JavaScriptCore/dfg/DFGPlan.cpp
@@ -183,7 +183,6 @@ void Plan::cancel()
m_identifiers = DesiredIdentifiers();
m_weakReferences = DesiredWeakReferences();
m_transitions = DesiredTransitions();
- m_objectProperties = DesiredObjectProperties();
m_callback = nullptr;
}
@@ -543,11 +542,7 @@ void Plan::reallyAdd(CommonData* commonData)
bool Plan::isStillValidOnMainThread()
{
- if (!m_watchpoints.areStillValidOnMainThread(*m_vm, m_identifiers))
- return false;
- if (!m_objectProperties.areStillValidOnMainThread(*m_vm))
- return false;
- return true;
+ return m_watchpoints.areStillValidOnMainThread(*m_vm, m_identifiers);
}
CompilationResult Plan::finalize()
diff --git a/Source/JavaScriptCore/dfg/DFGPlan.h b/Source/JavaScriptCore/dfg/DFGPlan.h
index e502542e4d25..e65910de072d 100644
--- a/Source/JavaScriptCore/dfg/DFGPlan.h
+++ b/Source/JavaScriptCore/dfg/DFGPlan.h
@@ -28,7 +28,6 @@
#include "CompilationResult.h"
#include "DFGDesiredGlobalProperties.h"
#include "DFGDesiredIdentifiers.h"
-#include "DFGDesiredObjectProperties.h"
#include "DFGDesiredTransitions.h"
#include "DFGDesiredWatchpoints.h"
#include "DFGDesiredWeakReferences.h"
@@ -93,7 +92,6 @@ public:
DesiredIdentifiers& identifiers() { return m_identifiers; }
DesiredWeakReferences& weakReferences() { return m_weakReferences; }
DesiredTransitions& transitions() { return m_transitions; }
- DesiredObjectProperties& objectProperties() { return m_objectProperties; }
RecordedStatuses& recordedStatuses() { return m_recordedStatuses; }
bool willTryToTierUp() const { return m_willTryToTierUp; }
@@ -134,7 +132,6 @@ private:
DesiredIdentifiers m_identifiers;
DesiredWeakReferences m_weakReferences;
DesiredTransitions m_transitions;
- DesiredObjectProperties m_objectProperties;
RecordedStatuses m_recordedStatuses;
HashMap<BytecodeIndex, FixedVector<BytecodeIndex>> m_tierUpInLoopHierarchy;