Medium CVSS 6.5 webkit UAF 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected Safari crash
ComponentWebCore
Sub-componentWebCore HTML
Bug ClassUAF
Tracker310544
Fix commit869d5c553137 (WebKit/WebKit) +87/-28
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
CISA KEVNot listed
CreditedGia Bui (@yabeow) from Calif.io, dr3dd, w0wbox
Disclosed2026-05-11

Background

Shadow edit element
DateTimeEditElement renders the editable subfields of date/time inputs and calls back into its owning input type for values and events.
Type change teardown
Setting input.type destroys the current InputType object and its shadow subtree, potentially mid-event.
WeakPtr upgrade to RefPtr
Converting a weak reference to a RefPtr before use both detects destruction (null) and keeps the object alive for the call.

Root Cause Analysis

This fixes a use-after-free of a date/time input’s edit-control owner when the input’s type changes during an event. DateTimeEditElement (the shadow element hosting editable date fields) referenced its owner (DateTimeEditElementEditControlOwner, implemented by BaseDateAndTimeInputType) through m_editControlOwner as a plain non-owning CanMakeWeakPtr target, and called back into it directly (didReceiveSpaceKeyFromControl, didBlurFromControl, didChangeValueFromControl, value(), placeholderValue(), localeIdentifier(), isFieldOwnerDisabled/ReadOnly, etc.). If author script changes the input’s type while such an event is being handled (e.g. inside an ‘input’ event handler), the BaseDateAndTimeInputType owner is destroyed as the shadow subtree is rebuilt, while the DateTimeEditElement can still invoke m_editControlOwner — dereferencing a freed owner (use-after-free).

The fix makes DateTimeEditElementEditControlOwner derive from AbstractRefCountedAndCanMakeWeakPtr (so it can be ref-counted and weak-tracked), adds ref()/deref() forwarding on BaseDateAndTimeInputType, and changes every call site to upgrade the weak owner to a RefPtr first (if (RefPtr editControlOwner = m_editControlOwner) …), which both null-checks (owner already gone) and keeps the owner alive across the callback. setupDateTimeChooserParameters also converts an ASSERT(element()) into a real null-check returning false.

The restored invariant is that the shadow edit element never calls into an owner that may have been destroyed; it holds a strong reference across each use. The regression test focuses a date input, types digits so an ‘input’ event fires, and in that handler sets input.type=‘text’ (destroying the date input type), expecting no crash.

Key insight
The date-field shadow element called into its input-type owner through a non-owning weak/raw reference, so changing the input’s type mid-event freed the owner while it was still being used; making the owner ref-counted and upgrading to a RefPtr across each call closes the UAF.

Attack Path

  1. Focus a date input Create <input type=date>, focus it, and edit its fields to generate field/input events routed to the DateTimeEditElement.
  2. Change type mid-event In an input-event handler, set input.type=‘text’, destroying the BaseDateAndTimeInputType owner and rebuilding the shadow subtree.
  3. Call the freed owner The DateTimeEditElement continues to invoke m_editControlOwner (value()/didChangeValueFromControl/etc.) on the destroyed owner.
  4. Use-after-free The dangling owner dereference corrupts/crashes the WebContent process.

Impact Assessment

A use-after-free in the WebContent process reachable from ordinary HTML/JS by changing a date input’s type during an input/field event. Type-change-during-event UAFs are controllable and groomable; the advisory rates it a crash, but the class is a common route to memory disclosure/corruption and potential code execution in WebContent.

Changed Functions

FunctionChangeNotes
DateTimeEditElementEditControlOwner
Source/WebCore/html/shadow/DateTimeEditElement.h
modified
Locale
Source/WebCore/html/shadow/DateTimeEditElement.h
modified

Files Changed

  • LayoutTests/fast/forms/date/date-editable-components/date-editable-components-change-type-on-input-event-expected.txt
  • LayoutTests/fast/forms/date/date-editable-components/date-editable-components-change-type-on-input-event.html
  • Source/WebCore/html/BaseDateAndTimeInputType.cpp
  • Source/WebCore/html/BaseDateAndTimeInputType.h
  • Source/WebCore/html/shadow/DateTimeEditElement.cpp
  • Source/WebCore/html/shadow/DateTimeEditElement.h

Audit Directions

  • Same file: remaining owner uses
    Confirm every m_editControlOwner dereference in DateTimeEditElement.cpp goes through a RefPtr upgrade, including any picker/chooser paths.
  • Shadow owner back-references
    Grep WebCore/html/shadow for members holding an owning InputType/element as raw or CanMakeWeakPtr and dereferenced from event handlers; type changes can free them.
