CVE-2026-43727
Overview
Background
- Async Clipboard API / ClipboardItem
- navigator.clipboard.write() accepts ClipboardItems whose type values may be Promises, so writing runs asynchronously and can interleave with script.
- Item type loader
- A per-type helper that resolves a ClipboardItem type’s Promise and then calls a completion handler; the data source keeps them in m_itemTypeLoaders.
- Re-entrancy
- A completion handler can run author script that calls back into the same object while it is mid-operation, breaking single-pass assumptions.
- std::exchange move-and-clear
- std::exchange(member, {}) atomically moves the member’s contents to a local and resets the member, decoupling later callbacks from the live member.
Root Cause Analysis
ClipboardItemBindingsDataSource::clearItemTypeLoaders() iterated the member vector m_itemTypeLoaders, calling itemTypeLoader->invokeCompletionHandler() on each element, and only cleared the member (m_itemTypeLoaders.clear()) afterwards. invokeCompletionHandler() runs a completion handler that can execute page script and re-enter the async-clipboard machinery — for example resolving/settling another navigator.clipboard.write() — which can call back into clearItemTypeLoaders() (or otherwise mutate or destroy m_itemTypeLoaders) while the outer loop is still iterating that same member vector. The re-entrant clear invalidates the outer iterator and can invoke a completion handler on a loader that was already processed and freed, i.e. a use-after-free.
The fix detaches the collection before running any handler: auto itemTypeLoaders = std::exchange(m_itemTypeLoaders, { }); moves the vector into a local and leaves the member empty, then the loop iterates the local copy. A re-entrant clearItemTypeLoaders() now sees an empty member and is a no-op, and the outer loop walks a stable local vector whose Refs keep the loaders alive for the duration.
The restored invariant is that the set of pending item-type loaders is detached from the object before any completion handler (which may run script and re-enter) is invoked. The layout test drives this by writing a ClipboardItem whose text/plain is a never-resolving Promise, then calling navigator.clipboard.write([item]) twice with a microtask drain in between so the second write clears loaders from the first while a handler re-enters.
Attack Path
- Build a half-pending item Create new ClipboardItem({ ’text/plain’: new Promise(()=>{}), ’text/html’: Promise.resolve(‘x’), ’text/uri-list’: Promise.resolve(‘http://a/’) }) so some type loaders complete and one stays pending.
- Start the first write Call navigator.clipboard.write([item]) to spin up item-type loaders held in m_itemTypeLoaders.
- Drain microtasks await 0 so the resolved-type completion handlers run and re-enter the clipboard data source.
- Start a second write on the same item Call navigator.clipboard.write([item]) again; clearing the first write’s loaders runs a completion handler that re-enters clearItemTypeLoaders while the outer loop is mid-iteration.
- Use-after-free The re-entrant clear frees loaders the outer loop still references / re-invokes an already-freed loader, corrupting memory in WebContent.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ClipboardItemBindingsDataSource::clearItemTypeLoadersSource/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp |
modified | Uses std::exchange(m_itemTypeLoaders, {}) to move the vector into a local and empty the member before invoking any completion handler, so re-entrant clears are no-ops and the iterated vector is stable. |
Files Changed
LayoutTests/editing/async-clipboard/clipboard-write-item-crash-expected.txtLayoutTests/editing/async-clipboard/clipboard-write-item-crash.htmlSource/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp
Audit Directions
- Same idiom in this fileReview collectDataForWriting/getType and other ClipboardItemBindingsDataSource methods for loops over a member container that call a handler then clear the member — the exact shape just fixed.
- Move-before-callback patterngrep WebCore for loops that iterate a member Vector/HashSet and invoke a handler before
.clear(); these are std::exchange candidates whenever the handler can run script. - Other clipboard completion pathsAudit pasteboard/clipboard completion handlers that can settle Promises and re-enter the same data source during another write/read.
Patch
diff --git a/LayoutTests/editing/async-clipboard/clipboard-write-item-crash-expected.txt b/LayoutTests/editing/async-clipboard/clipboard-write-item-crash-expected.txt
new file mode 100644
index 000000000000..d1f6de04a520
--- /dev/null
+++ b/LayoutTests/editing/async-clipboard/clipboard-write-item-crash-expected.txt
@@ -0,0 +1,3 @@
+This test passes if WebKit does not hit assertions or crash under ASAN.
+
+PASS
diff --git a/LayoutTests/editing/async-clipboard/clipboard-write-item-crash.html b/LayoutTests/editing/async-clipboard/clipboard-write-item-crash.html
new file mode 100644
index 000000000000..aaa046f74ab3
--- /dev/null
+++ b/LayoutTests/editing/async-clipboard/clipboard-write-item-crash.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html><!-- webkit-test-runner [ AsyncClipboardAPIEnabled=true ] -->
+<body><script>
+if (window.testRunner) {
+ testRunner.waitUntilDone();
+ testRunner.dumpAsText();
+}
+
+(async () => {
+ const item = new ClipboardItem({
+ "text/plain": new Promise(() => {}),
+ "text/html": Promise.resolve("x"),
+ "text/uri-list": Promise.resolve("http://a/")
+ });
+
+ navigator.clipboard.write([item]).catch(() => {});
+
+ await 0; // Drain microtasks
+
+ navigator.clipboard.write([item]).catch(() => {});
+
+ document.body.innerHTML = '<p>This test passes if WebKit does not hit assertions or crash under ASAN.</p><p>PASS</p>';
+
+ globalThis.testRunner?.notifyDone();
+})();
+</script>
diff --git a/Source/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp b/Source/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp
index 369a9e27a68a..714a57b763ce 100644
--- a/Source/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp
+++ b/Source/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp
@@ -133,10 +133,9 @@ void ClipboardItemBindingsDataSource::getType(const String& type, Ref<DeferredPr
void ClipboardItemBindingsDataSource::clearItemTypeLoaders()
{
- for (auto& itemTypeLoader : m_itemTypeLoaders)
+ auto itemTypeLoaders = std::exchange(m_itemTypeLoaders, { });
+ for (auto& itemTypeLoader : itemTypeLoaders)
itemTypeLoader->invokeCompletionHandler();
-
- m_itemTypeLoaders.clear();
}
void ClipboardItemBindingsDataSource::collectDataForWriting(Clipboard& destination, CompletionHandler<void(std::optional<PasteboardCustomData>)>&& completion)