Medium patch-now ~ plausible CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Media
DescriptionUse after free in Media
ComponentMedia
Sub-componentMSE
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (plausible) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIPlausible
Reviewer notes: 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.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker498285711
Fix commit055b8ce56f2e (chromium/src) +38/-5
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-31 (per vendor advisory)
Fix landed2026-04-08 (public git)
Disclosed2026-04-28
Patch gap20 days — fix public in source before the advisory

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 (including range_for_next_append_) so none are left dangling.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
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.

Key insight
The core mistake was deleting a buffered range during GC with a raw container pop that did not fix up range_for_next_append_, leaving that iterator dangling; the fix routes deletion through DeleteAndRemoveRange, which updates the iterator to ranges_.end().

Attack Path

  1. 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 NewCodedFrameGroupAppend calls.
  2. Point the append iterator at Range A Start a new coded-frame group overlapping Range A (OnStartOfCodedFrameGroup), which sets range_for_next_append_ to Range A.
  3. 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_.
  4. 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, a CHECK crash), as the test’s final AppendBuffers demonstrates.

Impact Assessment

An attacker controlling MSE append/eviction from a web page can cause a use-after-free of a freed buffered-range object in the renderer’s media pipeline, which the vendor advisory rates medium severity. The precondition is driving the SourceBuffer into GC such that the range referenced by 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

FunctionChangeNotes
if
media/filters/source_buffer_stream.cc
modified
TEST_F
media/filters/source_buffer_stream_unittest.cc
modified

Files Changed

  • media/filters/source_buffer_stream.cc
  • media/filters/source_buffer_stream_unittest.cc

Audit Directions

  • Stored iterators across mutation
    Audit 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 helper
    Look for direct pop_front/pop_back/erase calls where a dedicated removal helper (here DeleteAndRemoveRange) exists precisely to keep related state consistent, and the raw call bypasses it.
  • GC / eviction lifetime
    Review 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.
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • media/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:

  1. 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]).
  2. Reset Parser: Call SourceBuffer.abort() to reset the parser state and end the current coded frame group.
  3. Trigger New Coded Frame Group (Video-Only): Append a new media segment that contains only video frames. The FrameProcessor detects the new coded frame group and broadcasts NotifyStartOfCodedFrameGroup to all track buffers.
  4. Audio Stream State Update: For the audio track, SourceBufferStream::OnStartOfCodedFrameGroup sets range_for_next_append_ to an existing overlapping range and calls ResetLastAppendedState(), which sets last_appended_buffer_timestamp_ to kNoTimestamp.
  5. Trigger Garbage Collection: Append a large amount of video data to exceed the SourceBuffer’s memory limit. This triggers ChunkDemuxerStream::EvictCodedFrames() and subsequently SourceBufferStream::FreeBuffers() for the audio stream.
  6. Bypass Safety Check: FreeBuffers() deletes the range pointed to by range_for_next_append_. Because last_appended_buffer_timestamp_ is kNoTimestamp, the protective check is bypassed, and the range is popped while leaving range_for_next_append_ dangling.
  7. Trigger UAF: Append a new media segment containing audio frames without calling abort(). SourceBufferStream::Append() is called, which dereferences the dangling range_for_next_append_ iterator to call AppendBuffersToEnd.

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.

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.

View on issue tracker