diff --git a/Source/WebCore/html/BaseDateAndTimeInputType.cpp b/Source/WebCore/html/BaseDateAndTimeInputType.cpp
index dac2e40936bb..a1c91bb15fd1 100644
--- a/Source/WebCore/html/BaseDateAndTimeInputType.cpp
+++ b/Source/WebCore/html/BaseDateAndTimeInputType.cpp
@@ -650,9 +650,10 @@ void BaseDateAndTimeInputType::didEndChooser()
 
 bool BaseDateAndTimeInputType::setupDateTimeChooserParameters(DateTimeChooserParameters& parameters)
 {
-    ASSERT(element());
+    RefPtr element = this->element();
+    if (!element)
+        return false;
 
-    Ref element = *this->element();
     Ref document = element->document();
 
     if (!document->view())
diff --git a/Source/WebCore/html/BaseDateAndTimeInputType.h b/Source/WebCore/html/BaseDateAndTimeInputType.h
index 1320307f2af9..3cffb02f4731 100644
--- a/Source/WebCore/html/BaseDateAndTimeInputType.h
+++ b/Source/WebCore/html/BaseDateAndTimeInputType.h
@@ -55,6 +55,9 @@ public:
     bool typeMismatch() const final;
     bool hasBadInput() const final;
 
+    void ref() const final { InputType::ref(); }
+    void deref() const final { InputType::deref(); }
+
 protected:
     enum class DateTimeFormatValidationResults : uint8_t {
         HasYear = 1 << 0,
diff --git a/Source/WebCore/html/shadow/DateTimeEditElement.cpp b/Source/WebCore/html/shadow/DateTimeEditElement.cpp
index 9a0788a123c9..3f1bea1dc35d 100644
--- a/Source/WebCore/html/shadow/DateTimeEditElement.cpp
+++ b/Source/WebCore/html/shadow/DateTimeEditElement.cpp
@@ -236,9 +236,10 @@ size_t DateTimeEditElement::fieldIndexOf(const DateTimeFieldElement& fieldToFind
 void DateTimeEditElement::defaultEventHandler(Event& event)
 {
     if (RefPtr keyboardEvent = dynamicDowncast<KeyboardEvent>(event)) {
-        if (keyboardEvent->keyIdentifier() == "U+0020"_s && m_editControlOwner) {
+        RefPtr editControlOwner = m_editControlOwner;
+        if (editControlOwner && keyboardEvent->keyIdentifier() == "U+0020"_s) {
             // Forward space keypresses to the owner to activate the date picker.
-            m_editControlOwner->didReceiveSpaceKeyFromControl();
+            editControlOwner->didReceiveSpaceKeyFromControl();
             // We want to mark the event as handled to avoid scrolling the page.
             event.setDefaultHandled();
             return;
@@ -314,7 +315,8 @@ void DateTimeEditElement::layout(const LayoutParameters& layoutParameters)
 
 void DateTimeEditElement::didBlurFromField(Event& event)
 {
-    if (!m_editControlOwner)
+    RefPtr editControlOwner = m_editControlOwner;
+    if (!editControlOwner)
         return;
 
     if (RefPtr newFocusedElement = event.relatedTarget()) {
@@ -326,13 +328,13 @@ void DateTimeEditElement::didBlurFromField(Event& event)
             return;
     }
 
-    m_editControlOwner->didBlurFromControl();
+    editControlOwner->didBlurFromControl();
 }
 
 void DateTimeEditElement::fieldValueChanged()
 {
-    if (m_editControlOwner)
-        m_editControlOwner->didChangeValueFromControl();
+    if (RefPtr editControlOwner = m_editControlOwner)
+        editControlOwner->didChangeValueFromControl();
 }
 
 bool DateTimeEditElement::focusOnNextFocusableField(size_t startIndex)
@@ -382,12 +384,16 @@ bool DateTimeEditElement::focusOnPreviousField(const DateTimeFieldElement& field
 
 bool DateTimeEditElement::isFieldOwnerDisabled() const
 {
-    return m_editControlOwner && m_editControlOwner->isEditControlOwnerDisabled();
+    if (RefPtr editControlOwner = m_editControlOwner)
+        return editControlOwner->isEditControlOwnerDisabled();
+    return false;
 }
 
 bool DateTimeEditElement::isFieldOwnerReadOnly() const
 {
-    return m_editControlOwner && m_editControlOwner->isEditControlOwnerReadOnly();
+    if (RefPtr editControlOwner = m_editControlOwner)
+        return editControlOwner->isEditControlOwnerReadOnly();
+    return false;
 }
 
 bool DateTimeEditElement::isFieldOwnerHorizontal() const
@@ -399,18 +405,22 @@ bool DateTimeEditElement::isFieldOwnerHorizontal() const
 
 bool DateTimeEditElement::didFieldOwnerTransferFocusToPicker()
 {
-    return m_editControlOwner && m_editControlOwner->didEditControlOwnerTransferFocusToPicker();
+    if (RefPtr editControlOwner = m_editControlOwner)
+        return editControlOwner->didEditControlOwnerTransferFocusToPicker();
+    return false;
 }
 
 void DateTimeEditElement::didSuppressBlurDueToPickerFocusTransfer()
 {
-    if (m_editControlOwner)
-        m_editControlOwner->didSuppressBlurDueToPickerFocusTransfer();
+    if (RefPtr editControlOwner = m_editControlOwner)
+        editControlOwner->didSuppressBlurDueToPickerFocusTransfer();
 }
 
 AtomString DateTimeEditElement::localeIdentifier() const
 {
-    return m_editControlOwner ? m_editControlOwner->localeIdentifier() : nullAtom();
+    if (RefPtr editControlOwner = m_editControlOwner)
+        return editControlOwner->localeIdentifier();
+    return nullAtom();
 }
 
 const GregorianDateTime& DateTimeEditElement::placeholderDate() const
@@ -439,12 +449,16 @@ void DateTimeEditElement::setEmptyValue(const LayoutParameters& layoutParameters
 
 String DateTimeEditElement::value() const
 {
-    return m_editControlOwner ? m_editControlOwner->formatDateTimeFieldsState(valueAsDateTimeFieldsState()) : emptyString();
+    if (RefPtr editControlOwner = m_editControlOwner)
+        return editControlOwner->formatDateTimeFieldsState(valueAsDateTimeFieldsState());
+    return emptyString();
 }
 
 String DateTimeEditElement::placeholderValue() const
 {
-    return m_editControlOwner ? m_editControlOwner->formatDateTimeFieldsState(valueAsDateTimeFieldsState(DateTimePlaceholderIfNoValue::Yes)) : emptyString();
+    if (RefPtr editControlOwner = m_editControlOwner)
+        return editControlOwner->formatDateTimeFieldsState(valueAsDateTimeFieldsState(DateTimePlaceholderIfNoValue::Yes));
+    return emptyString();
 }
 
 DateTimeFieldsState DateTimeEditElement::valueAsDateTimeFieldsState(DateTimePlaceholderIfNoValue placeholderIfNoValue) const
diff --git a/Source/WebCore/html/shadow/DateTimeEditElement.h b/Source/WebCore/html/shadow/DateTimeEditElement.h
index b19211249c43..f90afb8ec710 100644
--- a/Source/WebCore/html/shadow/DateTimeEditElement.h
+++ b/Source/WebCore/html/shadow/DateTimeEditElement.h
@@ -28,24 +28,14 @@
 
 #include "DateComponents.h"
 #include "DateTimeFieldElement.h"
-
+#include <wtf/AbstractRefCountedAndCanMakeWeakPtr.h>
 #include <wtf/CheckedRef.h>
-#include <wtf/WeakPtr.h>
-
-namespace WebCore {
-class DateTimeEditElementEditControlOwner;
-}
-
-namespace WTF {
-template<typename T> struct IsDeprecatedWeakRefSmartPointerException;
-template<> struct IsDeprecatedWeakRefSmartPointerException<WebCore::DateTimeEditElementEditControlOwner> : std::true_type { };
-}
 
 namespace WebCore {
 
 class Locale;
 
-class DateTimeEditElementEditControlOwner : public CanMakeWeakPtr<DateTimeEditElementEditControlOwner> {
+class DateTimeEditElementEditControlOwner : public AbstractRefCountedAndCanMakeWeakPtr<DateTimeEditElementEditControlOwner> {
 public:
     virtual ~DateTimeEditElementEditControlOwner();
     virtual void didBlurFromControl() = 0;
diff --git a/LayoutTests/fast/forms/date/date-editable-components/date-editable-components-change-type-on-input-event-expected.txt b/LayoutTests/fast/forms/date/date-editable-components/date-editable-components-change-type-on-input-event-expected.txt
new file mode 100644
index 000000000000..d71615565c02
--- /dev/null
+++ b/LayoutTests/fast/forms/date/date-editable-components/date-editable-components-change-type-on-input-event-expected.txt
@@ -0,0 +1,11 @@
+Test that changing the input type during an input event does not crash.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS input.type is "text"
+PASS Did not crash.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/fast/forms/date/date-editable-components/date-editable-components-change-type-on-input-event.html b/LayoutTests/fast/forms/date/date-editable-components/date-editable-components-change-type-on-input-event.html
new file mode 100644
index 000000000000..79d7c2bae618
--- /dev/null
+++ b/LayoutTests/fast/forms/date/date-editable-components/date-editable-components-change-type-on-input-event.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../../../resources/js-test.js"></script>
+<script src="../../../../resources/ui-helper.js"></script>
+</head>
+<body>
+
+<input id="input" type="date">
+
+<script>
+
+jsTestIsAsync = true;
+
+description("Test that changing the input type during an input event does not crash.");
+
+input.addEventListener("input", () => {
+    input.type = "text";
+});
+
+addEventListener("load", async () => {
+    input.focus();                                         // [mm]/dd/yyyy
+    UIHelper.keyDown("9");                                 // -> [09]/dd/yyyy
+    UIHelper.keyDown("rightArrow");                        // -> 09/[dd]/yyyy
+    UIHelper.keyDown("2");                                 // -> 09/[02]/yyyy
+    UIHelper.keyDown("rightArrow");                        // -> 09/02/[yyyy]
+    UIHelper.keyDown("2");                                 // -> 09/02/[0002]
+    UIHelper.keyDown("0");                                 // -> 09/02/[0020]
+    UIHelper.keyDown("1");                                 // -> 09/02/[0201]
+    UIHelper.keyDown("2");                                 // -> 09/02/[2012] -> input event fires -> type changes to text
+
+    shouldBeEqualToString("input.type", "text");
+
+    testPassed("Did not crash.");
+    finishJSTest();
+});
+
+</script>
+</body>
+</html>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/LayoutTests/fast/forms/date/date-editable-components/date-editable-components-change-type-on-input-event-expected.txt b/LayoutTests/fast/forms/date/date-editable-components/date-editable-components-change-type-on-input-event-expected.txt
new file mode 100644
index 000000000000..d71615565c02
--- /dev/null
+++ b/LayoutTests/fast/forms/date/date-editable-components/date-editable-components-change-type-on-input-event-expected.txt
@@ -0,0 +1,11 @@
+Test that changing the input type during an input event does not crash.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS input.type is "text"
+PASS Did not crash.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/fast/forms/date/date-editable-components/date-editable-components-change-type-on-input-event.html b/LayoutTests/fast/forms/date/date-editable-components/date-editable-components-change-type-on-input-event.html
new file mode 100644
index 000000000000..79d7c2bae618
--- /dev/null
+++ b/LayoutTests/fast/forms/date/date-editable-components/date-editable-components-change-type-on-input-event.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../../../resources/js-test.js"></script>
+<script src="../../../../resources/ui-helper.js"></script>
+</head>
+<body>
+
+<input id="input" type="date">
+
+<script>
+
+jsTestIsAsync = true;
+
+description("Test that changing the input type during an input event does not crash.");
+
+input.addEventListener("input", () => {
+    input.type = "text";
+});
+
+addEventListener("load", async () => {
+    input.focus();                                         // [mm]/dd/yyyy
+    UIHelper.keyDown("9");                                 // -> [09]/dd/yyyy
+    UIHelper.keyDown("rightArrow");                        // -> 09/[dd]/yyyy
+    UIHelper.keyDown("2");                                 // -> 09/[02]/yyyy
+    UIHelper.keyDown("rightArrow");                        // -> 09/02/[yyyy]
+    UIHelper.keyDown("2");                                 // -> 09/02/[0002]
+    UIHelper.keyDown("0");                                 // -> 09/02/[0020]
+    UIHelper.keyDown("1");                                 // -> 09/02/[0201]
+    UIHelper.keyDown("2");                                 // -> 09/02/[2012] -> input event fires -> type changes to text
+
+    shouldBeEqualToString("input.type", "text");
+
+    testPassed("Did not crash.");
+    finishJSTest();
+});
+
+</script>
+</body>
+</html>
Loading diff…

Original Bug Report

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