CVE-2026-12437
Overview
Background
- WebShare (Windows)
- Chrome’s browser-process implementation of the Web Share API that streams shared file data to a target app via Windows Runtime (WRL) COM interfaces.
- `OutputStreamWriteOperation`
- a ref-counted browser-process object (
chrome/browser/webshare/win/share_operation.cc) that drives writing a shared blob to a WindowsIOutputStream. - `DeleteOnIOThread`
- a
base::RefCountedThreadSafedeletion-trait that posts the object’s destructor to thecontent::BrowserThread::IOsequence instead of destroying it wherever the last reference drops. - `FileWriterDelegate` / `BlobReader` / `BlobDataSnapshot`
- the IO-thread-affine ownership chain held by
OutputStreamWriteOperation; per the fix’s destructor comment,BlobDataSnapshotmust be destroyed on the IO thread; per the reporter, it holds non-atomically ref-countedBlobDataItems.
Root Cause Analysis
8badafd07b12. An earlier analysis was withdrawn because it had been written against a commit that was not the security fix (d04b3a27cd55).On the IO thread, OutputStreamWriteOperation::BlobDataHandleReady builds a FileWriterDelegate and starts it with a reader created from the blob handle (share_operation.cc:303-310), establishing the ownership chain OutputStreamWriteOperation → FileWriterDelegate → BlobReader → BlobDataSnapshot → scoped_refptr<BlobDataItem>. Pre-fix, the class derived only from base::RefCountedThreadSafe<OutputStreamWriteOperation> (share_operation.cc:233-234) and used ~OutputStreamWriteOperation() = default (share_operation.cc:263), so the object was destroyed on whatever thread released its last reference. The invariant violated is that BlobDataSnapshot must be torn down on the IO thread (stated in the fix’s destructor comment; the reporter adds that it releases non-atomically ref-counted BlobDataItems); because a scoped_refptr<OutputStreamWriteOperation> is held by a Windows COM delegate that Windows can release on an arbitrary thread-pool/COM thread (per the reporter; the off-thread release itself is not shown in the material), the destructor and its synchronous teardown of the IO-thread-only members could run off-sequence.
The fix (commit 8badafd07b12649b98bb1db5cfae908a7e4c376c) changes the base class to base::RefCountedThreadSafe<OutputStreamWriteOperation, content::BrowserThread::DeleteOnIOThread> and adds the matching friend declarations, so the final release re-posts destruction to the IO thread; it also replaces the defaulted destructor with one asserting DCHECK_CURRENTLY_ON(content::BrowserThread::IO) to catch any future off-thread teardown. This restores the IO-thread-only destruction invariant for the whole object and its owned children.
BlobDataSnapshot, per the fix’s comment) but could be destroyed on any thread; specifying the DeleteOnIOThread trait forces its destructor (and that teardown) back onto the IO thread.Attack Path
- Initiate a Web Share with a file
A renderer-driven Web Share operation streams a blob-backed file to a Windows share target, causing the browser to create
OutputStreamWriteOperationand its IO-thread blob-reader chain (share_operation.cc:303-310). - Windows holds the sole reference off-thread
The
scoped_refptr<OutputStreamWriteOperation>is captured by a WRL COM delegate owned by Windows, so the last reference lives outside Chrome’s IO sequence (reporter’s claim; not shown in the material, (inferred)). - Off-thread destruction
When the share target finishes or aborts, Windows releases the delegate on an arbitrary thread, running
~OutputStreamWriteOperationand synchronously destroyingFileWriterDelegate/BlobReader/BlobDataSnapshotoff the IO thread (reporter’s claim; the off-thread release is not shown in the material, (inferred)). - Refcount race / UAF
Per the reporter, non-atomic
BlobDataItemrefcount decrements race with IO-thread users, potentially freeing an item prematurely and yielding a use-after-free in the unsandboxed browser process (inferred).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
OutputStreamWriteOperationchrome/browser/webshare/win/share_operation.cc |
modified |
Files Changed
chrome/browser/webshare/win/share_operation.cc
Audit Directions
- Thread-affine ownership under foreign callbacksFlag
RefCountedThreadSafeobjects whose references are handed to OS/COM/third-party callbacks that may release on arbitrary threads, and confirm they specify aDeleteOn*Threadtrait when they own sequence-restricted members. - Non-atomic refcounts off-sequenceAudit chains that reach
base::RefCounted(non-atomic) members such asBlobDataItemto ensure their release always happens on the owning sequence. - Destructor DCHECKsTreat a destructor that must run on a specific thread as requiring an explicit
DCHECK_CURRENTLY_ON(...)so off-sequence teardown is caught by existing tests rather than shipping silently.
Patch
diff --git a/chrome/browser/webshare/win/share_operation.cc b/chrome/browser/webshare/win/share_operation.cc
index 6769897..6d0b7548 100644
--- a/chrome/browser/webshare/win/share_operation.cc
+++ b/chrome/browser/webshare/win/share_operation.cc
@@ -231,7 +231,9 @@
// Represents an ongoing operation of writing to an IOutputStream.
class OutputStreamWriteOperation
- : public base::RefCountedThreadSafe<OutputStreamWriteOperation> {
+ : public base::RefCountedThreadSafe<
+ OutputStreamWriteOperation,
+ content::BrowserThread::DeleteOnIOThread> {
public:
OutputStreamWriteOperation(
content::BrowserContext::BlobContextGetter blob_context_getter,
@@ -258,9 +260,23 @@
}
private:
- friend class base::RefCountedThreadSafe<OutputStreamWriteOperation>;
+ friend class base::RefCountedThreadSafe<
+ OutputStreamWriteOperation,
+ content::BrowserThread::DeleteOnIOThread>;
+ friend struct content::BrowserThread::DeleteOnThread<
+ content::BrowserThread::IO>;
+ friend class base::DeleteHelper<OutputStreamWriteOperation>;
- ~OutputStreamWriteOperation() = default;
+ ~OutputStreamWriteOperation() {
+ // Though at the time of this writing the only thing this class owns that
+ // needs to be destroyed on the IO thread is the FileWriterDelegate (because
+ // it owns a BlobReader, which owns a BlobDataSnapshot, which must be
+ // destroyed on the IO thread), this class is largely IO-centric, so to
+ // protect against future changes that may modify what needs to be destroyed
+ // on the IO thread we broadly enforce the class as a whole is always
+ // destroyed there.
+ DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
+ }
void WriteStreamOnIOThread() {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
Original Bug Report
Potential UAF in browser process via off-thread destruction of OutputStreamWriteOperation
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential Use-After-Free (UAF) vulnerability exists in the Windows-specific Web Share implementation. OutputStreamWriteOperation transitively owns resources that must only be destroyed on the IO thread. When its final reference is released on an arbitrary thread by Windows COM delegates, off-sequence destruction of non-atomic ref-counted blob items can lead to a race condition and subsequent memory corruption in the unsandboxed browser process.
Affected files:
chrome/browser/webshare/win/share_operation.ccstorage/browser/blob/blob_data_snapshot.hstorage/browser/blob/blob_data_item.h
Estimated timestamp from git blame: 2020-10-28
Root Cause Analysis
In chrome/browser/webshare/win/share_operation.cc, ShareOperation::PutShareContentInDataPackage creates an OutputStreamWriteOperation for each shared file and registers an IStreamedFileDataRequestedHandler COM delegate callback:
auto operation = base::MakeRefCounted<OutputStreamWriteOperation>(...);
...
auto raw_data_requested_callback =
Callback<IStreamedFileDataRequestedHandler>(
[operation](IOutputStream* stream) -> HRESULT {
operation->WriteStream(stream,
base::DoNothingWithBoundArgs(operation));
return S_OK;
});
The scoped_refptr<OutputStreamWriteOperation> is captured by the Windows Runtime (WRL) Callback object, which is passed to Windows via IStorageFileStatics::CreateStreamedFileAsync. Once the registration loop terminates, Windows retains the sole strong reference to this COM delegate.
When Windows eventually releases the delegate (e.g., when the target application releases the streamed StorageFile), the destruction callback runs on an arbitrary Windows thread-pool or COM thread. As a result, the ~OutputStreamWriteOperation destructor executes off-sequence on a non-IO thread.
Thread-Safety Violation on Blob Resources
During its execution, OutputStreamWriteOperation populates the following members on the IO thread:
writer_delegate_ = std::make_unique<storage::FileWriterDelegate>(
std::make_unique<DataWriterFileStreamWriter>(...),
storage::FlushPolicy::FLUSH_ON_COMPLETION);
writer_delegate_->Start(
blob_handle_->CreateReader(),
base::BindRepeating(&OutputStreamWriteOperation::OnFileWritten,
weak_factory_.GetWeakPtr()));
This sets up a ownership and destruction chain:
OutputStreamWriteOperationowns aFileWriterDelegate(storage/browser/file_system/file_writer_delegate.h).FileWriterDelegateowns aBlobReader(storage/browser/blob/blob_reader.h).BlobReaderowns aBlobDataSnapshot(storage/browser/blob/blob_reader.h).BlobDataSnapshotholds a list ofscoped_refptr<BlobDataItem>objects.
BlobDataSnapshot explicitly documents that it must be deleted on the IO thread. BlobDataItem uses non-atomic (non-thread-safe) reference counting:
class BlobDataItem : public base::RefCounted<BlobDataItem> { ... };
When ~OutputStreamWriteOperation executes on a non-IO thread, it triggers the synchronous destruction of FileWriterDelegate, BlobReader, and BlobDataSnapshot, performing non-atomic Release() decrements on BlobDataItem objects from an arbitrary background thread.
Potential Steps to Trigger
Because this is based on static code analysis, the following are potential steps that an attacker operating from a compromised renderer might use to trigger the race condition:
- The renderer initiates a Web Share operation containing a file associated with a specific Blob.
- Once the share target begins streaming the file data, the browser instantiates the IO-thread resources.
- The renderer concurrently and rapidly drives reference-count mutations on the same underlying Blob (e.g., via rapid
URL.createObjectURL/URL.revokeObjectURLcalls) to performAddRef()andRelease()operations on the IO thread. - When the file transfer finishes or is aborted, Windows releases the COM delegate off-thread, running
~BlobDataSnapshotand decrementing the non-atomic refcount ofBlobDataItemconcurrently. - The non-atomic race causes a lost reference increment/decrement, leading to a premature delete of a
BlobDataItemwhile a reference is still held in the registry. - A subsequent access to this item on the IO thread triggers a Use-After-Free (UAF) in the unsandboxed browser process.
Suggested Fix
To resolve this issue, ensure that OutputStreamWriteOperation and its thread-affine members are safely destroyed on the correct sequence. Because OutputStreamWriteOperation inherits from RefCountedThreadSafe, a custom deleter can be specified to force destruction on the IO thread:
class OutputStreamWriteOperation
: public base::RefCountedThreadSafe<OutputStreamWriteOperation,
content::BrowserThread::DeleteOnIOThread> {
// ...
};
Alternatively, specify a custom task runner when releasing sequence-affine members (like writer_delegate_ and blob_handle_) or post a task to delete them on the IO thread.
Evaluated with Chrome root at commit: a2bea94528f4bd6cc57739c43fa3bb890b8367d3
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.