← WebKit Silent-Fix Report — 2026-W25

810f13a41f  REGRESSION(307136@main): crash adding multiple attribute to appearance: base <select>

severity medium class TypeConfusion confidence 0.60 WebCore HTML HTMLSelectElement exploitable-grade
Anne van Kesteren Sun Jun 21 22:11:57 2026 -0700 full: 810f13a41f203822a3bab0e23374dcea46ad9531 bug report ↗ view on GitHub ↗
Primitive: stale renderer type after appearance:base-select multiple
Triage note: Script-triggerable crash from operating on a not-yet-rebuilt renderer of the wrong concrete type; a renderer type-confusion/bad-cast bug.
Contents

The bug at a glance

A script-reachable bad-cast: downcast<RenderListBox> was applied unconditionally to a renderer that, for an appearance:base-select with a freshly-added multiple attribute, is still the generic (non-RenderListBox) renderer. In a release build downcast<> is an unchecked static_cast, so the code operated on a wrong-type object – a type-confusion crash and potential memory-corruption primitive – but reaching a controllable corruption state is constrained by the transient renderer-rebuild window, keeping it medium.

The angle is a state/type desync between the DOM (m_multiple = true the instant the attribute is set) and layout (the RenderListBox rebuild is only scheduled, not yet done). Event handlers and option-change code assumed ‘multiple implies RenderListBox’ and downcast unconditionally, which was true for legacy selects but false for the new appearance:base-select generic renderer.

Root cause

OBSERVED: This is a REGRESSION from 307136@main (the customizable/appearance:base-select work). An appearance:base-select uses a generic renderer rather than the classic RenderMenuList/RenderListBox. When script sets the ‘multiple’ attribute, HTMLSelectElement immediately reflects m_multiple = true and usesMenuList() becomes false, but the switch to a RenderListBox renderer is only scheduled asynchronously; until that rebuild runs, this()->renderer() is still the generic base-appearance renderer.

OBSERVED: Three code sites then performed downcast<RenderListBox>(*renderer) unconditionally. In HTMLSelectElement::setOptionsChangedOnRenderer(), the else-branch guarded only on !usesMenuList() and then downcast<RenderListBox>(*renderer).setOptionsChanged(true). In listBoxDefaultEventHandler(), a mousemove path did CheckedRef renderListBox = downcast<RenderListBox>(*renderer()), and a keyboard/selection path did downcast<RenderListBox>(*renderer).scrollToRevealElementAtListIndex(endIndex).

OBSERVED: downcast<> in WebKit is an unchecked static_cast in release builds (RELEASE_ASSERT of the type only in debug). So when renderer is the generic renderer, these sites reinterpret its memory as a RenderListBox and call RenderListBox methods (setOptionsChanged, canBeScrolledAndHasScrollableArea, scrollToRevealElementAtListIndex) on an object that is not one – a classic bad-cast type confusion.

OBSERVED: The fix replaces each unconditional downcast with dynamicDowncast<RenderListBox> and a null check: setOptionsChangedOnRenderer now does ’else if (auto* renderListBox = dynamicDowncast<RenderListBox>(*renderer)) renderListBox->setOptionsChanged(true)’; the mousemove path takes CheckedPtr renderListBox = dynamicDowncast<…> and returns early if null; the selection path guards scrollToRevealElementAtListIndex behind an if-let. When the cast fails (renderer still generic), the code now no-ops until the real rebuild installs a RenderListBox.

INFERRED: The three added crash tests (select-add-multiple-crash, select-multiple-keydown-crash, select-multiple-mousemove-crash) each reproduce the window by setting ‘multiple’ inside requestAnimationFrame and then either doing nothing (option-change path), dispatching a keydown (selection/scroll path), or dispatching a mousemove (mousemove path) before the rebuild. INFERRED: exploitability as more than a crash depends on how much of the generic renderer’s layout overlaps a RenderListBox vtable/field layout; the patch treats it as a crash/type-confusion to be closed rather than demonstrating corruption.

Key code

Two of the three unconditional downcasts replaced by dynamicDowncast + no-op (HTMLSelectElement.cpp)

        if (auto* renderMenuList = dynamicDowncast<RenderMenuList>(*renderer))
            renderMenuList->setOptionsChanged(true);
        else if (auto* renderListBox = dynamicDowncast<RenderListBox>(*renderer))
            renderListBox->setOptionsChanged(true);
