0bcd5c84c365da592ed86a60c03e8e1aa011b51a [WebCore] use-after-free in ShareDataReader::didFinishLoading: cancel() called on freed this after completionHandler destruction
Triage note: ShareDataReader::start captured [this] in the BlobLoader completion lambda and called this->didFinishLoading after async blob load; if the reader was destroyed first this is a UAF. Fix switches to RefCountedAndCanMakeWeakPtr with WeakPtr capture and a RefPtr protectedThis guard.
The bug at a glance
ShareDataReader::start (Source/WebCore/page/ShareDataReader.cpp) captured a raw [this] in an asynchronous BlobLoader completion lambda and called this->didFinishLoading() after the blob finished loading, while ShareDataReader was only RefCounted<> and the lambda held no reference to the reader. If the reader is destroyed before the async blob read completes, the completion handler dereferences freed memory. This runs in the WebContent process and is reachable from the Web Share API (navigator.share({files:[…]})). The proven defect is a lifetime/raw-pointer-capture bug across an async boundary; a genuine use-after-free follows if the owning reference is dropped mid-load, which the diff does not itself show (inferred), so the severity is high but the free path is an inference.
A classic raw-this capture in an async FileReaderLoader/BlobLoader completion handler on a ref-counted object that the completion handler does not keep alive, so the callback can outlive the object it calls back into.
Root cause
The violated invariant is that an object whose method is invoked from an asynchronous completion handler must stay alive until that handler runs (or the handler must tolerate the object’s destruction). ShareDataReader is declared class ShareDataReader : public RefCounted<ShareDataReader>. In ShareDataReader::start(), for each blob file it does Ref blobLoader = BlobLoader::create([this, count, fileName = blob->name()](BlobLoader&) { this->didFinishLoading(count, fileName); }); and then m_pendingFileLoads.append(blobLoader.copyRef()); blobLoader->start(blob, document, FileReaderLoader::ReadAsArrayBuffer);. The reader owns the BlobLoader, but the BlobLoader’s completion lambda captures the reader by raw pointer only.
Pre-patch sequence: 1. start() captures raw this into the BlobLoader lambda. 2. The BlobLoader begins an asynchronous FileReaderLoader read (ReadAsArrayBuffer). 3. Control returns to the caller; nothing in the async chain holds a Ref to the ShareDataReader itself (only the reader holds Refs to the BlobLoaders via m_pendingFileLoads). 4. If whoever owns the ShareDataReader releases its last Ref (e.g. the share operation is abandoned/navigated away, or the owning object tears down) before the blob load completes, the ShareDataReader is destroyed and its m_pendingFileLoads vector (and the BlobLoaders) go with it — but if a load completion is already queued/in-flight it fires this->didFinishLoading(...) on the freed reader.
Because ShareDataReader is Ref-counted (not Oilpan), destruction is deterministic at refcount zero; the bug is that the completion closure kept the object reachable by raw pointer, not by Ref or WeakPtr. The exact caller that drops the last owning reference is not in the supplied diff (inferred), so the writeup proves the lifetime defect and the UAF-shaped fix, with the triggering free path marked inferred.
Key code
Raw this captured in the async BlobLoader completion lambda (pre-patch)
// before:
Ref blobLoader = BlobLoader::create([this, count, fileName = blob->name()](BlobLoader&) {
this->didFinishLoading(count, fileName);
});
// after:
Ref blobLoader = BlobLoader::create([weakThis = WeakPtr { *this }, count, fileName = blob->name()](BlobLoader&) {
if (RefPtr protectedThis = weakThis)
protectedThis->didFinishLoading(count, fileName);
});
Fix approach
The patch changes the base class from RefCounted to RefCountedAndCanMakeWeakPtr (adding #include <wtf/RefCountedAndCanMakeWeakPtr.h>) and rewrites the capture to weakThis = WeakPtr { *this }. In the completion handler it materializes a strong RefPtr protectedThis = weakThis and only calls didFinishLoading() if the reader is still alive. This closes the window: if the reader has been destroyed, weakThis is null and the call is skipped; if it is alive, protectedThis holds it alive for the duration of the call. The hot path collapses to a null-guarded, ref-protected dispatch rather than an unconditional raw-pointer call.
Patch walkthrough
Source/WebCore/page/ShareDataReader.cpp— The per-blob BlobLoader completion lambda now captures a WeakPtr to the reader instead of rawthis, and guards the didFinishLoading() call behindif (RefPtr protectedThis = weakThis), so a destroyed reader is never dereferenced.Source/WebCore/page/ShareDataReader.h— ShareDataReader’s base changes from RefCounted to RefCountedAndCanMakeWeakPtr, and <wtf/RefCountedAndCanMakeWeakPtr.h> is included, so WeakPtr{*this} is valid.
Background
Web Share API / ShareDataReader — navigator.share({files}) hands blobs to the platform share sheet; ShareDataReader asynchronously reads each shared File/Blob into an ArrayBuffer before the share proceeds.
BlobLoader / FileReaderLoader — WebCore helper that asynchronously reads a Blob’s bytes; BlobLoader::create takes a completion handler invoked when the read finishes, potentially long after start().
RefCounted vs RefCountedAndCanMakeWeakPtr — RefCounted gives intrusive strong refcounting (Ref/RefPtr). RefCountedAndCanMakeWeakPtr additionally lets code mint WeakPtrs that observe destruction without keeping the object alive.
WeakPtr + protectedThis idiom — WebKit’s standard safe-async pattern: capture WeakPtr in a callback, then upgrade to RefPtr inside the callback to both test liveness and pin the object for the call’s duration.
Raw-this capture hazard — A lambda capturing [this] stores a bare pointer; if the object is freed before the lambda runs (common with async I/O completions), invoking a method is a use-after-free.
Vulnerability window
- Setup — Script calls navigator.share with one or more File objects, driving ShareDataReader::start to spin up a BlobLoader per file.
- Trigger — Each BlobLoader begins an async ArrayBuffer read; the completion lambda holds only a raw pointer back to the reader.
- Window — Before the reads complete, the owning reference to the ShareDataReader is dropped (navigation/teardown/abandon), freeing the reader (inferred).
- Fire — A pending blob read completes and the completion lambda calls didFinishLoading() on the freed ShareDataReader — use-after-free.
Proof of concept
No regression test ships in the diff. Conceptual trigger (web path inferred): invoke navigator.share({files:[largeFile,…]}) so ShareDataReader starts async blob reads, then cause the reader’s owner to be torn down before the reads finish (e.g. navigate the frame away or discard the share) so the last Ref drops while a BlobLoader completion is still pending; the completion fires didFinishLoading() on freed memory. The precise owner/drop step is not in the diff.
Exploitation
- Reach — Web content calls navigator.share with files, a user-gesture-gated but web-reachable entry into ShareDataReader in the WebContent process.
- Trigger — Keep one or more blob reads in flight so a raw-this completion lambda is live and pending.
- Primitive — Dropping the owning reference frees the reader; the queued completion then dereferences a dangling ShareDataReader, giving a UAF read/call (primary primitive is a stale vtable/field access via didFinishLoading).
- Escalation (open) — Whether the freed ShareDataReader bytes can be reclaimed with attacker-controlled data in the narrow single-file completion window, and whether didFinishLoading’s field/callback use can steer control flow, gates any move beyond a crash (inferred).
Detection & hunting
For defenders and detection engineers:
- Raw [this] captured in BlobLoader/FileReaderLoader-style completion lambdas — Grep WebCore for
BlobLoader::create([thisand async completion handlers capturing this on RefCounted-but-not-weakable classes. - ASan heap-use-after-free in ShareDataReader::didFinishLoading — Fuzz/stress navigator.share with files under ASan while forcing frame teardown mid-share.
Remaining risk in the patch
- Other callbacks on the same object — The patch only hardens the per-file BlobLoader completion lambda; if ShareDataReader posts any other async continuation capturing this (not shown in the diff), that path remains unverified (inferred).
- Free path not in diff — The owner that drops the last Ref during an in-flight load is outside the supplied diff, so the end-to-end UAF reachability is asserted from the fix shape, not directly proven here.
Audit directions
- Async completion handlers on RefCounted WebCore objects — Audit all
*Loader::create([this ...])and CompletionHandler captures where the enclosing class is RefCounted but not WeakPtr-capable; convert to WeakPtr+protectedThis. - Web Share pipeline — Review the rest of ShareDataReader and its callers for other places the reader is assumed live across async blob/file reads.
- FileReaderLoader clients — Check other FileReaderLoader/BlobLoader consumers (clipboard, drag-and-drop, FileReader) for the same raw-this-across-async-read shape.