[WebCore] Use-after-free in DataListButtonElement::defaultEventHandler
CVE: CVE-2026-64783 · Safari 26.6 · Released July 27, 2026 Impact: Processing maliciously crafted web content may lead to an unexpected Safari crash Apple's description: A use-after-free issue was addressed with improved memory management. Credit: 杉山 壮太, lattice, Behzad Najjarpour Jabbari (@G4ru), Junyeong Lee, Mooth.ai, OGINOME Tomohito, Using GLM From Z.AI, Gia Bui (@yabeow) from Calif.io
High. A raw back-pointer that outlives its target, dereferenced as a pure-virtual call — the shape that turns a form-control teardown into an indirect branch. Escalation past the crash needs same-type heap reclaim that the shipped test case does not attempt.
Complex form controls in WebKit are not single elements; each <input> delegates its behavior to a polymorphic implementation object and grows an internal shadow tree of helper nodes that the page can see rendered but cannot script directly. Those helper nodes hold back-pointers to the implementation object that built them, and the implementation object is swapped wholesale the moment the element's type changes. DataListButtonElement — the dropdown indicator injected into <input list=...> — held that back-pointer as a raw C++ reference bound once in its constructor, with no path anywhere in the class that could clear it.
The angle: A page that gets one click on a datalist dropdown arrow can destroy the input's implementation object from a listener and then have native code make a virtual call through the freed pointer.
Source/WebCore/html/shadow/DataListButtonElement.h
Source/WebCore/html/shadow/DataListButtonElement.cpp
Source/WebCore/html/TextFieldInputType.cpp
LayoutTests/fast/forms/datalist/datalist-button-change-input-type-on-click-crash.html
Patch Details
Three production changes and one regression test, and the three production changes are not redundant with each other — they close the same hole at three different points on the lifetime.
In DataListButtonElement.h, the member type changes from DataListButtonOwner& m_owner to WeakPtr<DataListButtonOwner> m_owner. To make that legal, the nested DataListButtonOwner interface class now derives from AbstractRefCountedAndCanMakeWeakPtr<DataListButtonOwner>, the WTF mixin that lets an abstract interface be both weakly referenced and upgraded back to a strong reference. A new public one-liner void removeOwner() { m_owner = nullptr; } gives the owner a way to sever the edge explicitly, and <wtf/AbstractRefCountedAndCanMakeWeakPtr.h> is pulled in.
In DataListButtonElement.cpp, the unconditional m_owner.dataListButtonElementWasClicked() becomes if (RefPtr owner = m_owner) owner->dataListButtonElementWasClicked();. Two things happen in that one line: the weak pointer is null-checked, and — if it is still live — it is upgraded to a strong reference that is held across the virtual call.
In TextFieldInputType.cpp, removeShadowSubtree() gains a dataListDropdownIndicator->removeOwner() call before nulling m_dataListDropdownIndicator. It sits directly beneath the pre-existing autoFillButton->removeOwner() line and is textually identical in shape to it.
removeShadowSubtree(), before: after:
autoFillButton->removeOwner() autoFillButton->removeOwner()
m_autoFillButton = nullptr m_autoFillButton = nullptr
─── (no removeOwner) ─── dataListDropdownIndicator->removeOwner()
m_dataListDropdownIndicator = null m_dataListDropdownIndicator = nullptr
m_container = nullptr m_container = nullptr
The layout test builds the minimal reproducer: a text input with a list attribute, a one-shot click listener that reassigns input.type = 'button' and calls gc(), and UIHelper.activateElement() driving a real activation of the -webkit-list-button node reached through internals.shadowRoot().
Background
User-agent shadow DOM. WebKit implements complex form controls by attaching an internal shadow tree to the host element. These nodes render and receive events like any other node, but page script cannot reach them by ordinary DOM traversal; they are tagged with useragentpart attributes (-webkit-list-button here) and test harnesses reach them via internals.shadowRoot().
InputType and type swapping. An <input> element does not implement its own behavior. It delegates to a polymorphic InputType object selected by its type attribute. TextFieldInputType is the base for text-like types and is the sole implementer of the DataListButtonElement::DataListButtonOwner interface. When the type content attribute or IDL attribute changes, HTMLInputElement::updateType() runs: it calls removeShadowSubtree() on the outgoing type — the teardown hook that dismantles the shadow tree that type built — and then replaces the InputType object entirely.
<input list> and the dropdown indicator. When a text input references a <datalist>, TextFieldInputType adds a clickable dropdown indicator into the shadow subtree. That indicator is a DataListButtonElement, constructed with a reference to its owner: DataListButtonElement(Document&, DataListButtonOwner& owner) : m_owner(owner). Clicking it is how the control asks its owner to display the suggestion list.
defaultEventHandler. This is the per-node hook the event dispatcher invokes to implement a node's built-in behavior for an event. It runs after the author's listeners for that event have already run.
Event dispatch re-entrancy. Author JavaScript listeners execute synchronously in the middle of native event dispatch. Between the moment native code enters dispatch and the moment it resumes in a default handler, arbitrary script has had the opportunity to mutate the DOM and engine state.
Smart pointers. RefPtr is a strong reference-counted pointer that keeps its target alive. WeakPtr is non-owning and is automatically nulled when the pointee is destroyed. AbstractRefCountedAndCanMakeWeakPtr<T> is the WTF mixin that gives an abstract interface class both properties at once.
TZone allocation. TextFieldInputType carries WTF_MAKE_TZONE_ALLOCATED_IMPL, placing instances in a type-segregated heap zone. Freed memory in such a zone is preferentially reused by allocations of the same type rather than by arbitrary attacker-chosen objects.
Analysis
The bug is an asymmetric back-pointer lifecycle: the child→owner edge is established at construction and never severed on the owner's teardown path, so the child outlives the object it points at.
DataListButtonElement (event target) TextFieldInputType (owner)
──────────────────────────────────── ──────────────────────────
dispatch begins, node ref'd ────────► alive
author click listener runs
input.type = 'button' ──────────► updateType()
removeShadowSubtree()
m_dataListDropdownIndicator = null
(back-pointer NOT cleared) ◄── hole
InputType replaced → ~TextFieldInputType()
defaultEventHandler resumes FREED
m_owner.dataListButtonElementWasClicked() ──► virtual call on freed memory
The left column survives the whole sequence for a reason worth stating plainly: event dispatch retains the target node, so the DataListButtonElement is guaranteed alive from the start of dispatch to the end of the default handler. That guarantee is exactly what makes the bug reachable — the element is safely alive while the thing it points to is not. The right column is where the invariant breaks. removeShadowSubtree() drops the owner's strong references (m_autoFillButton, m_dataListDropdownIndicator, m_container) and calls removeOwner() on the autofill button, but does nothing about the datalist indicator's back-pointer. Nothing else in the codebase severed that edge, because the raw reference member had no setter at all.
Once updateType() releases the HTMLInputElement's owning reference to the old TextFieldInputType, the object is destroyed. This is a real destructor, not a bookkeeping no-op — ~TextFieldInputType() runs closeSuggestions() and detaches m_suggestionPicker, so the memory is genuinely returned. Control then unwinds back into DataListButtonElement::defaultEventHandler, which reaches the click branch and executes the call through m_owner unconditionally.
That call is a pure-virtual dispatch. dataListButtonElementWasClicked() is declared virtual void ... = 0 on the interface, so reaching it requires loading the vtable pointer from the freed object's first word and indirecting through it. The distance from "dangling reference" to "indirect call target read from reclaimed memory" is one load.
Each of the three production changes independently defeats the shipped test case, which is what makes this defense in depth rather than one fix:
removeOwner()inremoveShadowSubtree()clears the edge eagerly at detach time — the path the test exercises.WeakPtrnulls the member automatically at owner destruction, covering any path that destroys aTextFieldInputTypewithout routing throughremoveShadowSubtree().RefPtr owner = m_ownerpins the owner alive for the duration of the call, so re-entrancy triggered insidedataListButtonElementWasClicked()— which opens a suggestion picker and can itself run script — cannot free it mid-callback.
For impact: this lives entirely inside the WebContent process and crosses no sandbox boundary on its own. The trigger needs a real activation of the dropdown indicator, so a user gesture is in the chain, but the crash is reliably reproducible from a page. An attacker who reclaimed the freed TextFieldInputType slot before the virtual dispatch could turn this into a controlled indirect call in the renderer, the standard starting point for building read/write primitives — that escalation is plausible and unproven, and the TZone zone constrains it to same-type reclaim. Apple's "unexpected Safari crash" framing reflects the minimum observed impact.
A UA shadow element's raw back-pointer to its owning InputType survived the owner's teardown, so a click listener that reassigns input.type turns the default handler's pure-virtual call into a dereference of freed memory.
Insight
The safe idiom was already in the file — two lines above the bug. removeShadowSubtree() called autoFillButton->removeOwner(), and the fix's own commit message points at SpinButtonElement as the pattern being matched. The removeOwner() teardown convention was established, applied to a sibling member in the same function, and simply not extended when the datalist indicator was added later. Partial application of a known-good teardown idiom is a productive hunting ground: whenever a teardown function nulls N owned members but calls an unregister helper on only M < N of them, the remaining N−M are candidates.
Audit directions
-
Teardown functions that clear owning references but only partially sever the reciprocal back-pointers. The tell is a function that nulls N members while calling an unregister/detach helper on fewer than N of them. Narrow: re-read
TextFieldInputType::removeShadowSubtree()and the analogous teardown in the otherInputTypesubclasses, plusTextControlInnerElements.cpp,SearchFieldResultsButtonElement,SpinButtonElement, andAutoFillButtonElement— for every UA shadow element constructed with an owner argument, confirm aremoveOwner()-equivalent runs on every path that destroys the owner. Wider: the same asymmetry appears in any WebCore pairing where acreate(Document&, SomeOwner&)factory stores the owner, so search for constructors taking an owner reference and check whether the owner's destructor or detach hook clears it —RenderThemedelegates, media control shadow elements, and form validation bubble owners share the shape. Widest: this is the general "reciprocal reference established at construction, torn down on only one side" invariant, which holds in any object graph with parent/child back-edges — Blink's shadow elementowner_fields, Qt parent/child observers, or any ORM/UI framework with bidirectional links. Match tell per rung: narrow — a rawT&orT*member initialized in the constructor and never assigned anywhere else in the class; wider — a factory signature carrying an owner reference whose class has no clearing setter at all; widest — if a link is created in exactly one place and never written again, ask which side outlives the other. -
Native code that reads a member pointer after a re-entrancy boundary without re-validating it. Narrow: grep
Source/WebCore/html/shadow/andSource/WebCore/html/fordefaultEventHandlerbodies that dereference a member (m_owner,m_element,m_input) rather than a localRef/RefPtr, and ask whether a listener earlier in the same dispatch could destroy that member's target —input.typereassignment,element.remove(), and form reassociation are the cheap levers. Wider: the class covers any WebCore entry point invoked after script may have run — style resolution callbacks,didFinishInsertingNode, form-control state restore, andResizeObserver/IntersectionObserverdelivery loops. Widest: the reusable invariant is "after native code yields to user-supplied code, no pre-yield pointer to user-mutable state is trustworthy," which applies to any embedded scripting host — V8/Blink bindings, Lua/C hosts, Python C extensions calling back into interpreter code. Match tell: on the narrow rung, a member dereference textually after a call that can dispatch events or resolve style; on the widest rung, ask "what is the longest-lived pointer on this stack frame, and can the callback reach the object that owns it?" -
HTMLInputElement::updateType()as a general destruction primitive rather than as one bug's trigger. The pattern class is a single script-reachable attribute assignment that synchronously destroys and replaces a large polymorphic object other code holds pointers into. Narrow: enumerate every member and shadow node created by eachInputTypesubclass and ask, for each, whether anything outside theInputTyperetains a pointer that survives the swap — start from theneedsShadowSubtree()/createShadowSubtree()/removeShadowSubtree()triples. Wider: other WebCore operations with the same "replace the implementation object under live observers" shape include element re-association to a new form owner,document.open()replacing the document's state, and custom element upgrade replacing an element's behavior; each deserves the same what-still-points-here inventory. Widest: the principle is "any state transition that swaps an implementation strategy object must enumerate its inbound edges," applicable to any strategy/PIMPL/plugin architecture supporting live reconfiguration. Match tell: a setter that reassigns astd::unique_ptr/Refholding a polymorphic implementation, where the old implementation had handed outthisto anything. -
Whether TZone type-segregated allocation is doing the mitigation work that severity triage assumes for this bug class. The pattern class is a UAF on a class carrying
WTF_MAKE_TZONE_ALLOCATED_IMPLwhere the freed type is itself cheaply mass-allocatable from web content. Narrow: forTextFieldInputTypespecifically, measure whether creating many text-like<input>elements reliably reclaims a just-freed instance within a single event dispatch. Wider: ask the same of other TZone-allocated WebCore objects page script can instantiate in bulk —InputTypesubclasses,RenderObjectfamilies, shadow element classes — since a per-type zone only constrains an attacker to the extent the type is expensive or rate-limited to allocate. Widest: the invariant is "type-segregated heaps reduce exploitability only in proportion to how hard it is for the attacker to allocate that exact type," which applies equally to PartitionAlloc type buckets, isolated heaps in other engines, and slab allocators generally. Match tell: any UAF write-up citing heap partitioning as mitigating without stating how many instances of the freed type an attacker can create per second — that gap is the thing to measure. This rung genuinely requires runtime experimentation, not code reading.