Critical CVSS 8.8 webkit Type Confusion CISA KEV 🔧 Commit mapped

Overview

Critical
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to arbitrary code execution. Apple is aware of a report that this issue may have been exploited.
ComponentJSC
Sub-componentJSC DFG
Bug ClassType Confusion
Tracker267134
Fix commit66f60deae730 (WebKit/WebKit) +3/-151
CWECWE-843 (Type confusion)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVListed
CreditedAn anonymous researcher
Disclosed2024-01-22

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.

Key insight
DFG constant-folding trusted whatever property value the concurrent compiler observed, but only structures in the CheckStructure set were watched — a value folded from an unwatched intermediate structure during a live transition stayed valid after the object reached a checked structure. The fix folds only when a watchpoint guarantees the value cannot go stale.

Attack Path

  1. 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.
  2. 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.
  3. Reach a checked structure Finish transitioning the object to S3 (a member of the CheckStructure set) before the compiled code executes.
  4. 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.
  5. 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

A critical, exploited-in-the-wild type confusion in the WebContent process: by racing the background compiler against a structure transition, an attacker makes the JIT bake a stale/attacker-chosen value in as a trusted constant, then passes CheckStructure with a different structure. This yields a controllable type-confusion primitive — treating attacker bytes as a pointer/constant — a classic and reliable route to arbitrary read/write and remote code execution in WebContent.

Changed Functions

FunctionChangeNotes
for
Source/JavaScriptCore/dfg/DFGDesiredObjectProperties.cpp
modified
CodeBlock
Source/JavaScriptCore/dfg/DFGDesiredObjectProperties.h
modified
VM
Source/JavaScriptCore/dfg/DFGDesiredObjectProperties.h
modified
CommonData
Source/JavaScriptCore/dfg/DFGDesiredObjectProperties.h
modified
WatchpointCollector
Source/JavaScriptCore/dfg/DFGDesiredObjectProperties.h
modified
DesiredObjectProperties
Source/JavaScriptCore/dfg/DFGDesiredObjectProperties.h
modified

Files Changed

  • Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj
  • Source/JavaScriptCore/Sources.txt
  • Source/JavaScriptCore/dfg/DFGDesiredObjectProperties.cpp
  • Source/JavaScriptCore/dfg/DFGDesiredObjectProperties.h
  • Source/JavaScriptCore/dfg/DFGGraph.cpp
  • Source/JavaScriptCore/dfg/DFGPlan.cpp
  • Source/JavaScriptCore/dfg/DFGPlan.h

Audit Directions

  • Same function: concurrent reads
    Audit 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 sets
    Review 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 invariants
    Grep for cellLock()/getDirectConcurrently and main-thread revalidation (isStillValidOnMainThread) to find places relying on background-observed state that the mutator can change before execution.
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;
Loading diff…

Original Bug Report

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