High patch-now ✔︎ verified CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in media
DescriptionUse after free in media
ComponentChromium
Sub-componentMSE
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — 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 AIConfirmed
Reviewer notes: Removed CHECK guarded exactly this condition, so in CHECK-enabled Chromium builds the demonstrated eviction path may surface as a controlled crash (DoS) rather than a silent UAF; mechanism otherwise matches diff.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker500387779
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
CreditedJungwoo Lee (@physicube) and Wongi Lee (@_qwerty_po)
Reported2026-04-07 (per vendor advisory)
Fix landed2026-04-08 (public git)
Disclosed2026-04-28
Patch gap20 days — fix public in source before the advisory

Background

Media Source Extensions (MSE)
the web API letting JavaScript feed media bytes to a SourceBuffer, buffered server-side by SourceBufferStream as a list of time ranges_.
`SourceBufferStream` / `SourceBufferRange`
ranges_ is a container of SourceBufferRange objects; range_for_next_append_ is an iterator into ranges_ marking where the next appended data should go.
Garbage collection (GC)
when buffered data exceeds the memory limit, the stream frees SourceBufferRanges (popping from the front or back) to reclaim memory.
`DeleteAndRemoveRange()`
a helper that removes a range from ranges_ and correctly fixes up dependent iterators such as range_for_next_append_ and selected_range_.

Root Cause Analysis

Inside the GC free loop (the enclosing function is not named in the shown hunk; it is the range-freeing routine in source_buffer_stream.cc), when a current_range reached zero memory usage it was removed by a bare reverse_direction ? ranges_.pop_back() : ranges_.pop_front();, guarded only by a CHECK asserting that range_for_next_append_ did not point at current_range. That assertion encoded a false invariant: after OnStartOfCodedFrameGroup sets range_for_next_append_ to a range (e.g. Range A) with last_appended_buffer_timestamp_ cleared, a subsequent GC can legitimately choose to evict exactly that range, so popping it leaves range_for_next_append_ dangling (or trips the CHECK). The regression test SourceBufferStreamTest.GarbageCollectionUpdatesRangeForNextAppend reproduces this: it points range_for_next_append_ at Range A, lowers the memory limit, GCs so Range A is evicted from the front, then appends — dereferencing the stale iterator.

The fix replaces the raw pop with DeleteAndRemoveRange(&range_to_delete) after nulling the local current_range, so removal goes through the helper that resets range_for_next_append_ to ranges_.end() when the deleted range is the one it referenced.

Key insight
The GC path removed a range with a raw pop_front()/pop_back() while assuming range_for_next_append_ could never point at the victim range; the fix routes deletion through DeleteAndRemoveRange(), which correctly updates (resets to ranges_.end()) the dependent iterator.

Attack Path

  1. Create two ranges JavaScript appends via MSE to build Range A at low timestamps and Range B far ahead, approaching/exceeding the buffer memory limit.
  2. Point the append iterator at the victim Call the operation that starts a coded frame group overlapping Range A (OnStartOfCodedFrameGroup), which sets range_for_next_append_ to Range A and clears last_appended_buffer_timestamp_.
  3. Force eviction of that range Drive GC (e.g. with current media time near Range B) so the front range (Range A) is freed via the unguarded pop_front(), leaving range_for_next_append_ dangling.
  4. Use-after-free on next append Append more data; the code dereferences the stale range_for_next_append_ iterator into freed ranges_ storage (or hits the pre-fix CHECK), yielding a UAF.

Impact Assessment

A web page that controls MSE append/evict timing can drive a use-after-free of a SourceBufferRange in the media pipeline; the preconditions are purely web-reachable buffer manipulation (append two distant ranges, start an overlapping coded frame group, then force GC to evict the targeted range). The vendor advisory rates this high severity with impact “Use after free in media” and reports no in-the-wild exploitation; the media stack runs in the renderer, and escalation beyond the UAF is (inferred) rather than demonstrated 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

  • Iterators across mutation
    Search MSE/SourceBufferStream for members holding iterators or pointers into ranges_ (range_for_next_append_, selected_range_) that persist across any GC/eviction or range-merge operation, and verify they are updated or reset when the referenced range is removed.
  • Raw container removal vs. helper
    Flag direct pop_front()/pop_back()/erase() on ranges_ that bypass DeleteAndRemoveRange(), since the helper is what maintains dependent-iterator invariants.
  • Assertion-as-invariant
    Treat a removed CHECK/DCHECK that asserted “state X never happens” as a hint to the real bug class: look for other code assuming an iterator can never point at a to-be-freed object.
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]

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
  • updatestart event → JavaScript execution → heap spray
  • Async (AppendBufferAsyncPart): Append() → dereferences range_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.

  1. 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 linux
    

    Build config: is_asan=true, is_debug=false, is_component_build=false BuildId: 46d49f5b6958dc43

  2. 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-debug
    

    Build config: is_asan=true, is_debug=true, is_component_build=true, symbol_level=1

  3. 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

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)

View on issue tracker