Medium CVSS 6.5 webkit UAF 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected Safari crash
ComponentWebCore Async-clipboard
Bug ClassUAF
Tracker313691
Fix commitc5036aadbde4 (WebKit/WebKit) +30/-3
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
CISA KEVNot listed
CreditedTommy DeVoss from Braze Security Team (@thedawgyg), Gurpreet Shergill, Gia Bui (@yabeow) from Calif.io
Disclosed2026-06-29

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.

Key insight
Invoking completion handlers while the loaders were still owned by the member vector let a re-entrant call free or mutate that vector mid-iteration; moving it out first with std::exchange closes the re-entrancy.

Attack Path

  1. 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.
  2. Start the first write Call navigator.clipboard.write([item]) to spin up item-type loaders held in m_itemTypeLoaders.
  3. Drain microtasks await 0 so the resolved-type completion handlers run and re-enter the clipboard data source.
  4. 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.
  5. 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

A re-entrancy use-after-free of an item-type-loader object reachable from ordinary page script via the async clipboard API. The attacker controls timing through Promise resolution and microtask draining, but the freed object and reuse window are constrained, so the realistic outcome is a controlled crash, with UAF-to-corruption requiring additional heap grooming. Confined to the WebContent process; rated medium (CVSS 6.5).

Changed Functions

FunctionChangeNotes
ClipboardItemBindingsDataSource::clearItemTypeLoaders
Source/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.txt
  • LayoutTests/editing/async-clipboard/clipboard-write-item-crash.html
  • Source/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp

Audit Directions

  • Same idiom in this file
    Review 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 pattern
    grep 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 paths
    Audit pasteboard/clipboard completion handlers that can settle Promises and re-enter the same data source during another write/read.
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)
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker.