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 |
|---|---|---|
Graph::tryGetConstantPropertySource/JavaScriptCore/dfg/DFGGraph.cpp |
modified | Only keeps a constant-folded property if all CheckStructure structures are watched (dfgShouldWatch) or the set has exactly one structure; otherwise bails, closing the concurrent-transition stale-value fold. |
DesiredObjectProperties (added then removed)Source/JavaScriptCore/dfg/DFGDesiredObjectProperties.h |
added | Commit 64714692 added it to record JSObject*/offset/value/structure and re-check on the main thread (areStillValidOnMainThread); commit 66f60dea removes it since the single-structure case is covered by the property-replacement watchpoint. |
Plan::isStillValidOnMainThread / cancelSource/JavaScriptCore/dfg/DFGPlan.cpp |
modified | Wires the end-of-compilation main-thread validity check (and its teardown) for the recorded object properties in the first commit. |
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/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index 399d9430452c..3a1a74aa194a 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,15 @@
+2020-09-15 Zalan Bujtas <zalan@apple.com>
+
+ [LFC][FFC] 'display: flex' generates a flex container box that is block-level when placed in flow layout
+ https://bugs.webkit.org/show_bug.cgi?id=216596
+
+ Reviewed by Simon Fraser.
+
+ See https://www.w3.org/TR/css-flexbox-1/#flex-containers.
+
+ * layout/layouttree/LayoutBox.cpp:
+ (WebCore::Layout::Box::isBlockLevelBox const):
+
2020-09-15 Zalan Bujtas <zalan@apple.com>
[LFC] Use isBlockBox() in Box::establishesBlockFormattingContext
diff --git a/Source/WebCore/layout/layouttree/LayoutBox.cpp b/Source/WebCore/layout/layouttree/LayoutBox.cpp
index ae3b1a4e1369..6f2f203834cf 100644
--- a/Source/WebCore/layout/layouttree/LayoutBox.cpp
+++ b/Source/WebCore/layout/layouttree/LayoutBox.cpp
@@ -295,7 +295,7 @@ bool Box::isBlockLevelBox() const
{
// Block level elements generate block level boxes.
auto display = m_style.display();
- return display == DisplayType::Block || display == DisplayType::ListItem || display == DisplayType::Table;
+ return display == DisplayType::Block || display == DisplayType::ListItem || display == DisplayType::Table || display == DisplayType::Flex;
}
bool Box::isBlockBox() const