Medium CVSS 8.8 webkit UAF 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected Safari crash
ComponentWebCore HTML
Bug ClassUAF
Tracker313521
Fix commitbe0872059370 (WebKit/WebKit) +54/-3
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Credited杉山 壮太, lattice, Behzad Najjarpour Jabbari (@_G4ru_), Junyeong Lee, Mooth.ai, OGINOME Tomohito, Using GLM From Z.AI, Gia Bui (@yabeow) from Calif.io
Disclosed2026-07-27

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.

Key insight
The dropdown button outlived its owner because it held a raw reference to the TextFieldInputType, which the type change destroyed while the button could still receive the click. The fix converts the owner to a WeakPtr, nulls it on shadow-subtree teardown, and upgrades to a RefPtr across the callback — so the button never touches a destroyed owner.

Attack Path

  1. 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.
  2. 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.
  3. 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.
  4. Use-after-free Dereferencing the freed owner corrupts/reads freed memory and crashes the process.

Impact Assessment

A use-after-free in the WebContent process from a common, script-reachable interaction: changing an input’s type during a click on its datalist button. Attacker-controlled DOM mutation timing makes the freed object’s contents controllable, giving a reliable UAF primitive toward memory disclosure and control-flow hijack.

Changed Functions

FunctionChangeNotes
TextFieldInputType::removeShadowSubtree
Source/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::defaultEventHandler
Source/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 / DataListButtonOwner
Source/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.txt
  • LayoutTests/fast/forms/datalist/datalist-button-change-input-type-on-click-crash.html
  • Source/WebCore/html/TextFieldInputType.cpp
  • Source/WebCore/html/shadow/DataListButtonElement.cpp
  • Source/WebCore/html/shadow/DataListButtonElement.h

Audit Directions

  • Shadow elements' back-references to input types
    Audit 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 teardown
    Look for defaultEventHandler paths that call back into an owner without a WeakPtr guard and RefPtr upgrade to keep it alive across the call.
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 };
 };
 
Loading diff…

Original Bug Report

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