CVE-2026-7355
Overview
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
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.