← WebKit Silent-Fix Report — 2026-W22

869d5c5531  Use-After-Free in `BaseDateAndTimeInputType::didChangeValueFromControl`

severity high class UAF confidence 0.80 WebCore forms date/time input exploitable-grade
Aditya Keerthi Fri May 29 12:23:17 2026 -0700 full: 869d5c55313783da5714584b7db649435dbb16b0 bug report ↗ view on GitHub ↗
Primitive: input type changed during input event frees element
Triage note: Changing input.type during the input event destroys the input type object; null-checking element() before use fixes a reentrancy UAF.
Contents

The bug at a glance

High. BaseDateAndTimeInputType::didChangeValueFromControl dispatches an input event during date/time editing. A script input handler can set input.type = ’text’, which replaces HTMLInputElement::m_inputType and destroys the BaseDateAndTimeInputType (and its DateTimeEditElement owner) mid-call; execution then continues on the freed object through setupDateTimeChooserParameters/showDateTimeChooser, a use-after-free directly reachable from web content via a normal input event. rdar-backed with a crash regression test.

The input type object is owned by HTMLInputElement::m_inputType and is not ref-protected across the synchronous input-event dispatch it triggers. Reentrancy through the event handler swaps the input type (a documented way to free the current InputType), so any use of this / element() after the dispatch touches freed memory. The DateTimeEditElement’s edit-control owner was only WeakPtr-tracked, not ref-counted, so nothing kept it alive.

Root cause

Editing a date/time input’s fields flows through DateTimeEditElement, whose m_editControlOwner points at the BaseDateAndTimeInputType (the InputType implementation). When a field value changes, DateTimeEditElement::fieldValueChanged() calls m_editControlOwner->didChangeValueFromControl(), which synchronously dispatches an ‘input’ DOM event. During that dispatch a page script handler runs.

The HTMLInputElement’s concrete behavior is provided by m_inputType, an owning pointer to an InputType subclass (here BaseDateAndTimeInputType). Setting input.type to a different value causes HTMLInputElement to replace m_inputType, destroying the previous BaseDateAndTimeInputType instance. Because didChangeValueFromControl (and the surrounding DateTimeEditElement plumbing) did not hold a strong reference to the input type across the event dispatch, the object is freed while its own method is still on the stack. When control returns, the code continues into setupDateTimeChooserParameters() and showDateTimeChooser() operating on the freed this / a stale element(), a use-after-free.

The fix has two parts. First, DateTimeEditElementEditControlOwner is changed from CanMakeWeakPtr to AbstractRefCountedAndCanMakeWeakPtr, making the owner (the input type) ref-countable, and the IsDeprecatedWeakRefSmartPointerException specialization is removed. BaseDateAndTimeInputType forwards ref()/deref() to InputType. Now DateTimeEditElement methods take a strong RefPtr to m_editControlOwner before calling into it: fieldValueChanged does ‘if (RefPtr editControlOwner = m_editControlOwner) editControlOwner->didChangeValueFromControl();’, and defaultEventHandler/didBlurFromField/isFieldOwnerDisabled/isFieldOwnerReadOnly/didFieldOwnerTransferFocusToPicker/didSuppressBlurDueToPickerFocusTransfer/localeIdentifier/value/placeholderValue are all converted to acquire a local RefPtr first. This keeps the BaseDateAndTimeInputType alive across the synchronous event dispatch even if the page swaps input.type.

Second, setupDateTimeChooserParameters is hardened defensively: it replaces ‘ASSERT(element()); Ref element = *this->element();’ with ‘RefPtr element = this->element(); if (!element) return false;’, so that if the element/type has already been torn down the function bails out gracefully rather than dereferencing null/garbage. Together, the strong ref prevents the object from being freed during the event, and the null-check defends the remaining path.

Key code

Owner protected by a strong ref across the input-event dispatch (DateTimeEditElement.cpp)

void DateTimeEditElement::fieldValueChanged()
{
    if (RefPtr editControlOwner = m_editControlOwner)
        editControlOwner->didChangeValueFromControl();
}

