CVE-2026-7355
Overview
Background
- MSE / `SourceBufferStream`
- The Media Source Extensions buffering engine; it stores appended media as a list of buffered ranges (
ranges_, a container of range objects). - `range_for_next_append_`
- An iterator into
ranges_that remembers which range the next appended coded-frame group will be added to;ranges_.end()means “no specific range.” - Garbage collection (GC)
- The routine that evicts buffered ranges to stay under the memory limit, deleting range objects from
ranges_from the front or back. - `DeleteAndRemoveRange`
- The helper that erases a range from
ranges_and updates bookkeeping iterators (includingrange_for_next_append_) so none are left dangling.
Root Cause Analysis
Verify before building a PoC
What the commit does not establish: Removed CHECK already trapped range_for_next_append_ aliasing current_range before the pop, so the shown hunk alone yields a CHECK crash, not a demonstrated UAF; DeleteAndRemoveRange's reset-to-end() behavior is inferred from the test comment, its body isn't in the diff.
In the GC eviction loop (the enclosing function name is not shown in the @@ -1030 hunk header; it is the memory-freeing routine in source_buffer_stream.cc), a range whose memory usage dropped to zero was removed directly with ranges_.pop_back() / ranges_.pop_front(). Those raw pops delete the range object and invalidate the corresponding iterator without updating range_for_next_append_, which may point at the very range being evicted. The old code only CHECK-ed that range_for_next_append_ did not alias current_range, but as the regression test shows, GC can legitimately evict the range that range_for_next_append_ references (Range A, overlapped by a new coded-frame group and then evicted as far behind media time). The violated invariant is that range_for_next_append_ must never dangle after a range is deleted.
The fix replaces the raw pop with DeleteAndRemoveRange(&range_to_delete) (and nulls the local current_range first), so the shared deletion helper resets range_for_next_append_ to ranges_.end() when it removes that range, preventing the later dangling dereference or CHECK failure on the next append.
range_for_next_append_, leaving that iterator dangling; the fix routes deletion through DeleteAndRemoveRange, which updates the iterator to ranges_.end().Attack Path
- Build two ranges
Via MSE, append media to create a kept range (Range A) and a second range (Range B), as in the test’s two
NewCodedFrameGroupAppendcalls. - Point the append iterator at Range A
Start a new coded-frame group overlapping Range A (
OnStartOfCodedFrameGroup), which setsrange_for_next_append_to Range A. - Evict Range A via GC
Drive memory pressure (lower the limit) and garbage-collect with media time at Range B so GC frees Range A from the front; pre-patch this pops the range without clearing
range_for_next_append_. - Dangling use on next append
Append more data; the stale
range_for_next_append_iterator is dereferenced, causing a use-after-free (or, in checked builds, aCHECKcrash), as the test’s finalAppendBuffersdemonstrates.
Impact Assessment
range_for_next_append_ is evicted, then appending again. The advisory does not report in-the-wild exploitation, and any escalation of the UAF beyond a crash to exploitable memory corruption is (inferred), not shown by the diff.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmedia/filters/source_buffer_stream.cc |
modified | |
TEST_Fmedia/filters/source_buffer_stream_unittest.cc |
modified |
Files Changed
media/filters/source_buffer_stream.ccmedia/filters/source_buffer_stream_unittest.cc
Audit Directions
- Stored iterators across mutationAudit classes that cache iterators/pointers into a container and then mutate the container (erase/pop/evict) elsewhere; confirm every deletion path updates or invalidates the cached reference.
- Raw erase vs. bookkeeping helperLook for direct
pop_front/pop_back/erasecalls where a dedicated removal helper (hereDeleteAndRemoveRange) exists precisely to keep related state consistent, and the raw call bypasses it. - GC / eviction lifetimeReview memory-pressure and GC routines in the media stack for assumptions that the “current” or “next-append” object cannot be the one being evicted; the overlap-then-evict sequence is the trigger pattern.
Patch
From 055b8ce56f2e737e68730a9d3178ff4cb93fa7fb Mon Sep 17 00:00:00 2001
From: Dale Curtis <dalecurtis@chromium.org>
Date: Wed, 08 Apr 2026 10:58:16 -0700
Subject: [PATCH] [MSE] Correct range_for_next_append_ during SourceBuffer GC
The range may become invalid during garbage collection.
R=tmathmeyer
Fixed: 498285711, 500387779
Change-Id: I2bc9150f69cf8daa92168d04022c6efd69f7ae6c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7737855
Auto-Submit: Dale Curtis <dalecurtis@chromium.org>
Reviewed-by: Ted (Chromium) Meyer <tmathmeyer@chromium.org>
Commit-Queue: Ted (Chromium) Meyer <tmathmeyer@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1611660}
---
diff --git a/media/filters/source_buffer_stream.cc b/media/filters/source_buffer_stream.cc
index 21486d00..e970821 100644
--- a/media/filters/source_buffer_stream.cc
+++ b/media/filters/source_buffer_stream.cc
@@ -1030,11 +1030,10 @@
if (current_range->GetMemoryUsage() == 0) {
CHECK_NE(current_range, selected_range_);
- CHECK(range_for_next_append_ == ranges_.end() ||
- range_for_next_append_->get() != current_range);
-
- // Delete |current_range| by popping it out of |ranges_|.
- reverse_direction ? ranges_.pop_back() : ranges_.pop_front();
+ auto range_to_delete =
+ reverse_direction ? std::prev(ranges_.end()) : ranges_.begin();
+ current_range = nullptr;
+ DeleteAndRemoveRange(&range_to_delete);
}
if (reverse_direction && new_range_for_append) {
diff --git a/media/filters/source_buffer_stream_unittest.cc b/media/filters/source_buffer_stream_unittest.cc
index b154eeb..7cccec0 100644
--- a/media/filters/source_buffer_stream_unittest.cc
+++ b/media/filters/source_buffer_stream_unittest.cc
@@ -5747,4 +5747,38 @@
EXPECT_TRUE(IsRangeListSorted());
}
+TEST_F(SourceBufferStreamTest, GarbageCollectionUpdatesRangeForNextAppend) {
+ // Set memory limit to 10 buffers.
+ SetMemoryLimit(10);
+
+ // 1. Append 10 buffers to create Range A [0, 90ms].
+ NewCodedFrameGroupAppend("0K 10K 20K 30K 40K 50K 60K 70K 80K 90K");
+
+ // 2. Append 10 buffers to create Range B [1000ms, 1090ms].
+ // This exceeds the memory limit and triggers GC, but Range A is kept because
+ // it was recently appended.
+ NewCodedFrameGroupAppend(
+ "1000K 1010K 1020K 1030K 1040K 1050K 1060K 1070K 1080K 1090K");
+
+ // 3. Start a new coded frame group that overlaps Range A.
+ // This sets range_for_next_append_ to Range A and
+ // last_appended_buffer_timestamp_ to kNoTimestamp.
+ stream_->OnStartOfCodedFrameGroup(base::Milliseconds(0));
+
+ // 4. Trigger Garbage Collection.
+ // We want to free enough data that Range A is deleted.
+ // Set memory limit very low so GC must evict something.
+ SetMemoryLimit(5);
+
+ // Garbage collect with media time at Range B (1000ms).
+ // This should evict Range A from the front since it is far behind media time.
+ EXPECT_TRUE(GarbageCollect(base::Milliseconds(1000), 0));
+
+ // 5. Append data. If the bug exists, range_for_next_append_ is dangling and
+ // dereferencing it will cause a UAF or hit a CHECK.
+ // With the fix, range_for_next_append_ is reset to ranges_.end() when
+ // Range A is deleted.
+ AppendBuffers("0K 10K");
+}
+
} // namespace media
Regression Test / PoC
diff --git a/media/filters/source_buffer_stream_unittest.cc b/media/filters/source_buffer_stream_unittest.cc
index b154eeb..7cccec0 100644
--- a/media/filters/source_buffer_stream_unittest.cc
+++ b/media/filters/source_buffer_stream_unittest.cc
@@ -5747,4 +5747,38 @@
EXPECT_TRUE(IsRangeListSorted());
}
+TEST_F(SourceBufferStreamTest, GarbageCollectionUpdatesRangeForNextAppend) {
+ // Set memory limit to 10 buffers.
+ SetMemoryLimit(10);
+
+ // 1. Append 10 buffers to create Range A [0, 90ms].
+ NewCodedFrameGroupAppend("0K 10K 20K 30K 40K 50K 60K 70K 80K 90K");
+
+ // 2. Append 10 buffers to create Range B [1000ms, 1090ms].
+ // This exceeds the memory limit and triggers GC, but Range A is kept because
+ // it was recently appended.
+ NewCodedFrameGroupAppend(
+ "1000K 1010K 1020K 1030K 1040K 1050K 1060K 1070K 1080K 1090K");
+
+ // 3. Start a new coded frame group that overlaps Range A.
+ // This sets range_for_next_append_ to Range A and
+ // last_appended_buffer_timestamp_ to kNoTimestamp.
+ stream_->OnStartOfCodedFrameGroup(base::Milliseconds(0));
+
+ // 4. Trigger Garbage Collection.
+ // We want to free enough data that Range A is deleted.
+ // Set memory limit very low so GC must evict something.
+ SetMemoryLimit(5);
+
+ // Garbage collect with media time at Range B (1000ms).
+ // This should evict Range A from the front since it is far behind media time.
+ EXPECT_TRUE(GarbageCollect(base::Milliseconds(1000), 0));
+
+ // 5. Append data. If the bug exists, range_for_next_append_ is dangling and
+ // dereferencing it will cause a UAF or hit a CHECK.
+ // With the fix, range_for_next_append_ is reset to ranges_.end() when
+ // Range A is deleted.
+ AppendBuffers("0K 10K");
+}
+
} // namespace media
Original Bug Report
UAF in SourceBufferStream::FreeBuffers via dangling range_for_next_append_ iterator
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A Use-After-Free (UAF) vulnerability exists in SourceBufferStream because the range_for_next_append_ iterator can become dangling when its range is removed during garbage collection. A safety check fails to reset the iterator when last_appended_buffer_timestamp_ is kNoTimestamp, allowing an attacker to dereference the dangling iterator and potentially achieve arbitrary code execution.
Affected files:
media/filters/source_buffer_stream.ccmedia/filters/source_buffer_stream.h
Estimated timestamp from git blame: 2024-04-03
Description
A potential Use-After-Free (UAF) vulnerability exists in media::SourceBufferStream within the Chromium Renderer process. The issue stems from the management of the range_for_next_append_ iterator, which points to a SourceBufferRange within a std::list. Under specific conditions, this iterator can become dangling when the range it points to is removed from the list during garbage collection.
Vulnerability Details
SourceBufferStream maintains range_for_next_append_ (a std::list<std::unique_ptr<SourceBufferRange>>::iterator) to track where the next media append should occur. The FreeBuffers method is responsible for removing ranges to satisfy memory limits. It includes a check to reset the iterator if the range being deleted is the one containing the last appended buffer:
// media/filters/source_buffer_stream.cc
// Check to see if we've just deleted the GOP that was last appended.
base::TimeDelta end_timestamp = buffers.back()->timestamp();
if (end_timestamp == last_appended_buffer_timestamp_) {
// ...
range_for_next_append_ = ranges_.end();
} else {
bytes_freed += bytes_deleted;
}
if (current_range->GetMemoryUsage() == 0) {
DCHECK_NE(current_range, selected_range_);
DCHECK(range_for_next_append_ == ranges_.end() ||
range_for_next_append_->get() != current_range);
// Delete |current_range| by popping it out of |ranges_|.
reverse_direction ? ranges_.pop_back() : ranges_.pop_front();
}
If last_appended_buffer_timestamp_ is kNoTimestamp (which can occur if a new coded frame group is started but no buffers have yet been appended to the stream), the condition end_timestamp == last_appended_buffer_timestamp_ will never be true. In this case, range_for_next_append_ is not reset. When the range is popped from the list, the iterator becomes dangling. Note that the DCHECK verifying the iterator’s safety is compiled out in Release builds.
Potential Reachability
This vulnerable state is reachable by performing the following steps through the Media Source Extensions (MSE) API:
- Append Muxed Media: Append an initialization segment and a media segment containing multiplexed audio and video frames to create disjoint buffered ranges (e.g., [0-5s] and [10-15s]).
- Reset Parser: Call
SourceBuffer.abort()to reset the parser state and end the current coded frame group. - Trigger New Coded Frame Group (Video-Only): Append a new media segment that contains only video frames. The
FrameProcessordetects the new coded frame group and broadcastsNotifyStartOfCodedFrameGroupto all track buffers. - Audio Stream State Update: For the audio track,
SourceBufferStream::OnStartOfCodedFrameGroupsetsrange_for_next_append_to an existing overlapping range and callsResetLastAppendedState(), which setslast_appended_buffer_timestamp_tokNoTimestamp. - Trigger Garbage Collection: Append a large amount of video data to exceed the
SourceBuffer’s memory limit. This triggersChunkDemuxerStream::EvictCodedFrames()and subsequentlySourceBufferStream::FreeBuffers()for the audio stream. - Bypass Safety Check:
FreeBuffers()deletes the range pointed to byrange_for_next_append_. Becauselast_appended_buffer_timestamp_iskNoTimestamp, the protective check is bypassed, and the range is popped while leavingrange_for_next_append_dangling. - Trigger UAF: Append a new media segment containing audio frames without calling
abort().SourceBufferStream::Append()is called, which dereferences the danglingrange_for_next_append_iterator to callAppendBuffersToEnd.
Impact
Since std::list iterators in libc++ store raw pointers to nodes, this UAF is not mitigated by MiraclePtr (BRP). An attacker can use heap spraying to control the freed list node’s contents, allowing them to forge the this pointer for the AppendBuffersToEnd call. Inside AppendBuffersToEnd, operations on member variables (like std::deque::push_back and std::map::insert) can be manipulated to achieve a powerful arbitrary memory write primitive. This could bypass Control-Flow Integrity (CFI) protections and potentially lead to Remote Code Execution (RCE) within the sandboxed Renderer process.
Recommended Fix
Ensure that FreeBuffers correctly resets range_for_next_append_ whenever the range it points to is removed, regardless of the value of last_appended_buffer_timestamp_. A safer approach would be to check if range_for_next_append_->get() == current_range and reset it explicitly before the range is popped, similar to the logic used in SourceBufferStream::DeleteAndRemoveRange.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.