Released July 29, 2024

CVE-2024-40782

CVSS 6.5 severity medium CWE-416 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H

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.