Released June 29, 2026

CVE-2026-43727

CVSS 6.5 severity medium CWE-416 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H

Overview

Impact:

Description: Processing maliciously crafted web content may lead to an unexpected Safari crash

Researchers: Tommy DeVoss from Braze Security Team (@thedawgyg), Gurpreet Shergill, Gia Bui (@yabeow) from Calif.io

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.

Patch Details

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)

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2026-43727
Bugzilla 313691
Component WebCore
Bug Class UAF
Severity medium
Commit c5036aadbde48318…
Advisory Apple Advisory

Terminology

  • 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

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).

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.

Changed Functions

Function File Change Note
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

Source Files

  • Source/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp

Test Files

  • LayoutTests/editing/async-clipboard/clipboard-write-item-crash-expected.txt
  • LayoutTests/editing/async-clipboard/clipboard-write-item-crash.html

Regression Test

The fix ships this proof-of-concept / regression test:

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>