Medium CVSS 5.5 webkit Logic Error 🔧 Commit mapped

Overview

Medium
Severity
5.5
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected process crash
ComponentWebCore Rendering
Bug ClassLogic Error
Tracker268770
Fix commitc2f9092d3a8e (WebKit/WebKit)
CWECWE-400
CVSS vectorCVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
CISA KEVNot listed
CreditedTashita Software Security
Disclosed2024-09-16

Background

Ruby / RubyBlock rendering
CSS ruby lays out annotation text alongside base text; WebKit uses a RubyBlock renderer that wraps ruby children in an anonymous Ruby-display box.
Render-tree continuation
The mechanism WebKit uses when an inline box contains a block-level box: the inline is split and linked via continuations, inserting extra anonymous wrapper renderers into the tree.
Anonymous renderer
A renderer with no corresponding DOM element that the engine synthesizes to satisfy box-model invariants (e.g. the anonymous ruby wrapper).
firstChildSlow()
A RenderObject accessor that returns the first child, used here to descend the first-child chain layer by layer while searching for the anonymous ruby box.
ASSERT vs release behavior
ASSERT/ASSERT_NOT_REACHED abort in debug builds but compile to nothing in release, so a violated ASSERT becomes silent wrong behavior (here, a bad downcast) in shipping builds.

Root Cause Analysis

The bug is in RenderTreeBuilder::Ruby::findOrCreateParentForStyleBasedRubyChild in Source/WebCore/rendering/updating/RenderTreeBuilderRuby.cpp, which, when appending a child under a RubyBlock parent, tries to locate the existing anonymous ruby box that should wrap ruby children. The pre-fix code did ‘if (parent.style().display() == DisplayType::RubyBlock && parent.firstChild())’ and then ASSERTed that ‘parent.firstChild()->style().display() == DisplayType::Ruby’ before returning parent.firstChild() downcast to RenderElement. The violated invariant is that the RubyBlock’s immediate first child is always the anonymous Ruby-display box. That assumption is false in the presence of render-tree continuations: when inline content (the ruby base) contains a block-level box (the <div> inside the <span> inside <rb> in the test), the inline is split and continuations are inserted, so the RubyBlock’s first child may be some other anonymous wrapper rather than the expected Ruby box. In a release build the ASSERT is compiled out, so the code would unconditionally downcast and return a first child that is not the anonymous ruby box, wiring subsequent children into the wrong renderer and corrupting the render tree (leading to the crash the test guards against); in a debug build the ASSERT fires.

The fix replaces the single-firstChild check with a descent loop: for (CheckedPtr first = parent.firstChild(); first; first = first->firstChildSlow()) it walks down the first-child chain, bailing with ASSERT_NOT_REACHED if it ever encounters a non-anonymous box, and returns the first box whose display is Ruby. This restores the invariant by actually searching for the anonymous ruby box through the continuation-introduced wrapper layers instead of assuming it is the immediate first child, and the accompanying FIXME explicitly notes ‘It should be the immediate child but continuations can break this assumption.’ The regression test (<ruby style=‘position: absolute’> with an <rb><span>…<div>forced</div>…</span></rb>) reproduces exactly the continuation-splitting scenario and passes if it does not crash.

Key insight
The code encoded a structural assumption (’the RubyBlock’s immediate first child is the anonymous ruby box’) as an ASSERT rather than a real check, and render-tree continuations violate that assumption, so in release builds the bad downcast/misparenting corrupts the render tree; the fix turns the assumption into an actual search for the anonymous box.

Attack Path

  1. Build a ruby subtree that forces continuations Author HTML where a ruby base contains inline content wrapping a block-level element (e.g. <ruby><rb><span>text <div>forced</div> break</span></rb><rt>…</rt></ruby>), so the inline is split and continuations are inserted.
  2. Trigger RubyBlock render-tree construction Use styling such as position: absolute on the ruby to force the style-based ruby render path so findOrCreateParentForStyleBasedRubyChild runs against a RubyBlock parent.
  3. Reach the false first-child assumption During child insertion the code assumes parent.firstChild() is the anonymous Ruby box, but continuations have made the first child a different anonymous wrapper.
  4. Bad downcast / misparented child In release builds (ASSERT compiled out) the code downcasts and returns the wrong renderer as the ruby parent, corrupting the render tree.
  5. Provoke the crash Subsequent layout/render-tree operations on the malformed tree dereference or manipulate an unexpected node type, causing an unexpected process crash.

Impact Assessment

The established primitive is render-tree corruption leading to an unexpected process crash in the sandboxed WebContent process, matching the CVE’s stated impact and medium severity. The pre-fix code returned/operated on a renderer of an unexpected type (an incorrect downcast target), which is a type-confusion-flavored logic error; whether it yields anything beyond a controlled crash (e.g. a limited type confusion usable toward memory corruption) is not demonstrated by the diff and would depend on how the misparented renderer is later used. Realistically this reads as a reliability/DoS bug reachable purely from crafted HTML/CSS with no user interaction; escalation to code execution would require additional primitives not shown here. It is confined to WebContent and does not by itself cross the sandbox.

Changed Functions

FunctionChangeNotes
RenderTreeBuilder::Ruby::findOrCreateParentForStyleBasedRubyChild
Source/WebCore/rendering/updating/RenderTreeBuilderRuby.cpp
modified Replaces the 'firstChild() is the anonymous Ruby box' assumption (guarded only by an ASSERT) with a firstChildSlow() descent loop that searches for the anonymous Ruby-display box and bails via ASSERT_NOT_REACHED on any non-anonymous node, handling continuation-split trees.
ruby-block-continuation-crash test
LayoutTests/fast/ruby/ruby-block-continuation-crash.html
added New regression test: absolutely-positioned ruby whose base wraps a block-level <div>, forcing continuations; passes if it does not crash.
ruby-block-continuation-crash expected
LayoutTests/fast/ruby/ruby-block-continuation-crash-expected.txt
added Expected text output for the regression test.

Audit Directions

  • Other firstChild() assumptions in ruby building
    Scan RenderTreeBuilderRuby.cpp and related ruby code for remaining uses of ‘firstChild()’ / ‘downcast<RenderElement>’ that assume a specific child type without a loop or isAnonymous()/display() check; verify each tolerates continuation-inserted wrappers.
  • ASSERT-guarded downcasts across RenderTreeBuilder
    Grep the rendering/updating RenderTreeBuilder*.cpp files for the pattern of an ASSERT on style().display() or node type immediately followed by a downcast<> and return, which is the same ‘assumption compiled out in release’ shape fixed here.
  • Continuation-breaking-invariants pattern
    Search WebCore rendering for places that assume an immediate parent/child relationship (firstChild/parent) for anonymous wrappers; grep for ‘continuation’, ‘isAnonymous’, ‘firstChildSlow’ and look for spots that inspect only the immediate child where block-in-inline splitting could insert extra layers.

Original Bug Report

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