CVE-2026-64783
Overview
Background
- TextFieldInputType datalist
- A text input with a <datalist> renders a dropdown-indicator button in its shadow subtree, owned by the TextFieldInputType.
- Raw reference vs WeakPtr
- The button held a raw DataListButtonOwner& to its owner; a raw reference does not detect when the owner is destroyed.
- Type-change teardown
- Changing an input’s type destroys its current input type object and shadow subtree — which can happen mid-click.
Root Cause Analysis
This fixes a use-after-free of a form control’s input type via the datalist dropdown button’s owner pointer. DataListButtonElement held a raw reference DataListButtonOwner& m_owner (the owning TextFieldInputType) and, on click, called m_owner.dataListButtonElementWasClicked() directly. When the input’s shadow subtree is torn down — e.g. the page changes the input’s type during the click — the owner (TextFieldInputType) is destroyed while the DataListButtonElement can still receive the click, so the raw m_owner reference dangles and the call is a use-after-free.
The fix makes DataListButtonOwner derive from AbstractRefCountedAndCanMakeWeakPtr and changes m_owner to a WeakPtr<DataListButtonOwner>; TextFieldInputType::removeShadowSubtree() now calls dataListDropdownIndicator->removeOwner() (which nulls m_owner) when tearing down, and defaultEventHandler() guards the callback with if (RefPtr owner = m_owner).
The restored invariant is that the button never dereferences an owner that may have been destroyed: the weak pointer becomes null on teardown and the RefPtr upgrade both null-checks and keeps the owner alive across the call. The regression test sets input.type=‘button’ (removing the datalist shadow subtree) inside the input’s click handler and then activates the list button, exercising exactly this path. Fully established by the diff.
Attack Path
- Create a text input with a datalist The page has <input type=text list=…> which builds a datalist dropdown button in the input’s shadow tree.
- Change the input type during a click A click handler on the input sets input.type=‘button’, which removes the datalist shadow subtree and destroys the owning TextFieldInputType, then forces GC.
- Deliver a click to the dangling button The datalist dropdown button still receives the click and, pre-patch, calls m_owner.dataListButtonElementWasClicked() through a now-dangling raw reference.
- Use-after-free Dereferencing the freed owner corrupts/reads freed memory and crashes the process.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
TextFieldInputType::removeShadowSubtreeSource/WebCore/html/TextFieldInputType.cpp |
modified | Now calls m_dataListDropdownIndicator->removeOwner() before clearing it, so the dropdown button drops its owner pointer when the shadow subtree is torn down. |
DataListButtonElement::defaultEventHandlerSource/WebCore/html/shadow/DataListButtonElement.cpp |
modified | Guards the owner callback with `if (RefPtr owner = m_owner)`, null-checking and protecting the owner across dataListButtonElementWasClicked(). |
DataListButtonElement / DataListButtonOwnerSource/WebCore/html/shadow/DataListButtonElement.h |
modified | m_owner changes from a raw DataListButtonOwner& to WeakPtr<DataListButtonOwner>; DataListButtonOwner now derives from AbstractRefCountedAndCanMakeWeakPtr; adds removeOwner() to null the weak pointer. |
Files Changed
LayoutTests/fast/forms/datalist/datalist-button-change-input-type-on-click-crash-expected.txtLayoutTests/fast/forms/datalist/datalist-button-change-input-type-on-click-crash.htmlSource/WebCore/html/TextFieldInputType.cppSource/WebCore/html/shadow/DataListButtonElement.cppSource/WebCore/html/shadow/DataListButtonElement.h
Audit Directions
- Shadow elements' back-references to input typesAudit other shadow DOM helper elements that store a raw pointer/reference to their owning input type or renderer; type changes and shadow teardown can free the owner mid-event.
- Event delivery during teardownLook for defaultEventHandler paths that call back into an owner without a WeakPtr guard and RefPtr upgrade to keep it alive across the call.
Patch
diff --git a/LayoutTests/fast/forms/datalist/datalist-button-change-input-type-on-click-crash-expected.txt b/LayoutTests/fast/forms/datalist/datalist-button-change-input-type-on-click-crash-expected.txt
new file mode 100644
index 000000000000..e1f00b8f5f64
--- /dev/null
+++ b/LayoutTests/fast/forms/datalist/datalist-button-change-input-type-on-click-crash-expected.txt
@@ -0,0 +1,5 @@
+PASS if no crash.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/fast/forms/datalist/datalist-button-change-input-type-on-click-crash.html b/LayoutTests/fast/forms/datalist/datalist-button-change-input-type-on-click-crash.html
new file mode 100644
index 000000000000..cfb0e901651f
--- /dev/null
+++ b/LayoutTests/fast/forms/datalist/datalist-button-change-input-type-on-click-crash.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
+<script src="../../../resources/js-test.js"></script>
+<script src="../../../resources/ui-helper.js"></script>
+<style>
+input {
+ width: 300px;
+ height: 50px;
+}
+</style>
+</head>
+<body>
+<input id="input" type="text" list="list">
+<datalist id="list"><option value="a"><option value="b"></datalist>
+<script>
+
+jsTestIsAsync = true;
+
+addEventListener("load", async () => {
+ input.addEventListener('click', (e) => {
+ input.type = 'button';
+ gc();
+ }, { once: true });
+
+ if (window.internals) {
+ let shadow = internals.shadowRoot(input);
+ let listButton = shadow.querySelector("div[useragentpart='-webkit-list-button']");
+
+ await UIHelper.activateElement(listButton);
+ }
+
+ debug("PASS if no crash.");
+ finishJSTest();
+});
+
+</script>
+</body>
+</html>
diff --git a/Source/WebCore/html/TextFieldInputType.cpp b/Source/WebCore/html/TextFieldInputType.cpp
index e07626ba7197..a1e8fa815c81 100644
--- a/Source/WebCore/html/TextFieldInputType.cpp
+++ b/Source/WebCore/html/TextFieldInputType.cpp
@@ -428,6 +428,8 @@ void TextFieldInputType::removeShadowSubtree()
if (RefPtr autoFillButton = m_autoFillButton.get())
autoFillButton->removeOwner();
m_autoFillButton = nullptr;
+ if (RefPtr dataListDropdownIndicator = m_dataListDropdownIndicator)
+ dataListDropdownIndicator->removeOwner();
m_dataListDropdownIndicator = nullptr;
m_container = nullptr;
}
diff --git a/Source/WebCore/html/shadow/DataListButtonElement.cpp b/Source/WebCore/html/shadow/DataListButtonElement.cpp
index be70a35cbf4c..71e15afbe7d4 100644
--- a/Source/WebCore/html/shadow/DataListButtonElement.cpp
+++ b/Source/WebCore/html/shadow/DataListButtonElement.cpp
@@ -61,7 +61,8 @@ void DataListButtonElement::defaultEventHandler(Event& event)
}
if (isAnyClick(*mouseEvent)) {
- m_owner.dataListButtonElementWasClicked();
+ if (RefPtr owner = m_owner)
+ owner->dataListButtonElementWasClicked();
event.setDefaultHandled();
}
diff --git a/Source/WebCore/html/shadow/DataListButtonElement.h b/Source/WebCore/html/shadow/DataListButtonElement.h
index 1b483b651c07..75fefd1dae14 100644
--- a/Source/WebCore/html/shadow/DataListButtonElement.h
+++ b/Source/WebCore/html/shadow/DataListButtonElement.h
@@ -26,6 +26,7 @@
#pragma once
#include "HTMLDivElement.h"
+#include <wtf/AbstractRefCountedAndCanMakeWeakPtr.h>
namespace WebCore {
@@ -35,7 +36,7 @@ class DataListButtonElement final : public HTMLDivElement {
WTF_MAKE_TZONE_ALLOCATED(DataListButtonElement);
WTF_OVERRIDE_DELETE_FOR_CHECKED_PTR(DataListButtonElement);
public:
- class DataListButtonOwner {
+ class DataListButtonOwner : public AbstractRefCountedAndCanMakeWeakPtr<DataListButtonOwner> {
public:
virtual ~DataListButtonOwner() = default;
virtual void dataListButtonElementWasClicked() = 0;
@@ -47,6 +48,8 @@ class DataListButtonElement final : public HTMLDivElement {
bool canAdjustStyleForAppearance() const { return m_canAdjustStyleForAppearance; }
+ void removeOwner() { m_owner = nullptr; }
+
private:
explicit DataListButtonElement(Document&, DataListButtonOwner&);
@@ -56,7 +59,7 @@ class DataListButtonElement final : public HTMLDivElement {
void defaultEventHandler(Event&) final;
bool isDisabledFormControl() const final;
- DataListButtonOwner& m_owner;
+ WeakPtr<DataListButtonOwner> m_owner;
bool m_canAdjustStyleForAppearance { true };
};