70753442a3 Use-after-free in HTMLDialogElement::close
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
- Activate — Script clicks a command=close button inside an open <dialog>; handleCommandInternal takes the Close branch.
- Bind — Pre-patch, invoker.value().string() is passed inline to close(), tied to the button attribute’s live backing buffer.
- Re-entrancy — close() dispatches beforetoggle synchronously; the page’s handler calls button.removeAttribute(‘value’), freeing the attribute’s string buffer.
- UAF — close() continues and reads the now-freed value to set dialog.returnValue - use-after-free (ASan crash).
- 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
- Trigger — Fully deterministic from unprivileged HTML/JS: open dialog + command=close button + beforetoggle handler removing the button’s value attribute.
- Free — removeAttribute(‘value’) during beforetoggle frees the attribute’s string backing buffer mid-close.
- 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 —