Released July 27, 2026

CVE-2026-64783

CVSS 8.8 severity high CWE-416 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

Overview

Impact:

Description: Processing maliciously crafted web content may lead to an unexpected Safari crash

Researchers: 杉山 壮太, lattice, Behzad Najjarpour Jabbari (@G4ru), Junyeong Lee, Mooth.ai, OGINOME Tomohito, Using GLM From Z.AI, Gia Bui (@yabeow) from Calif.io

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.

Patch Details

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 };
 };
 

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2026-64783
Bugzilla 313521
Component WebCore
Bug Class UAF
Severity medium
Commit be08720593705c04…
Advisory Apple Advisory

Terminology

  • 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

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.

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.

Changed Functions

Function File Change Note
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

Source Files

  • Source/WebCore/html/TextFieldInputType.cpp
  • Source/WebCore/html/shadow/DataListButtonElement.cpp
  • Source/WebCore/html/shadow/DataListButtonElement.h

Test Files

  • 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

Regression Test

The fix ships this proof-of-concept / regression test:

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>