...
    } else if (event.type() == eventNames.mousemoveEvent && mouseEvent) {
        CheckedPtr renderListBox = dynamicDowncast<RenderListBox>(*renderer());
        if (!renderListBox)
            return;
        if (renderListBox->canBeScrolledAndHasScrollableArea())
            return;

Patch walkthrough

  • Source/WebCore/html/HTMLSelectElement.cpp — Three unconditional downcast<RenderListBox> sites are converted to dynamicDowncast<RenderListBox> with null handling. setOptionsChangedOnRenderer() replaces the ‘!usesMenuList()’ + downcast else-branch with ’else if (auto* renderListBox = dynamicDowncast<RenderListBox>(renderer))’. listBoxDefaultEventHandler()’s mousemove branch uses CheckedPtr + early return when the cast fails. The keyboard-driven selection branch guards scrollToRevealElementAtListIndex behind ‘if (auto renderListBox = dynamicDowncast<RenderListBox>(*renderer))’. Net effect: each site no-ops while the renderer is still the generic base-appearance renderer.
  • LayoutTests/.../customizable-select/select-add-multiple-crash.html — Adds ‘multiple’ to an appearance:base-select inside requestAnimationFrame with no further interaction, exercising the option-change / setOptionsChangedOnRenderer path against the not-yet-rebuilt renderer.
  • LayoutTests/.../customizable-select/select-multiple-keydown-crash.html — Sets ‘multiple’ then dispatches a KeyboardEvent(‘keydown’) before the rebuild, hitting the scrollToRevealElementAtListIndex downcast path; the comment explicitly notes the renderer is still generic, not RenderListBox.
  • LayoutTests/.../customizable-select/select-multiple-mousemove-crash.html — Sets ‘multiple’ then dispatches a MouseEvent(‘mousemove’) before the rebuild, hitting the mousemove downcast path.

Background

appearance:base-select (customizable select) — A newer feature (landed in 307136@main) letting <select> be rendered with a generic, fully-stylable renderer rather than the platform-native RenderMenuList/RenderListBox. This means the invariant that a multiple-select maps to a RenderListBox no longer holds for base-appearance selects.

usesMenuList() vs multiple — HTMLSelectElement chooses a dropdown (menu list) versus a list box renderer based partly on the multiple attribute and size. Setting ‘multiple’ flips m_multiple and usesMenuList() synchronously, but the renderer that backs the element is only rebuilt asynchronously, opening a window where element state and renderer type disagree.

downcast<> vs dynamicDowncast<> — WebKit’s downcast<T>(x) is a type-asserting cast that in release builds compiles to an unchecked static_cast (the type assertion is debug-only). dynamicDowncast<T>(x) returns a pointer that is null when x is not a T. Using downcast where the runtime type is not guaranteed is the standard source of WebKit bad-cast type-confusion bugs.

RenderListBox event handling — listBoxDefaultEventHandler drives keyboard and mouse interaction for list-box-style selects (scrolling to the active item, active-selection anchor/end tracking), calling RenderListBox-specific methods like canBeScrolledAndHasScrollableArea and scrollToRevealElementAtListIndex, which only exist on RenderListBox.

Renderer rebuild scheduling — Changing attributes that affect box construction schedules a style/renderer update rather than rebuilding synchronously. requestAnimationFrame is a reliable way for a test (or an attacker) to interleave script with the frame lifecycle so events are dispatched while the old renderer is still installed.

Vulnerability window

  1. Regression introduced — 307136@main added appearance:base-select with a generic renderer, breaking the implicit ‘multiple select => RenderListBox’ assumption baked into HTMLSelectElement’s downcasts.
  2. Report — Keith Cirkel flagged the crash to Anne van Kesteren; bug 317533 filed as a REGRESSION.
  3. Root cause — Setting ‘multiple’ on a base-select flips m_multiple/usesMenuList() immediately while the RenderListBox rebuild is still pending, so downcast<RenderListBox>(renderer) operates on the generic renderer.
  4. Fix — Convert the three downcast sites to dynamicDowncast with null-guarded no-ops so the code safely does nothing until the correct renderer exists.
  5. Tests — Three WPT crash tests added (add-multiple, keydown, mousemove) plus an upstream WPT PR, each reproducing the transient window via requestAnimationFrame.
  6. Landed — Committed as 315575@main on 2026-06-21, reviewed by Tim Nguyen.

Proof of concept

OBSERVED: This is the verbatim added select-multiple-mousemove-crash.html. It styles a <select> as appearance:base-select, then inside requestAnimationFrame sets the ‘multiple’ attribute (flipping m_multiple while the renderer is still generic) and dispatches a mousemove, driving listBoxDefaultEventHandler’s mousemove branch into the unconditional downcast<RenderListBox>. Pre-patch this bad-casts the generic renderer and crashes; the keydown and add-multiple variants trigger the scroll and option-change paths respectively.

<!DOCTYPE html>
<html class=test-wait>
<style>select { appearance: base-select; }</style>
<select></select>
<script>
requestAnimationFrame(() => {
  const select = document.querySelector('select');
  select.setAttribute('multiple', '');
  // Renderer rebuild is scheduled but has not happened yet; the renderer is
  // still the generic base-appearance one rather than RenderListBox.
  select.dispatchEvent(new MouseEvent('mousemove', {bubbles: true}));
  document.documentElement.classList.remove('test-wait');
});
</script>

Exploitation

  1. Trigger — From script, set ‘multiple’ on an appearance:base-select and, within the same frame (requestAnimationFrame), dispatch mousemove/keydown or mutate options so a downcast<RenderListBox> site runs against the still-generic renderer.
  2. Type confusion — downcast<> is an unchecked static_cast in release; RenderListBox methods are invoked on a non-RenderListBox object, reading/writing memory according to the wrong class layout and vtable.
  3. Outcome — Reliably a crash (the added tests are crash tests). Turning the bad-cast into a controlled corruption primitive would require grooming the generic renderer’s memory so RenderListBox field/vtable accesses land on attacker-influenced data, which the patch does not demonstrate; treat as crash-only absent further evidence.

Detection & hunting

For defenders and SOC / detection engineers:

  • multiple set on base-select then immediate interaction
  • Crashes in RenderListBox methods with wrong-type renderer
  • Debug builds

Audit directions

  • Remaining downcasts in HTMLSelectElement / form controls
  • Other appearance:base controls
  • State/renderer desync windows generally
  • downcast vs dynamicDowncast policy

Before / after

Loading diff…