CVE-2026-7335
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 <[email protected]> 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 <[email protected]> Reviewed-by: Ted (Chromium) Meyer <[email protected]> Commit-Queue: Ted (Chromium) Meyer <[email protected]> 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
SourceBufferStream::FreeBuffers Dangling Iterator UAF Read/Write
Report description
SourceBufferStream::FreeBuffers Dangling Iterator UAF Read/Write
Bug location
Where do you want to report your vulnerability?
Chrome VRP β Report security issues affecting the Chrome browser. See program rules
Which URL (or repository) have you found the vulnerability in?
https://chromium.googlesource.com/chromium/src
The problem
Please describe the technical details of the vulnerability
The problem
Summary
SourceBufferStream::FreeBuffers() in media/filters/source_buffer_stream.cc pops a depleted SourceBufferRange from the ranges_ list without resetting the range_for_next_append_ iterator. The iterator becomes dangling and is subsequently dereferenced, resulting in a heap-use-after-free.
The freed std::list node (operator new(24) β allocator_shim::g_roots β PartitionAlloc 32-byte bucket) is not a MiraclePtr-protected allocation. In non-ASAN release builds, the dangling iterator reaches SourceBufferRange::AppendBuffersToEnd(). The heap spray PoC (poc_spray.html) shows that the freed slot can be reclaimed via WebSocket FastMalloc(24), with the this pointer in AppendBuffersToEnd() set to 0x0EADBEEFCAFE from the spray payload.
Chrome Version
Tested on Chromium 146.0.7680.177 (Linux x86_64) with ASAN, Debug-ASAN, and non-ASAN release builds.
Root Cause
FreeBuffers() at source_buffer_stream.cc:1024-1030 pops a depleted range with only a DCHECK (no-op in release):
// source_buffer_stream.cc:1024-1030
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);
reverse_direction ? ranges_.pop_back() : ranges_.pop_front();
}
DeleteAndRemoveRange() at lines 1987-1993 handles the same situation by resetting the iterator:
// source_buffer_stream.cc:1987-1993
if (*itr == range_for_next_append_) {
range_for_next_append_ = ranges_.end();
ResetLastAppendedState();
}
*itr = ranges_.erase(*itr);
The two code paths that both destroy ranges from the same container handle iterator invalidation differently.
How range_for_next_append_ Becomes Stale
OnStartOfCodedFrameGroup() (line 189) sets range_for_next_append_ to an existing range. This function is called for all tracks via FrameProcessor::NotifyStartOfCodedFrameGroup() (frame_processor.cc:580-588). When a video-only cluster is appended, the audio track’s OnStartOfCodedFrameGroup is called but no audio Append() follows. This leaves range_for_next_append_ pointing to Range A and last_appended_buffer_timestamp_ set to kNoTimestamp.
The guard at line 1009 compares end_timestamp == last_appended_buffer_timestamp_, which does not match kNoTimestamp, so the branch that resets range_for_next_append_ = ranges_.end() is not taken.
Trigger Sequence
All steps use the standard MSE API from JavaScript. No user interaction beyond page navigation is required.
Step 1: Create Range A at [0, 2s] with audio+video data (~6KB audio).
Step 2: Fill Range B at [100+] with ~12MB of padded audio, near the kDemuxerStreamAudioMemoryLimitDefault limit.
Step 3: Call abort(), then append a video-only cluster at t=0. The audio OnStartOfCodedFrameGroup sets range_for_next_append_ to Range A and last_appended to kNoTimestamp.
Step 4: Seek to 50s (gap between A and B). Sets seek_pending_ = true, selected_range_ = nullptr.
Step 5: Append exceeds 12MB β EvictCodedFrames β FreeBuffers drains Range A then calls pop_front(). The iterator is now dangling.
Step 6: Subsequent operation dereferences the dangling range_for_next_append_. In AppendBuffersToEnd(), a WRITE is performed on freed memory.
Heap Spray: this Pointer Control
The heap spray PoC (poc_spray.html + ws_server.py) reclaims the freed 24-byte std::list node with WebSocket message data buffers (FastMalloc(24), same g_roots partition and 32-byte bucket).
appendBuffer() splits internally into sync and async phases:
- Sync (
PrepareAppend): GC βFreeBuffersβ list node freed updatestartevent β JavaScript execution β heap spray- Async (
AppendBufferAsyncPart):Append()β dereferencesrange_for_next_append_
Register dump confirms this is set to the spray payload value:
r14: 00000eadbeefcafe
di: 00000eadbeefcafe β this in AppendBuffersToEnd()
ip: <AppendBuffersToEnd+0x17> β mov 0x18(%rdi),%rax
AppendBuffersToEnd() reads this->buffers_.begin_ and this->buffers_.end_, then calls emplace_back. When this is controlled, this becomes a write to a controlled address.
Reproduction
Test Builds
Three Chromium builds were used.
-
Release-ASAN build (Chromium 146.0.7680.177, Linux x64)
python3 tools/get_asan_chrome/get_asan_chrome.py --version 146.0.7680.177 --os linuxBuild config:
is_asan=true, is_debug=false, is_component_build=falseBuildId:46d49f5b6958dc43 -
Debug-ASAN build (Chromium 146.0.7680.177, Linux x64)
python3 tools/get_asan_chrome/get_asan_chrome.py --version 146.0.7680.177 --os linux-debugBuild config:
is_asan=true, is_debug=true, is_component_build=true, symbol_level=1 -
Non-ASAN release build (Chromium 146.0.7680.0, Linux x64) Downloaded from Chrome for Testing:
wget https://storage.googleapis.com/chrome-for-testing-public/146.0.7680.0/linux64/chrome-linux64.zip unzip chrome-linux64.zip
How to Run
# Generate binary segments
python3 prepare_webm.py
# Release-ASAN β heap-use-after-free
./run_asan.sh /path/to/chromium-asan
# Debug-ASAN β DCHECK failure
./run_debug.sh /path/to/chromium-debug-asan
# Non-ASAN release β UAF WRITE crash (signal 11)
./run_noasan.sh /path/to/chrome-linux
# Heap spray β this = 0x0EADBEEFCAFE (requires: pip install websockets)
python3 run_spray.py
Crash Logs
1. Release-ASAN: heap-use-after-free READ
=================================================================
==24239==ERROR: AddressSanitizer: heap-use-after-free on address 0x7b9c906a5c00 at pc 0x5620a2163fc6 bp 0x7ffdb8c595f0 sp 0x7ffdb8c595e8
READ of size 8 at 0x7b9c906a5c00 thread T0 (chrome)
SCARINESS: 51 (8-byte-read-heap-use-after-free)
#0 0x5620a2163fc5 (chrome+0x14d8afc5)
#1 0x5620a207ea47 (chrome+0x14ca5a47)
#2 0x5620a212b0c5 (chrome+0x14d520c5)
#3 0x5620a212c762 (chrome+0x14d53762)
#4 0x5620a215c526 (chrome+0x14d83526)
#5 0x5620a215f1e8 (chrome+0x14d861e8)
#6 0x5620a1fe133f (chrome+0x14c0833f)
#7 0x5620a2022f70 (chrome+0x14c49f70)
#8 0x5620a20220e3 (chrome+0x14c490e3)
#9 0x5620a2154e91 (chrome+0x14d7be91)
#10 0x5620a208ec2d (chrome+0x14cb5c2d)
0x7b9c906a5c00 is located 16 bytes inside of 24-byte region [0x7b9c906a5bf0,0x7b9c906a5c08)
freed by thread T0 here:
#0 0x5620a216b8b9 (chrome+0x14d928b9)
#1 0x5620a216a829 (chrome+0x14d91829)
#2 0x5620a207eeff (chrome+0x14ca5eff)
#3 0x5620a2155f0f (chrome+0x14d7cf0f)
#4 0x5620a208e4ff (chrome+0x14cb54ff)
SUMMARY: AddressSanitizer: heap-use-after-free (chrome+0x14d8afc5)
MiraclePtr Status: NOT PROTECTED
No raw_ptr<T> access to this region was detected prior to this crash.
This crash is still exploitable with MiraclePtr.
2. Debug-ASAN: DCHECK failure
[20374:20374:0408/014944.743133:FATAL:media/filters/source_buffer_stream.cc(1027)]
DCHECK failed: range_for_next_append_ == ranges_.end() ||
range_for_next_append_->get() != current_range
#0 0x55a8fcbb3e56 (chrome+0xf04ae55)
#1 0x7f703ba15bdb (libbase.so+0x1363bda)
...
#11 0x7f7006a65eed (libmedia.so+0x1a60eec) β SourceBufferStream::FreeBuffers
#12 0x7f7006a62245 (libmedia.so+0x1a5d244)
#13 0x7f7006840ef3 (libmedia.so+0x183bef2)
#14 0x7f7006a22d68 (libmedia.so+0x1a1dd67)
#15 0x7f7006857801 (libmedia.so+0x1852800)
Received signal 6
3. Non-ASAN Release: UAF WRITE crash (Signal 11)
Received signal 11 SI_KERNEL000000000000
Possibly a General Protection Fault
#0 0x55daf10b2e92 base::debug::CollectStackTrace()
#1 0x55daf109f47e base::debug::StackTrace::StackTrace()
#2 0x55daf10b2908 base::debug::(anonymous namespace)::StackDumpSignalHandler()
#3 0x7f378eabe330 (/usr/lib/x86_64-linux-gnu/libc.so.6+0x4532f)
#4 0x55daec513f57 media::SourceBufferRange::AppendBuffersToEnd()
#5 0x55daec51cb05 media::SourceBufferStream::Append()
#6 0x55daec4eb688 media::ChunkDemuxerStream::Append()
#7 0x55daec50ebc3 media::MseTrackBuffer::FlushProcessedFrames()
#8 0x55daec50efd2 media::FrameProcessor::ProcessFrames()
#9 0x55daec51bad7 media::SourceBufferState::OnNewBuffers()
#10 0x55daeb6185c7 base::RepeatingCallback<>::Run()
#11 0x55daec4d30f9 media::WebMStreamParser::ParseCluster()
#12 0x55daec4d2bce media::WebMStreamParser::Parse()
#13 0x55daec5192d3 media::SourceBufferState::RunSegmentParserLoop()
...
#16 0x55daf6b88bfc blink::SourceBuffer::AppendBufferAsyncPart_Locked()
Registers:
r14: badbad00badbad00 r15: 000017b4006a8c40
di: badbad00badbad00 (this = PartitionAlloc freed memory poison)
si: 000017b4006a8c40 (StreamParserBuffer pointer to be appended)
ax: 000017b4009e0100
ip: 000055daec513f57 β mov 0x18(%rdi),%rax
The crash instruction is mov 0x18(%rdi),%rax, reading buffers_.begin_ from this + 0x18. Since rdi is the PartitionAlloc poison 0xbadbad00badbad00, the read targets 0xbadbad00badbad18 (non-canonical), causing GPF. If this pointed to controlled memory, the value loaded into rax would become the target address for the subsequent emplace_back write, with the StreamParserBuffer pointer in si written to that address.
4. Non-ASAN + Heap Spray: this Pointer Control
Received signal 11 SEGV_MAPERR 0eadbeefcb16
#4 media::SourceBufferRange::AppendBuffersToEnd()
#5 media::SourceBufferStream::Append()
#6 media::ChunkDemuxerStream::Append()
#7 media::MseTrackBuffer::FlushProcessedFrames()
#8 media::FrameProcessor::ProcessFrames()
...
#16 blink::SourceBuffer::AppendBufferAsyncPart_Locked()
Registers:
r14: 00000eadbeefcafe r15: 000004b4008888e0
di: 00000eadbeefcafe (this)
si: 000004b4008888e0 (StreamParserBuffer pointer to be appended)
ax: 000004b400990c60
ip: β mov 0x18(%rdi),%rax
cr2: 00000eadbeefcb16 (= di + 0x18, faulting address)
cr2 = 0x0eadbeefcb16 = rdi + 0x18, the SEGV occurs reading this->buffers_.begin_. The freed list node slot was reclaimed by a WebSocket FastMalloc(24) spray payload with 0x0EADBEEFCAFE at offset 0x10, setting this to that value. If valid memory were mapped at the this address, the value loaded into rax would become the target address for emplace_back, and the StreamParserBuffer pointer in si would be written there.
Suggested Fix
Reset range_for_next_append_ before popping in FreeBuffers(), matching DeleteAndRemoveRange():
--- a/media/filters/source_buffer_stream.cc
+++ b/media/filters/source_buffer_stream.cc
@@ -1024,6 +1024,11 @@
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);
+ if (range_for_next_append_ != ranges_.end() &&
+ range_for_next_append_->get() == current_range) {
+ range_for_next_append_ = ranges_.end();
+ ResetLastAppendedState();
+ }
// Delete |current_range| by popping it out of |ranges_|.
reverse_direction ? ranges_.pop_back() : ranges_.pop_front();
Impact analysis
A heap-use-after-free in the renderer process reachable from JavaScript via the MSE API. Triggered by page navigation without further user interaction. The freed 24-byte std::list node is not MiraclePtr-protected. The heap spray PoC shows that the freed slot is reclaimed with controlled content, with the this pointer in AppendBuffersToEnd() set to the spray payload value. Since AppendBuffersToEnd() reads buffers_ members from this and calls emplace_back, controlling this provides a path to write at a controlled address.
AAW appears to be achievable, but we have not yet verified whether this leads to RCE. We plan to continue analysis from this point.
The cause
What version of Chrome have you found the security issue in?
146.0.7680.177
Is the security issue related to a crash?
Yes, it is related to a crash.
Choose the type of vulnerability
Memory Corruption (in a sandboxed process)
How would you like to be publicly acknowledged for your report?
Jungwoo Lee (@physicube) and Wongi Lee (@_qwerty_po)