597af50a64 Potential use after free of m_stream in ReadableStreamBYOBReader::visitAdditionalChildren()
Triage note: Same lifetime/race pattern: locks all m_stream access so the GC visitor cannot race with release, a genuine memory-safety fix.
Contents
The bug at a glance
ReadableStreamBYOBReader is obtained via stream.getReader({mode:‘byob’}) and, like the default reader, is visited by the concurrent GC thread which dereferenced m_stream while the main thread could null it. Reachable from ordinary script, cross-thread UAF of a live ReadableStream, so high.
Exactly the sibling of the default-reader race: ReadableStreamBYOBReader::visitAdditionalChildrenInGCThread and isReachableFromOpaqueRoots dereference m_stream on the GC thread, while releaseLock/genericRelease/initialize mutate it on the main thread. The fix adds m_streamLock and serializes all m_stream access through it.
Root cause
ReadableStreamBYOBReader holds RefPtr<ReadableStream> m_stream, set in initialize() and cleared in genericRelease() (m_stream = nullptr). The GC marking thread runs visitAdditionalChildrenInGCThread(visitor), which does if (m_stream) m_stream->visitAdditionalChildrenInGCThread(visitor), and isReachableFromOpaqueRoots(), which reads readIntoRequestsSize() && m_stream && m_stream->isReachableFromOpaqueRoots(). Both dereference m_stream with no synchronization.
On the main thread, readForBindings/releaseLock guard on if (!m_stream), read() and genericRelease() take Ref stream = *m_stream, genericCancel/destructor take RefPtr stream = m_stream, and genericRelease finally assigns m_stream = nullptr. When script releases the BYOB reader (or drops it) during a concurrent marking pass, the main-thread clear can free the ReadableStream just as the GC thread dereferences the stale pointer, producing a use-after-free while marking.
The patch adds mutable Lock m_streamLock with m_stream WTF_GUARDED_BY_LOCK(m_streamLock). initialize() now assigns m_stream under the lock; readForBindings/releaseLock/cancel snapshot the presence of a stream under the lock into a local (bool hasStream or RefPtr); read/genericRelease/genericCancel/destructor snapshot m_stream into a local RefPtr under the lock; genericRelease’s final null-out is locked; and both GC-thread functions take the lock before dereferencing m_stream. Note the read() ASSERT(m_stream)/Ref stream = *m_stream is replaced by a locked RefPtr snapshot, removing an unsynchronized deref that could also observe a torn pointer.
Key code
Lock-guarded GC-thread visitor in ReadableStreamBYOBReader.cpp
bool ReadableStreamBYOBReader::isReachableFromOpaqueRoots() const
{
Locker locker { m_streamLock };
return readIntoRequestsSize() && m_stream && m_stream->isReachableFromOpaqueRoots();
}
template<typename Visitor>
void ReadableStreamBYOBReader::visitAdditionalChildrenInGCThread(Visitor& visitor)
{
Locker locker { m_streamLock };
if (m_stream)
SUPPRESS_UNCOUNTED_ARG m_stream->visitAdditionalChildrenInGCThread(visitor);
}
Patch walkthrough
Source/WebCore/Modules/streams/ReadableStreamBYOBReader.cpp— Wraps all m_stream access in Locker{m_streamLock}: destructor and read()/genericRelease()/genericCancel() snapshot into a local RefPtr under the lock; readForBindings/releaseLock checkif (!m_stream)inside a locked scope; cancel() reads a bool hasStream under the lock; initialize() assigns m_stream = stream under the lock; genericRelease() locks its terminal m_stream = nullptr; isReachableFromOpaqueRoots() and visitAdditionalChildrenInGCThread() take the lock before dereferencing m_stream.Source/WebCore/Modules/streams/ReadableStreamBYOBReader.h— Adds <wtf/Lock.h>, addsmutable Lock m_streamLock, and annotatesRefPtr<ReadableStream> m_stream WTF_GUARDED_BY_LOCK(m_streamLock).
Background
ReadableStreamBYOBReader — The ‘bring your own buffer’ reader obtained from getReader({mode:‘byob’}), used for byte streams. It reads directly into caller-supplied ArrayBufferViews and, like the default reader, holds m_stream as a GC-visited back-reference.
initialize() — Sets up the reader by assigning m_stream = &stream and calling stream.setByobReader(this). The pre-patch assignment was unsynchronized; the fix performs it under m_streamLock so the GC thread never observes a half-published pointer.
genericRelease() — The generic reader release algorithm; its final step m_stream = nullptr drops the reader’s reference to the stream. This main-thread write, racing the GC-thread read, is the crux of the UAF.
readIntoRequests — Pending BYOB read requests; isReachableFromOpaqueRoots gates stream reachability on their count. Because that predicate dereferences m_stream, it too must be evaluated under the lock.
Vulnerability window
- Baseline — m_stream accessed unsynchronized by main thread (initialize/release) and GC thread (visitor/reachability).
- Trigger — Script does getReader({mode:‘byob’}), issues reads, then releaseLock()/drops the reader while a concurrent GC marking pass runs.
- Race window — GC thread dereferences m_stream as genericRelease sets it to nullptr and drops the last ref.
- UAF — Freed ReadableStream is called into by the GC thread’s visitor/reachability check.
- Fix — m_streamLock serializes every access including the GC-thread functions.
Triggering
No regression test ships with the patch. Conceptual trigger: obtain a BYOB reader via getReader({mode:‘byob’}), start reads into ArrayBufferViews, then repeatedly releaseLock()/reacquire while allocating heavily so a concurrent GC marking pass dereferences m_stream just as genericRelease nulls it. Probabilistic; best surfaced under TSAN/ASAN.
Exploitation
- Reachability — BYOB readers are standard for byte streams (fetch bodies, etc.); the race is reachable from unprivileged script.
- Window widening — Interleave release/reacquire with allocation pressure to keep the marker busy; keep pending read-into requests to influence the reachability predicate.
- Primitive — GC-thread dereference of a freed ReadableStream, realistically crash-only absent slot grooming and a won race; no weaponization shown.
Detection & hunting
For defenders and SOC / detection engineers:
- TSAN race on BYOB m_stream —
- ASAN UAF via BYOB visitor —
- Reachability-check crashes —
Audit directions
- Cross-check default reader —
- Byte-stream controllers —
- initialize/setup publication order —
- ASSERT(m_stream)+deref patterns —