CVE-2024-40782
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Maksymilian Motyl
Key insight. Ruby render-tree building assumed a RubyBlock’s first child is the Ruby box, but continuations insert anonymous wrappers, so it returned the wrong renderer; locating the child by its actual Ruby display type fixes it.
Patch Details
diff --git a/LayoutTests/fast/ruby/ruby-block-continuation-crash-expected.txt b/LayoutTests/fast/ruby/ruby-block-continuation-crash-expected.txt
new file mode 100644
index 000000000000..f85a15505104
--- /dev/null
+++ b/LayoutTests/fast/ruby/ruby-block-continuation-crash-expected.txt
@@ -0,0 +1,3 @@
+base with
+forced
+line break annotation This test passes if it doesn't crash.
diff --git a/LayoutTests/fast/ruby/ruby-block-continuation-crash.html b/LayoutTests/fast/ruby/ruby-block-continuation-crash.html
new file mode 100644
index 000000000000..3f762d4236ea
--- /dev/null
+++ b/LayoutTests/fast/ruby/ruby-block-continuation-crash.html
@@ -0,0 +1,9 @@
+<script>
+if (window.testRunner)
+ testRunner.dumpAsText();
+</script>
+<ruby style="position: absolute">
+ <rb><span>base with <div>forced</div> line break</span></rb>
+ <rt>annotation</rt>
+</ruby>
+This test passes if it doesn't crash.
diff --git a/Source/WebCore/rendering/updating/RenderTreeBuilderRuby.cpp b/Source/WebCore/rendering/updating/RenderTreeBuilderRuby.cpp
index 9259b2330dd4..fde1f46e67d7 100644
--- a/Source/WebCore/rendering/updating/RenderTreeBuilderRuby.cpp
+++ b/Source/WebCore/rendering/updating/RenderTreeBuilderRuby.cpp
@@ -65,10 +65,17 @@ RenderElement& RenderTreeBuilder::Ruby::findOrCreateParentForStyleBasedRubyChild
if (!child.isRenderText() && child.style().display() == DisplayType::Ruby && parent.style().display() == DisplayType::RubyBlock)
return parent;
- if (parent.style().display() == DisplayType::RubyBlock && parent.firstChild()) {
+ if (parent.style().display() == DisplayType::RubyBlock) {
// See if we have an anonymous ruby box already.
- ASSERT(parent.firstChild()->style().display() == DisplayType::Ruby);
- return downcast<RenderElement>(*parent.firstChild());
+ // FIXME: It should be the immediate child but continuations can break this assumption.
+ for (CheckedPtr first = parent.firstChild(); first; first = first->firstChildSlow()) {
+ if (!first->isAnonymous()) {
+ ASSERT_NOT_REACHED();
+ break;
+ }
+ if (first->style().display() == DisplayType::Ruby)
+ return downcast<RenderElement>(*first);
+ }
}
if (parent.style().display() != DisplayType::Ruby) {
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2024-40782 |
| Bugzilla | 268770 |
| Component | WebKit |
| Bug Class | LogicError |
| Severity | medium |
| Commit | c2f9092d3a8e7bf6… |
| Advisory | Apple Advisory |
Terminology
- Ruby rendering — CSS ruby lays out annotation text over base text; WebKit builds dedicated RubyBlock/Ruby render boxes.
- Render-tree continuation — A block element inside inline content splits the inline into anonymous continuation renderers, changing the expected child structure.
- downcast<> — An unchecked static cast to a render subtype; casting the wrong renderer type corrupts subsequent operations.
- Anonymous renderer — An engine-generated wrapper box with no DOM node, inserted to satisfy render-tree invariants.
Root Cause Analysis
RenderTreeBuilder::Ruby::findOrCreateParentForStyleBasedRubyChild assumed that a render box whose display is RubyBlock has, as its direct first child, the RenderElement whose display is Ruby: it did if (parent.style().display()==RubyBlock && parent.firstChild()) { ASSERT(parent.firstChild()->style().display()==Ruby); return downcast<RenderElement>(*parent.firstChild()); }. That invariant holds for a normal ruby subtree, but render-tree continuations break it: a block-level element inside inline ruby content (e.g. a <div> forcing a block-in-inline split, as in the test’s <rb><span>base with <div>forced</div> line break</span>) inserts anonymous continuation wrappers, so the RubyBlock’s first child is an anonymous wrapper, not the Ruby box. Downcasting and returning that wrong renderer as the ruby parent corrupts the render-tree building (operating on a renderer of the wrong type/role), leading to the reported crash. The fix walks down through anonymous first-children (for (CheckedPtr first = parent.firstChild(); first; first = first->firstChildSlow())), skipping anonymous wrappers and returning the first child whose display is actually Ruby (bailing via ASSERT_NOT_REACHED if a non-anonymous non-ruby child is hit). The restored invariant is that the ruby parent is located by its actual Ruby display type rather than by blindly taking the first child, so continuations no longer misdirect ruby render-tree construction.
Attack Path
1. Craft ruby with a forced continuation
Author ruby markup where inline base content contains a block element (e.g. <rb><span>text <div>x</div> more</span></rb>) so the render tree inserts anonymous continuation wrappers under the RubyBlock.
2. Force ruby render-tree building
Lay out the ruby (e.g. position:absolute) so findOrCreateParentForStyleBasedRubyChild runs on the RubyBlock.
3. Mislocate the ruby parent
Pre-patch, parent.firstChild() is an anonymous continuation wrapper (not the Ruby box), but it is downcast and returned as the ruby parent.
4. Corrupt render-tree construction
Subsequent ruby child insertion operates on the wrong renderer, crashing (or corrupting the render tree) in the WebContent process.
Impact Assessment
A render-tree structural-assumption bug (advisory class UAF) reachable from crafted ruby markup with a continuation, in the WebContent process. The immediate effect is a crash from operating on a mis-typed renderer; escalation depends on how the wrong renderer is subsequently used. Confined to WebContent; rated medium.
Audit Directions
Other firstChild() type assumptions
grep RenderTreeBuilder* for firstChild()/lastChild() followed by downcast<> or an ASSERT on display type without walking anonymous wrappers.
Continuation-affected paths
Audit ruby/table/inline-block builders for structural assumptions broken by block-in-inline continuations.
ASSERT-guarded downcasts
Find ASSERT(x->style().display()==…) immediately before downcast<>; release builds skip the assert and take the bad cast.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
RenderTreeBuilder::Ruby::findOrCreateParentForStyleBasedRubyChild |
Source/WebCore/rendering/updating/RenderTreeBuilderRuby.cpp |
modified | Walks down anonymous first-children to find the child whose display is actually Ruby, instead of assuming parent.firstChild() is the Ruby box; handles continuation-inserted wrappers. |
Regression Test
The fix ships this proof-of-concept / regression test:
diff --git a/LayoutTests/fast/ruby/ruby-block-continuation-crash-expected.txt b/LayoutTests/fast/ruby/ruby-block-continuation-crash-expected.txt
new file mode 100644
index 000000000000..f85a15505104
--- /dev/null
+++ b/LayoutTests/fast/ruby/ruby-block-continuation-crash-expected.txt
@@ -0,0 +1,3 @@
+base with
+forced
+line break annotation This test passes if it doesn't crash.
diff --git a/LayoutTests/fast/ruby/ruby-block-continuation-crash.html b/LayoutTests/fast/ruby/ruby-block-continuation-crash.html
new file mode 100644
index 000000000000..3f762d4236ea
--- /dev/null
+++ b/LayoutTests/fast/ruby/ruby-block-continuation-crash.html
@@ -0,0 +1,9 @@
+<script>
+if (window.testRunner)
+ testRunner.dumpAsText();
+</script>
+<ruby style="position: absolute">
+ <rb><span>base with <div>forced</div> line break</span></rb>
+ <rt>annotation</rt>
+</ruby>
+This test passes if it doesn't crash.