← WebKit Silent-Fix Report — 2026-W25

70753442a3  Use-after-free in HTMLDialogElement::close

severity medium class UAF confidence 0.80 WebCore HTML HTMLDialogElement exploitable-grade
Ryosuke Niwa Tue Jun 16 13:59:46 2026 -0700 full: 70753442a3d86ab5085352b3f3a8072c6733c7f3 bug report ↗ view on GitHub ↗
Primitive: dangling reference to freed attribute value
Triage note: PoC removes the button's value attribute during beforetoggle; snapshotting the string first fixes a use-after-free on the attribute value.
Contents

The bug at a glance

A use-after-free reachable synchronously from a trivial page: a button with command=close inside an open <dialog> whose beforetoggle handler removes the button’s value attribute. The freed memory is the backing store of the invoker’s value string, dereferenced during dialog close. Medium reflects that the added test demonstrates only a crash/ASan hit rather than a controlled read-write primitive, though it is deterministically triggerable from unprivileged script.

HTMLDialogElement::handleCommandInternal passed invoker.value().string() straight into close()/requestClose(). Firing the dialog’s beforetoggle event during close lets a script handler remove the button’s value attribute, freeing the string backing that reference while close() still uses it - a use-after-free.

Root cause

When a command button (command=close/requestClose, commandfor a dialog) is activated, HTMLDialogElement::handleCommandInternal(HTMLButtonElement& invoker, …) runs. For CommandType::Close it previously called close(invoker.value().string(), &invoker) and for CommandType::RequestClose it called requestClose(invoker.value().string(), &invoker).

invoker.value() returns the button’s value attribute and .string() yields a String whose backing buffer is owned by the attribute. Passing it inline means the argument is bound as a reference/temporary tied to the attribute’s live storage. Inside close(), WebKit fires the dialog’s beforetoggle event synchronously. A script handler for that event can call button.removeAttribute(‘value’), which drops the attribute and frees the underlying string buffer. When close() subsequently reads that value (to set dialog.returnValue), it dereferences freed memory - a use-after-free.

The fix is minimal and idiomatic: snapshot the value into a local before the call, String value = invoker.value().string(); then close(value, &invoker); (and the same for requestClose). The local String holds its own reference to the buffer for the duration of the close operation, so a handler that removes the attribute mid-close no longer invalidates the value being used. The added layout test dialog-close-from-button-crash.html sets the button value to ‘PASS’, removes it inside a beforetoggle listener, clicks the button, and writes dialog.returnValue, expecting no crash under ASan.

Key code

Snapshot the invoker value into a local before closing the dialog

    if (isOpen()) {
        if (command == CommandType::Close) {
            String value = invoker.value().string();
            close(value, &invoker);
            return true;
        }
        if (command == CommandType::RequestClose) {
            String value = invoker.value().string();
            requestClose(value, &invoker);
            return true;
        }
    } else {

Patch walkthrough

  • Source/WebCore/html/HTMLDialogElement.cpp — In handleCommandInternal, both the Close and RequestClose branches now copy invoker.value().string() into a local String value before calling close(value, &invoker) / requestClose(value, &invoker), instead of passing the temporary inline. This gives the value its own owning reference across the synchronous beforetoggle dispatch that close/requestClose trigger.
  • LayoutTests/fast/html/dialog-close-from-button-crash.html — Added regression test: an open dialog with a command=close button; the button’s value is set to ‘PASS’, a beforetoggle handler removes the value attribute, the button is clicked, and dialog.returnValue is written out. Under ASan the old code faulted; the fix yields PASS.
  • LayoutTests/fast/html/dialog-close-from-button-crash-expected.txt — Added expected output containing PASS, asserting WebKit neither crashes nor hits assertions under ASan.

Background

Command/commandfor invoker buttons — The HTML invoker commands feature lets a <button command=close commandfor=dialogId> close a dialog declaratively. Activating it routes through HTMLDialogElement::handleCommandInternal with the button as the invoker, and the button’s value attribute is used as the dialog’s close return value.

beforetoggle event — Dialogs fire a beforetoggle event as they open/close. It dispatches synchronously during close(), giving page script a re-entrancy point in which it can mutate the DOM - including removing the very attribute whose value is being consumed by the close operation.

WTF::String backing and attribute values — HTMLButtonElement::value() returns an AtomString/attribute-backed value; calling .string() produces a String sharing that buffer. If the attribute is removed, its owning reference drops and the buffer can be freed. Holding an independent String local retains a reference so the buffer stays alive.

dialog.returnValue — When a dialog closes, its returnValue is set from the value passed to close(). Reading the invoker value after the beforetoggle handler ran is exactly where the freed string was dereferenced, which the test surfaces by writing document.write(dialog.returnValue).

Vulnerability window

  1. Activate — Script clicks a command=close button inside an open <dialog>; handleCommandInternal takes the Close branch.
  2. Bind — Pre-patch, invoker.value().string() is passed inline to close(), tied to the button attribute’s live backing buffer.
  3. Re-entrancy — close() dispatches beforetoggle synchronously; the page’s handler calls button.removeAttribute(‘value’), freeing the attribute’s string buffer.
  4. UAF — close() continues and reads the now-freed value to set dialog.returnValue - use-after-free (ASan crash).
  5. Fix — Copying into a local String before calling close keeps the buffer alive across the beforetoggle dispatch; the regression test passes under ASan.

Proof of concept

Verbatim added layout test. It creates an open dialog with a command=close button, sets the button’s value to ‘PASS’, registers a beforetoggle handler that removes the value attribute, then clicks the button. On the vulnerable build, close() reads the freed value string (ASan use-after-free); with the fix the local copy survives and dialog.returnValue is ‘PASS’.

<!DOCTYPE html>
<html>
<body>
<p>This tests removing the value attribute of a button as it tries to close the dialog element.<br>
WebKit should not crash or hit any assertions under ASAN.</p>
<dialog id=dialog open>
    <button command=close commandfor=dialog>Close</button>
</dialog>
<script>
window?.testRunner?.dumpAsText();
window?.GCController?.collect();
const button = document.querySelector('button');
button.setAttribute('value', 'PA' + 'SS');
document.querySelector('dialog').addEventListener('beforetoggle', (event) => {
    button.removeAttribute('value');
});
button.click();
document.write(dialog.returnValue);
</script>
</body>
</html>

Exploitation

  1. Trigger — Fully deterministic from unprivileged HTML/JS: open dialog + command=close button + beforetoggle handler removing the button’s value attribute.
  2. Free — removeAttribute(‘value’) during beforetoggle frees the attribute’s string backing buffer mid-close.
  3. Use — close() dereferences the freed string to set returnValue. Impact demonstrated is a crash under ASan; exploiting further would require reallocating the freed String buffer with controlled data, not shown by the test.

Detection & hunting

For defenders and SOC / detection engineers:

  • ASan UAF in HTMLDialogElement::close / handleCommandInternal
  • Attribute mutation inside dialog beforetoggle handlers

Audit directions

  • Values passed across synchronous event dispatch
  • Other HTMLDialogElement/invoker command paths
  • beforetoggle re-entrancy

Before / after

Loading diff…