Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in MediaRecording
DescriptionUse after free in MediaRecording
ComponentMediaRecording
Bug ClassUAF
Tracker504660052
Fix commit6184f4484a82 (webm/libwebm) +83/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Changed Functions

FunctionChangeNotes
FailingWriter
testing/mkvmuxer_test.cc
modified
if
testing/mkvmuxer_test.cc
modified
TEST
testing/mkvmuxer_test.cc
modified
MuxerTest
testing/mkvmuxer_test.cc
modified

Files Changed

  • mkvmuxer/mkvmuxer.cc
  • testing/mkvmuxer_test.cc
From 6184f4484a826724b5293837134ab9492261b941 Mon Sep 17 00:00:00 2001
From: [email protected] <[email protected]>
Date: Tue, 21 Apr 2026 09:35:31 -0700
Subject: [PATCH] mkvmuxer: fix UAF and double-free in WriteFramesLessThan

Failures in writing frames within `Segment::WriteFramesLessThan` failed
to increment the `shift_left` compaction counter. This left deleted
frame pointers in the array as dangling references, triggering
heap-use-after-free or double-free on cleanup when subsequent
frames are written or the segment is destroyed.

This patch ensures the `shift_left` array shift bounds counter is
incremented for each error handling failure path where deletion is
triggered without immediate break from loop.

Also add unit tests that will fail without this fix.

Bug: 504660052
Change-Id: I345c90b1ff10bfe82c7ede1f5ec5a4d5f8ee318a
---

diff --git a/mkvmuxer/mkvmuxer.cc b/mkvmuxer/mkvmuxer.cc
index 56e21fb..9006ef0 100644
--- a/mkvmuxer/mkvmuxer.cc
+++ b/mkvmuxer/mkvmuxer.cc
@@ -4259,12 +4259,14 @@
         doc_type_version_ = 4;
       if (!cluster->AddFrame(frame_prev)) {
         delete frame_prev;
+        ++shift_left;
         continue;
       }
 
       if (new_cuepoint_ && cues_track_ == frame_prev->track_number()) {
         if (!AddCuePoint(frame_prev->timestamp(), cues_track_)) {
           delete frame_prev;
+          ++shift_left;
           continue;
         }
       }
