CVE-2026-7929
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
FailingWritertesting/mkvmuxer_test.cc |
modified | |
iftesting/mkvmuxer_test.cc |
modified | |
TESTtesting/mkvmuxer_test.cc |
modified | |
MuxerTesttesting/mkvmuxer_test.cc |
modified |
Files Changed
mkvmuxer/mkvmuxer.cctesting/mkvmuxer_test.cc
Patch
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:
Regression Test / PoC
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:
Original Bug Report
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.ccchromeos/ash/services/recording/webm_encoder_muxer.ccmedia/muxers/file_webm_muxer_delegate.ccmedia/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.
- 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.
- User Interaction: The attacker convinces the user to start a ChromeOS Capture Mode screen recording (capturing screen and audio).
- 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::MakeNewClustercallsWriteFramesLessThanto flush the queued audio frames. - Dangling Pointers: Because the disk is full,
FileWebmMuxerDelegate::DoWritereturns-1, causingcluster->AddFrameto fail. TheFrameobjects are deleted, but left in theframes_array. - UAF Read:
WriteFramesLessThanreturns control toMakeNewCluster, which immediately executes a Use-After-Free read by accessing the timestamp of the dangling pointer (frames_[0]->timestamp()). - Double-Free:
MakeNewClusterreturns toDoNewClusterProcessing, which then callsWriteFramesAll(). This function iterates over the corruptedframes_array and again callscluster->AddFrameon the dangling pointers. The write fails again (disk still full), and the error handler inWriteFramesAllexecutesdelete frame;on the already-freed pointer. - Arbitrary Free / RCE: The
Framedestructor executesdelete[] frame_;anddelete[] additional_;. If the attacker can groom the heap to reallocate theFrameobject between the two frees, they can control these raw pointers to execute an arbitrary free primitive, leading to potential RCE in the unsandboxedRecordingService.
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.