CVE-2026-28942
Overview
Background
- Invoker commands
- HTML button command/commandfor lets a button drive a dialog (close/requestClose); the button’s value is passed as the dialog returnValue.
- beforetoggle event
- An author-observable event fired as a dialog opens/closes; its handler can run arbitrary script and mutate the DOM mid-operation.
- Attribute-backed string lifetime
- A string taken from an element’s attribute can be invalidated if the attribute is changed while the string is still in use.
Root Cause Analysis
This fixes a use-after-free (observed as a crash / ASan hit) in HTMLDialogElement command handling by snapshotting the invoker’s value string before dispatching author-observable events. HTMLDialogElement::handleCommandInternal, for the Close and RequestClose commands, called close(invoker.value().string(), &invoker) / requestClose(invoker.value().string(), &invoker) — passing a string derived from the invoker button’s value attribute directly as an argument while also passing &invoker. close()/requestClose() dispatch events (e.g. beforetoggle/close) that run author script; that script can mutate or remove the invoker button’s value attribute during the call. Because the string was obtained from the invoker’s attribute storage as part of the call, author code mutating that attribute mid-close can invalidate the underlying string data the operation is still using, yielding a use-after-free of the attribute-backed string.
The fix hoists the value into an owned local first (String value = invoker.value().string();) and passes that local to close(value, &invoker) / requestClose(value, &invoker), so the value is captured before any event fires and cannot be invalidated by author script that alters the invoker during the close sequence.
The restored invariant is that the command value is a stable snapshot taken before author-observable side effects run. The regression test opens a dialog, and in a beforetoggle handler removes the button’s value attribute while the close command runs, expecting no crash and PASS returnValue.
Attack Path
- Set up a dialog + invoker Create an open <dialog> with a <button command=close commandfor=dialog> and a value attribute.
- Hook the close event Add a beforetoggle listener that removes/mutates the button’s value attribute.
- Invoke close Click the button so handleCommandInternal reads invoker.value().string() and calls close(), firing beforetoggle.
- Free during use The handler mutates the invoker’s value attribute mid-close, invalidating the in-use string — a use-after-free in WebContent.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifSource/WebCore/html/HTMLDialogElement.cpp |
modified |
Files Changed
LayoutTests/fast/html/dialog-close-from-button-crash-expected.txtLayoutTests/fast/html/dialog-close-from-button-crash.htmlSource/WebCore/html/HTMLDialogElement.cpp
Audit Directions
- Same file: other command handlersAudit HTMLDialogElement and other command/invoker handlers for element-derived strings passed into calls that dispatch events; snapshot before dispatch.
- Mutate-during-event patternGrep for value()/getAttribute() results passed directly into methods that fire events (dispatchEvent, toggle, focus) where author script can invalidate them.
Patch
diff --git a/Source/WebCore/html/HTMLDialogElement.cpp b/Source/WebCore/html/HTMLDialogElement.cpp
index 0b39c2a2f885..e0ef22618ef9 100644
--- a/Source/WebCore/html/HTMLDialogElement.cpp
+++ b/Source/WebCore/html/HTMLDialogElement.cpp
@@ -305,11 +305,13 @@ bool HTMLDialogElement::handleCommandInternal(HTMLButtonElement& invoker, const
if (isOpen()) {
if (command == CommandType::Close) {
- close(invoker.value().string(), &invoker);
+ String value = invoker.value().string();
+ close(value, &invoker);
return true;
}
if (command == CommandType::RequestClose) {
- requestClose(invoker.value().string(), &invoker);
+ String value = invoker.value().string();
+ requestClose(value, &invoker);
return true;
}
} else {
diff --git a/LayoutTests/fast/html/dialog-close-from-button-crash-expected.txt b/LayoutTests/fast/html/dialog-close-from-button-crash-expected.txt
new file mode 100644
index 000000000000..d6d0936dfab3
--- /dev/null
+++ b/LayoutTests/fast/html/dialog-close-from-button-crash-expected.txt
@@ -0,0 +1,4 @@
+This tests removing the value attribute of a button as it tries to close the dialog element.
+WebKit should not crash or hit any assertions under ASAN.
+
+PASS
diff --git a/LayoutTests/fast/html/dialog-close-from-button-crash.html b/LayoutTests/fast/html/dialog-close-from-button-crash.html
new file mode 100644
index 000000000000..ac956c9a8823
--- /dev/null
+++ b/LayoutTests/fast/html/dialog-close-from-button-crash.html
@@ -0,0 +1,21 @@
+<!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>
Regression Test / PoC
diff --git a/LayoutTests/fast/html/dialog-close-from-button-crash-expected.txt b/LayoutTests/fast/html/dialog-close-from-button-crash-expected.txt
new file mode 100644
index 000000000000..d6d0936dfab3
--- /dev/null
+++ b/LayoutTests/fast/html/dialog-close-from-button-crash-expected.txt
@@ -0,0 +1,4 @@
+This tests removing the value attribute of a button as it tries to close the dialog element.
+WebKit should not crash or hit any assertions under ASAN.
+
+PASS
diff --git a/LayoutTests/fast/html/dialog-close-from-button-crash.html b/LayoutTests/fast/html/dialog-close-from-button-crash.html
new file mode 100644
index 000000000000..ac956c9a8823
--- /dev/null
+++ b/LayoutTests/fast/html/dialog-close-from-button-crash.html
@@ -0,0 +1,21 @@
+<!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>