diff --git a/testing/mkvmuxer_test.cc b/testing/mkvmuxer_test.cc
index 824bd1f..71719cc 100644
--- a/testing/mkvmuxer_test.cc
+++ b/testing/mkvmuxer_test.cc
@@ -39,6 +39,87 @@
 
 namespace test {
 
+class FailingWriter : public mkvmuxer::IMkvWriter {
+ public:
+  mkvmuxer::int32 Write(const void* /*buf*/, mkvmuxer::uint32 len) override {
+    if (fail_ && len >= fail_min_len_) {
+      return -1;
+    }
+    pos_ += len;
+    return 0;
+  }
+  mkvmuxer::int64 Position() const override { return pos_; }
+  mkvmuxer::int32 Position(mkvmuxer::int64 position) override {
+    pos_ = position;
+    return 0;
+  }
+  bool Seekable() const override { return true; }
+  void ElementStartNotify(mkvmuxer::uint64, mkvmuxer::int64) override {}
+
+  void StartFailing(mkvmuxer::uint32 min_len) {
+    fail_ = true;
+    fail_min_len_ = min_len;
+  }
+
+ private:
+  mkvmuxer::int64 pos_ = 0;
+  bool fail_ = false;
+  mkvmuxer::uint32 fail_min_len_ = 0;
+};
+
+constexpr uint64_t kMs = 1000000ULL;  // 1 ms in ns (default timecode_scale).
+constexpr uint64_t kAudioFrameLen = 100;  // > all EBML metadata writes.
+constexpr uint64_t kVideoFrameLen = 8;
+
+// Test case from crbug.com/504660052.
+TEST(MkvmuxerUafTest, WriteFramesLessThanDanglingSlot_UseAfterFreeRead) {
+  FailingWriter writer;
+  mkvmuxer::Segment segment;
+  ASSERT_TRUE(segment.Init(&writer));
+
+  const uint64_t vtrack = segment.AddVideoTrack(320, 240, 1);
+  const uint64_t atrack = segment.AddAudioTrack(48000, 2, 2);
+  ASSERT_NE(vtrack, 0u);
+  ASSERT_NE(atrack, 0u);
+
+  uint8_t vdata[kVideoFrameLen] = {};
+  uint8_t adata[kAudioFrameLen] = {};
+
+  ASSERT_TRUE(segment.AddFrame(vdata, kVideoFrameLen, vtrack, 0 * kMs, true));
+  ASSERT_TRUE(segment.AddFrame(adata, kAudioFrameLen, atrack, 10 * kMs, true));
+  ASSERT_TRUE(segment.AddFrame(adata, kAudioFrameLen, atrack, 20 * kMs, true));
+  ASSERT_TRUE(segment.AddFrame(adata, kAudioFrameLen, atrack, 30 * kMs, true));
+
+  writer.StartFailing(/*min_len=*/50);
+
+  // It does not matter whether this succeeds or fails, it should not crash.
+  segment.AddFrame(vdata, kVideoFrameLen, vtrack, 40 * kMs, true);
+}
+
+// Test case from crbug.com/504660052.
+TEST(MkvmuxerUafTest, WriteFramesLessThanDanglingSlot_DoubleFree) {
+  FailingWriter writer;
+  mkvmuxer::Segment segment;
+  ASSERT_TRUE(segment.Init(&writer));
+
+  const uint64_t vtrack = segment.AddVideoTrack(320, 240, 1);
+  const uint64_t atrack = segment.AddAudioTrack(48000, 2, 2);
+  ASSERT_NE(vtrack, 0u);
+  ASSERT_NE(atrack, 0u);
+
+  uint8_t vdata[kVideoFrameLen] = {};
+  uint8_t adata[kAudioFrameLen] = {};
+
+  ASSERT_TRUE(segment.AddFrame(vdata, kVideoFrameLen, vtrack, 0 * kMs, true));
+  ASSERT_TRUE(segment.AddFrame(adata, kAudioFrameLen, atrack, 10 * kMs, true));
+  ASSERT_TRUE(segment.AddFrame(adata, kAudioFrameLen, atrack, 20 * kMs, true));
+  ASSERT_TRUE(segment.AddFrame(adata, kAudioFrameLen, atrack, 30 * kMs, true));
+
+  writer.StartFailing(/*min_len=*/0);
+
+  EXPECT_FALSE(segment.AddFrame(vdata, kVideoFrameLen, vtrack, 40 * kMs, true));
+}
+
 // Base class containing boiler plate stuff.
 class MuxerTest : public testing::Test {
  public:
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/testing/mkvmuxer_test.cc b/testing/mkvmuxer_test.cc
index 824bd1f..71719cc 100644
--- a/testing/mkvmuxer_test.cc
+++ b/testing/mkvmuxer_test.cc
@@ -39,6 +39,87 @@
 
 namespace test {
 
+class FailingWriter : public mkvmuxer::IMkvWriter {
+ public:
+  mkvmuxer::int32 Write(const void* /*buf*/, mkvmuxer::uint32 len) override {
+    if (fail_ && len >= fail_min_len_) {
+      return -1;
+    }
+    pos_ += len;
+    return 0;
+  }
+  mkvmuxer::int64 Position() const override { return pos_; }
+  mkvmuxer::int32 Position(mkvmuxer::int64 position) override {
+    pos_ = position;
+    return 0;
+  }
+  bool Seekable() const override { return true; }
+  void ElementStartNotify(mkvmuxer::uint64, mkvmuxer::int64) override {}
+
+  void StartFailing(mkvmuxer::uint32 min_len) {
+    fail_ = true;
+    fail_min_len_ = min_len;
+  }
+
+ private:
+  mkvmuxer::int64 pos_ = 0;
+  bool fail_ = false;
+  mkvmuxer::uint32 fail_min_len_ = 0;
+};
+
+constexpr uint64_t kMs = 1000000ULL;  // 1 ms in ns (default timecode_scale).
+constexpr uint64_t kAudioFrameLen = 100;  // > all EBML metadata writes.
+constexpr uint64_t kVideoFrameLen = 8;
+
+// Test case from crbug.com/504660052.
+TEST(MkvmuxerUafTest, WriteFramesLessThanDanglingSlot_UseAfterFreeRead) {
+  FailingWriter writer;
+  mkvmuxer::Segment segment;
+  ASSERT_TRUE(segment.Init(&writer));
+
+  const uint64_t vtrack = segment.AddVideoTrack(320, 240, 1);
+  const uint64_t atrack = segment.AddAudioTrack(48000, 2, 2);
+  ASSERT_NE(vtrack, 0u);
+  ASSERT_NE(atrack, 0u);
+
+  uint8_t vdata[kVideoFrameLen] = {};
+  uint8_t adata[kAudioFrameLen] = {};
+
+  ASSERT_TRUE(segment.AddFrame(vdata, kVideoFrameLen, vtrack, 0 * kMs, true));
+  ASSERT_TRUE(segment.AddFrame(adata, kAudioFrameLen, atrack, 10 * kMs, true));
+  ASSERT_TRUE(segment.AddFrame(adata, kAudioFrameLen, atrack, 20 * kMs, true));
+  ASSERT_TRUE(segment.AddFrame(adata, kAudioFrameLen, atrack, 30 * kMs, true));
+
+  writer.StartFailing(/*min_len=*/50);
+
+  // It does not matter whether this succeeds or fails, it should not crash.
+  segment.AddFrame(vdata, kVideoFrameLen, vtrack, 40 * kMs, true);
+}
+
+// Test case from crbug.com/504660052.
+TEST(MkvmuxerUafTest, WriteFramesLessThanDanglingSlot_DoubleFree) {
+  FailingWriter writer;
+  mkvmuxer::Segment segment;
+  ASSERT_TRUE(segment.Init(&writer));
+
+  const uint64_t vtrack = segment.AddVideoTrack(320, 240, 1);
+  const uint64_t atrack = segment.AddAudioTrack(48000, 2, 2);
+  ASSERT_NE(vtrack, 0u);
+  ASSERT_NE(atrack, 0u);
+
+  uint8_t vdata[kVideoFrameLen] = {};
+  uint8_t adata[kAudioFrameLen] = {};
+
+  ASSERT_TRUE(segment.AddFrame(vdata, kVideoFrameLen, vtrack, 0 * kMs, true));
+  ASSERT_TRUE(segment.AddFrame(adata, kAudioFrameLen, atrack, 10 * kMs, true));
+  ASSERT_TRUE(segment.AddFrame(adata, kAudioFrameLen, atrack, 20 * kMs, true));
+  ASSERT_TRUE(segment.AddFrame(adata, kAudioFrameLen, atrack, 30 * kMs, true));
+
+  writer.StartFailing(/*min_len=*/0);
+
+  EXPECT_FALSE(segment.AddFrame(vdata, kVideoFrameLen, vtrack, 40 * kMs, true));
+}
+
 // Base class containing boiler plate stuff.
 class MuxerTest : public testing::Test {
  public:
Loading diff…

Original Bug Report

reported by [email protected]

Potential UAF and Double-Free in libwebm Segment::WriteFramesLessThan

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A logic error in libwebm’s Segment::WriteFramesLessThan fails to correctly compact a frame queue when disk writes fail, leaving dangling pointers. In ChromeOS, an attacker can trigger this by filling the disk and waiting for the user to initiate a screen recording. This results in a Use-After-Free and Double-Free in the unsandboxed RecordingService utility process.

Affected files:

  • third_party/libwebm/source/mkvmuxer/mkvmuxer.cc
  • chromeos/ash/services/recording/webm_encoder_muxer.cc
  • media/muxers/file_webm_muxer_delegate.cc
  • media/muxers/muxer_timestamp_adapter.cc

Estimated timestamp from git blame: 2020-11-24

Summary

A potential Use-After-Free (UAF) and Double-Free vulnerability exists in libwebm, specifically within the mkvmuxer::Segment::WriteFramesLessThan function. When the muxer attempts to write queued frames to a cluster and the write fails (e.g., due to an I/O error like ENOSPC), the code deletes the frame object but fails to properly update the queue state. This leaves dangling pointers in the array, which are subsequently accessed and freed again.

Root Cause Analysis

In third_party/libwebm/source/mkvmuxer/mkvmuxer.cc, the function Segment::WriteFramesLessThan iterates through a queue of audio frames (frames_). If writing the frame to the cluster fails, the frame is deleted. However, the shift_left variable, which tracks how many frames have been removed from the queue for subsequent array compaction, is not incremented:

// third_party/libwebm/source/mkvmuxer/mkvmuxer.cc:4153-4156
if (!cluster->AddFrame(frame_prev)) {
  delete frame_prev;      // The object is deleted.
  continue;               // BUG: shift_left is NOT incremented.
}

Because shift_left remains 0 (if all queued frames fail to write), the array compaction logic at the end of the function is bypassed:

// mkvmuxer.cc:4175
if (shift_left > 0) { // Evaluates to false
  // ... array compaction logic ...
  frames_size_ = new_frames_size;
}

Consequently, frames_size_ remains unchanged, and the frames_ array contains dangling pointers to the already-deleted Frame objects. The function then incorrectly returns true (success).

Suggested Exploitation Path

Note: These are potential steps based on static code analysis; a working proof-of-concept has not been executed.

  1. Disk Exhaustion: An attacker on a malicious webpage aggressively consumes the ChromeOS device’s local disk space (e.g., via the Cache API) until the device is entirely out of space.
  2. User Interaction: The attacker convinces the user to start a ChromeOS Capture Mode screen recording (capturing screen and audio).
  3. Muxer Failure: The browser launches the RecordingService, which runs as an unsandboxed utility process (kNoSandbox). As recording progresses, audio frames are queued. When a video key-frame triggers the creation of a new cluster, Segment::MakeNewCluster calls WriteFramesLessThan to flush the queued audio frames.
  4. Dangling Pointers: Because the disk is full, FileWebmMuxerDelegate::DoWrite returns -1, causing cluster->AddFrame to fail. The Frame objects are deleted, but left in the frames_ array.
  5. UAF Read: WriteFramesLessThan returns control to MakeNewCluster, which immediately executes a Use-After-Free read by accessing the timestamp of the dangling pointer (frames_[0]->timestamp()).
  6. Double-Free: MakeNewCluster returns to DoNewClusterProcessing, which then calls WriteFramesAll(). This function iterates over the corrupted frames_ array and again calls cluster->AddFrame on the dangling pointers. The write fails again (disk still full), and the error handler in WriteFramesAll executes delete frame; on the already-freed pointer.
  7. Arbitrary Free / RCE: The Frame destructor executes delete[] frame_; and delete[] additional_;. If the attacker can groom the heap to reallocate the Frame object between the two frees, they can control these raw pointers to execute an arbitrary free primitive, leading to potential RCE in the unsandboxed RecordingService.

Suggested Fix

Ensure shift_left is incremented when a frame write fails so the array is correctly compacted:

if (!cluster->AddFrame(frame_prev)) {
  delete frame_prev;
  ++shift_left; // FIX: Increment counter to ensure compaction
  continue;
}

The same fix should be applied to the !AddCuePoint(...) failure case immediately below it.

Evaluated with Chrome root at commit: 7353d249d9cacf9c7218e1d7b8a39cf39c72d646


Results 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