56909e17f99a Incorrect process termination when a file-backed blob is posted to another process
Triage note: Introduces retainBlobURLsWhileMessageIsInFlight()/CompletionHandlerCallingScope so the network process holds blob URL handles until the destination dispatches the message, and refuses to file-back a blob whose path the process is not allowed and whose srcURL is unregistered (registerInternalBlobURLOptionallyFileBacked); fixes a blob-lifetime UAF/dangling-reference where the source could revoke a blob before delivery.
The bug at a glance
A blob URL could be revoked/freed by the source while a cross-process postMessage carrying it (MessagePort, BroadcastChannel, or ServiceWorker client paths) was still in flight, leaving the network/destination process with a dangling blob reference when the message is finally dispatched. The patch threads a CompletionHandlerCallingScope (‘blobURLsInFlight’ / ‘messageDispatched’) through every in-flight message path so the blob URL handles are held until the destination actually dispatches the message. This is a blob-lifetime use-after-free / dangling-reference defect across the message-delivery boundary; UAF/high is plausible, though the concrete freed-object and the blob-handle registration are only partially shown in the supplied diff, so the escalation is (inferred).
A delivery-window lifetime bug: postMessage blob transfer assumed the blob stayed alive until delivery, but the only thing tying blob lifetime to delivery was missing, so a source could revoke the blob mid-flight.
Root cause
The invariant is that a blob URL referenced by an in-flight message must stay registered/alive until the message is actually dispatched at the destination. Pre-patch the MessagePort, BroadcastChannel, and ServiceWorker client delivery paths moved the message along without carrying any object that keeps the blob handles alive for the full in-flight duration. The sequence: 1. source posts a message whose SerializedScriptValue carries blob URLs; 2. the message is queued/relayed cross-process (MessagePortChannel::postMessageToRemote appends to m_pendingMessages, or BroadcastChannel::dispatchMessageTo, or SWClientConnection’s post-to-client); 3. the source revokes the blob URL / drops its handle before delivery; 4. the destination finally dispatches the message and resolves the blob URL, now dangling/unregistered.
The patch introduces a CompletionHandlerCallingScope that is captured into each in-flight message and only fires when delivery completes, so holding it keeps the blob URL handles retained for the window. Concretely: WTF::CompletionHandlerCallingScope gains explicit operator bool(). BroadcastChannel::dispatchMessageTo builds a CompletionHandlerCallingScope that re-posts the original completionHandler to the main thread, and dispatchMessage now takes CompletionHandlerCallingScope&& messageDispatched and captures it into the queueTaskKeepingObjectAlive lambda. MessagePortChannel::postMessageToRemote takes CompletionHandlerCallingScope&& blobURLsInFlight and stores it beside each message: m_pendingMessages becomes std::array<Vector<PendingMessage>,2> with using PendingMessage = std::pair<MessageWithMessagePorts, CompletionHandlerCallingScope>; takeAllMessagesForPort splits the pairs, mapping messages and blobURLsInFlight separately and capturing the scopes into the batch-completion lambda so they outlive delivery. MessagePortChannelRegistry::didPostMessageToRemote forwards the scope. SWClientConnection::postMessageToServiceWorkerClient is renamed to dispatchMessageToServiceWorkerClient and takes CompletionHandlerCallingScope&& messageDispatched, captured into postTaskTo; ServiceWorkerThread::queueTaskToPostMessage likewise carries messageDispatched into the fired-event lambda and the finishedFiringMessageEvent callback.
The triage bugclass (UAF) is consistent: the change is pure lifetime plumbing (a scope guard carried end-to-end), not validation or bounds. The supplied diff proves the delivery-window holder was missing and is now threaded through; the blob-handle acquisition itself (the triage’s retainBlobURLsWhileMessageIsInFlight / registerInternalBlobURLOptionallyFileBacked) is referenced but not present in the visible diff, so that leg is (inferred).
Key code
An in-flight scope guard travels with each pending message and fires only on delivery
using PendingMessage = std::pair<MessageWithMessagePorts, CompletionHandlerCallingScope>;
std::array<Vector<PendingMessage>, 2> m_pendingMessages;
// takeAllMessagesForPort: scopes captured into the batch-completion lambda
auto blobURLsInFlight = WTF::map(WTF::move(pendingMessages), [](auto&& pendingMessage) {
return WTF::move(pendingMessage.second);
});
callback(WTF::move(messages), [size, port, blobURLsInFlight = WTF::move(blobURLsInFlight),
protectedThis = WTF::move(m_pendingMessageProtectors[i])] { ... });
Fix approach
The fix adds a CompletionHandlerCallingScope that represents ’this message is still in flight; keep its blob URL handles alive’ and threads it through every cross-process delivery path (MessagePort, BroadcastChannel, ServiceWorker client). Each pending message now stores its scope (MessagePortChannel’s m_pendingMessages holds PendingMessage pairs; BroadcastChannel/ServiceWorker capture messageDispatched into their task lambdas), and the scope’s completion only fires once the destination has dispatched the message, at which point the retained blob URL handles can be released. CompletionHandlerCallingScope gets an operator bool for validity checks, and is constructed with CompletionHandlerCallThread::AnyThread so it can fire off the delivery thread by re-posting to the main thread. The net effect is that blob lifetime is bound to delivery completion rather than to the source’s whims, closing the revoke-before-delivery window.
Patch walkthrough
Source/WTF/wtf/CompletionHandler.h— Addsexplicit operator bool()to CompletionHandlerCallingScope so holders can test whether a scope is still armed.Source/WebCore/bindings/js/SerializedScriptValue.h— Exports blobURLs() (WEBCORE_EXPORT) so other modules can enumerate the blob URLs a serialized message carries for retention.Source/WebCore/dom/BroadcastChannel.cpp— dispatchMessageTo builds a CompletionHandlerCallingScope (AnyThread) wrapping the original completion handler; dispatchMessage takes and captures messageDispatched into its queueTaskKeepingObjectAlive lambda so the scope lives until the task runs.Source/WebCore/dom/BroadcastChannel.h— dispatchMessage signature gains CompletionHandlerCallingScope&&; includes wtf/CompletionHandler.h.Source/WebCore/dom/messageports/MessagePortChannel.cpp— postMessageToRemote takes a blobURLsInFlight scope and stores it with the message; m_pendingMessages holds PendingMessage pairs; takeAllMessagesForPort splits messages/scopes and captures the scopes into the batch-completion lambda so they outlive delivery.Source/WebCore/dom/messageports/MessagePortChannel.h— Declares PendingMessage = pair<MessageWithMessagePorts, CompletionHandlerCallingScope>, changes m_pendingMessages to Vector, and updates postMessageToRemote’s signature.Source/WebCore/dom/messageports/MessagePortChannelRegistry.cpp— didPostMessageToRemote forwards the blobURLsInFlight scope to channel->postMessageToRemote.Source/WebCore/dom/messageports/MessagePortChannelRegistry.h— didPostMessageToRemote signature gains CompletionHandlerCallingScope&& blobURLsInFlight = { }; includes wtf/CompletionHandler.h.Source/WebCore/workers/service/SWClientConnection.cpp— postMessageToServiceWorkerClient renamed dispatchMessageToServiceWorkerClient, takes messageDispatched and captures it into the postTaskTo lambda so the scope survives the hop to the destination context.Source/WebCore/workers/service/SWClientConnection.h— Declaration renamed and extended with CompletionHandlerCallingScope&& messageDispatched.Source/WebCore/workers/service/context/ServiceWorkerThread.cpp— queueTaskToPostMessage takes messageDispatched and carries it through the fired-event lambda and the callOnMainThread finishedFiringMessageEvent callback, keeping it alive until the event is fired.Source/WebCore/workers/service/context/ServiceWorkerThread.h— queueTaskToPostMessage signature extended with CompletionHandlerCallingScope&& (include added).
Background
Blob URL — A URL (blob:…) referencing in-memory/file-backed blob data registered in a per-origin blob registry; revoking it (URL.revokeObjectURL or the owning document going away) drops the registration and can free the backing data.
SerializedScriptValue blob handles — A structured-clone message can carry blob URLs; blobURLs()/blobHandles() enumerate them, and other transports (IndexedDB, service worker) take explicit URLKeepingBlobAlive handles to keep them registered during transfer.
CompletionHandlerCallingScope — A WTF scope-guard wrapping a CompletionHandler that fires when destroyed; used here as an in-flight token whose lifetime (and whatever it retains) is bound to message delivery, constructed AnyThread so it can fire off-thread.
Cross-process message delivery paths — MessagePortChannel (network process relay), BroadcastChannel (same-origin multi-context), and SWClientConnection/ServiceWorkerThread (service worker <-> client) each queue a message and dispatch it asynchronously at the destination.
queueTaskKeepingObjectAlive / postTaskTo — WebKit helpers that post a task to a target context while keeping the target object alive; capturing the in-flight scope in these lambdas is what extends blob retention to delivery time.
Vulnerability window
- Setup — A document creates a blob URL and posts a message carrying it via MessagePort, BroadcastChannel, or to a service worker client.
- In-flight — The message is queued/relayed cross-process and awaits dispatch at the destination.
- Trigger — Pre-patch, the source revokes the blob URL / drops its handle before the destination dispatches the message.
- Dangling use — The destination dispatches the message and resolves a now-unregistered/freed blob URL.
- Post-patch — An in-flight CompletionHandlerCallingScope travels with the message and retains the blob handles until delivery, so revocation cannot free them early.
Proof of concept
No regression test ships in the diff. Conceptual trigger (inferred): postMessage a Blob (or a message referencing a blob: URL) across a MessagePort/BroadcastChannel/ServiceWorker boundary, then immediately URL.revokeObjectURL() and drop all references so the source frees the blob before the destination’s queued task dispatches the message; pre-patch the destination resolves a dangling blob handle. The exact race window and the blob-handle registration (registerInternalBlobURLOptionallyFileBacked) are not fully in the supplied diff, so the trigger is (inferred).
Exploitation
- Reach — Ordinary web content: create a blob URL and send it through MessagePort, BroadcastChannel, or a service worker client message.
- Trigger — Revoke/free the blob between posting and the destination dispatching the queued message (a timing race on the delivery window).
- Primitive — Destination/network-process resolution of a freed or unregistered blob URL handle (dangling reference).
- Escalation (inferred) — Open questions: whether the freed blob backing can be reclaimed with attacker data before resolution and whether resolving it yields a read of or write through reclaimed memory; not demonstrated by the diff, so impact beyond a dangling-reference fault is (inferred).
Detection & hunting
For defenders and detection engineers:
- Blob URL resolution after revocation during message dispatch — Instrument the blob registry to flag resolve-after-unregister on message-delivery paths.
- In-flight messages without a retained scope — Audit that every cross-process message path now carries a CompletionHandlerCallingScope through to dispatch (the pattern this patch adds).
- ASan dangling-reference in blob handling on MessagePort/BroadcastChannel/SW dispatch — Fuzz post-then-revoke sequences across the three transports under ASan.
Remaining risk in the patch
- Blob-retention body not in the supplied diff — The triage cites retainBlobURLsWhileMessageIsInFlight() and registerInternalBlobURLOptionallyFileBacked (refuse to file-back a blob whose path is disallowed / whose srcURL is unregistered), but those bodies are not present in the visible diff; the scope plumbing shown here only carries a holder, so the actual blob-handle acquisition/release and the file-backing refusal cannot be verified from this diff.
- Default-constructed scope on the registry path — MessagePortChannelRegistry::didPostMessageToRemote defaults blobURLsInFlight to
{ }; any caller that does not supply a real scope would leave messages without in-flight blob retention on that path. - Diff truncation — The supplied bundle for this candidate is cut off mid-file (ServiceWorkerThread.h), so later files in the commit are not visible and one or more delivery paths may be unverified here.
Audit directions
- Other message transports carrying blobs — Check any remaining postMessage-like path (e.g. the WebPageProxy::postMessageToRemote FIXME that takes no blob handles) for the same missing in-flight retention.
- CompletionHandlerCallingScope propagation completeness — Verify the scope is captured on every branch (early returns, closed channels, dropped ports) so blobs are retained or released exactly once.
- Blob handle acquisition site — Locate the retainBlobURLsWhileMessageIsInFlight / registerInternalBlobURLOptionallyFileBacked logic (not in this diff) and confirm it pairs with the scope added here.
- Other scope-guard-as-lifetime patterns — Review places that rely on makeScopeExit for cross-thread lifetime; AnyThread firing and capture into async tasks are easy to get wrong.