Patch walkthrough

  • Source/WebCore/html/shadow/DateTimeEditElement.cpp — Every use of m_editControlOwner is guarded by first taking a local ‘RefPtr editControlOwner = m_editControlOwner;’. The critical one is fieldValueChanged(), which now protects the owner (the BaseDateAndTimeInputType) with a strong ref before calling didChangeValueFromControl(), so the synchronous input-event dispatch cannot free it mid-call. defaultEventHandler, didBlurFromField, isFieldOwnerDisabled/ReadOnly, didFieldOwnerTransferFocusToPicker, didSuppressBlurDueToPickerFocusTransfer, localeIdentifier, value and placeholderValue are converted identically.
  • Source/WebCore/html/shadow/DateTimeEditElement.h — DateTimeEditElementEditControlOwner’s base changes from CanMakeWeakPtr<…> to AbstractRefCountedAndCanMakeWeakPtr<…>, and the WTF IsDeprecatedWeakRefSmartPointerException<DateTimeEditElementEditControlOwner> specialization is removed, so the owner is now ref-countable and can be protected by RefPtr.
  • Source/WebCore/html/BaseDateAndTimeInputType.h — Adds ‘void ref() const final { InputType::ref(); }’ and ‘void deref() const final { InputType::deref(); }’ so the input type participates in the ref-counting demanded by the new owner base class.
  • Source/WebCore/html/BaseDateAndTimeInputType.cpp — setupDateTimeChooserParameters replaces the ASSERT(element()) + Ref element = *this->element() with ‘RefPtr element = this->element(); if (!element) return false;’, bailing out safely if the element is gone after a type change rather than dereferencing freed/null state.
  • LayoutTests/fast/forms/date/date-editable-components/date-editable-components-change-type-on-input-event.html — New regression test: a date input whose ‘input’ handler sets input.type=‘text’; keystrokes complete the date, firing the input event and swapping the type, and the test asserts no crash and input.type===‘text’.

Background

HTMLInputElement::m_inputType — An owning pointer to the InputType implementation for an input element; assigning input.type replaces it, destroying the prior InputType object synchronously.

BaseDateAndTimeInputType — The InputType subclass backing date/time inputs; it drives the shadow DateTimeEditElement and opens the date/time chooser.

DateTimeEditElementEditControlOwner — Interface (implemented by the input type) that the shadow edit element calls back into; changed here from weak-only to ref-countable so callers can protect it.

Reentrancy via input event — Dispatching a DOM event synchronously runs page script that can mutate the DOM (including input.type), a classic source of use-after-free when the dispatching object is not ref-protected.

Vulnerability window

  1. Edit — User/script edits a date input field, causing DateTimeEditElement::fieldValueChanged().
  2. Dispatch — didChangeValueFromControl() synchronously dispatches an ‘input’ event.
  3. Reentrancy — A page input handler sets input.type = ’text’.
  4. Free — HTMLInputElement replaces m_inputType, destroying the BaseDateAndTimeInputType currently on the stack.
  5. UAF — Control returns and continues into setupDateTimeChooserParameters/showDateTimeChooser on the freed object/stale element().
  6. Fix — A RefPtr protects the owner across dispatch and setupDateTimeChooserParameters null-checks element().

Proof of concept

Verbatim body of date-editable-components-change-type-on-input-event.html. A type=date input has an ‘input’ handler that flips input.type to ’text’. Completing the last digit of the date fires the input event mid-edit, which pre-patch frees the BaseDateAndTimeInputType while didChangeValueFromControl/setupDateTimeChooserParameters is still executing. The test verifies no crash and that the type actually changed.

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

Exploitation

  1. Reach — Pure web content: a date/time input with an ‘input’ event listener that reassigns input.type triggers the free during the synchronous dispatch.
  2. Groom — Arrange heap so the freed InputType slot is reclaimed by an attacker-controlled object before the continued setupDateTimeChooserParameters/showDateTimeChooser use.
  3. Primitive — Continued virtual/member access on the freed object can yield a controlled call or type confusion, escalatable to R/W within the WebContent process.
  4. Reliability — Because the free and reuse happen deterministically within one synchronous event, exploitation is more reliable than a threaded race.

Detection & hunting

For defenders and SOC / detection engineers:

  • Crash after input.type change during input/change event
  • Unprotected this across event dispatch

Audit directions

  • Other InputType event dispatchers
  • WeakPtr-only owner callbacks
  • element() lifetime after reentrancy

Before / after

Loading diff…