CVE-2026-10946
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Pmedia/cast/encoding/video_encoder_unittest.cc |
modified | |
BindOncemedia/cast/encoding/video_encoder_unittest.cc |
modified |
Files Changed
media/cast/encoding/video_encoder_unittest.ccmedia/cast/encoding/vpx_encoder.ccmedia/cast/encoding/vpx_encoder.h
Patch
From c79b17a08c9ae3984d134d7e23301f4c01ad247e Mon Sep 17 00:00:00 2001 From: Jordan Bayles <[email protected]> Date: Wed, 22 Apr 2026 15:51:22 -0700 Subject: [PATCH] media/cast: Fix Heap OOB in VpxEncoder during resize Identified that VpxEncoder was reusing libvpx instances based on frame area, which is unsafe for VP9 if a dimension increases. Updated the reuse logic in ConfigureForNewFrameSize to check both width and height against the last initialized size (last_init_size_). Bug: 504587797 Change-Id: I4df4c1b28742d32ec50068d01e6365d66e7c7688 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7779614 Reviewed-by: Muyao Xu <[email protected]> Reviewed-by: Ted (Chromium) Meyer <[email protected]> Commit-Queue: Jordan Bayles <[email protected]> Cr-Commit-Position: refs/heads/main@{#1619153} --- diff --git a/media/cast/encoding/video_encoder_unittest.cc b/media/cast/encoding/video_encoder_unittest.cc index 7957ac9..897614e 100644 --- a/media/cast/encoding/video_encoder_unittest.cc +++ b/media/cast/encoding/video_encoder_unittest.cc @@ -375,6 +375,39 @@ } } +// Tests that the encoder can handle a frame size change that keeps the area the +// same but increases one of the dimensions (e.g., a rotation). This is a +// regression test for crbug.com/504587797. +TEST_P(VideoEncoderTest, EncodesRotatedFrameSize) { + if (is_testing_external_video_encoder() || + (GetParam().codec != VideoCodec::kVP8 && + GetParam().codec != VideoCodec::kVP9)) { + GTEST_SKIP() << "Skipping test for non-VP8/VP9 or external encoders."; + } + + CreateEncoder(); + SetVEAFactoryAutoRespond(true); + + const gfx::Size size1(128, 72); + const gfx::Size size2(72, 128); // Same area, but larger height. + + auto video_frame1 = CreateTestVideoFrame(size1); + EXPECT_TRUE(video_encoder()->EncodeVideoFrame( + std::move(video_frame1), NowTicks(), + base::BindOnce([](std::unique_ptr<SenderEncodedFrame> encoded_frame) { + EXPECT_TRUE(encoded_frame); + }))); + RunTasksAndAdvanceClock(); + + auto video_frame2 = CreateTestVideoFrame(size2); + EXPECT_TRUE(video_encoder()->EncodeVideoFrame( + std::move(video_frame2), NowTicks(), + base::BindOnce([](std::unique_ptr<SenderEncodedFrame> encoded_frame) { + EXPECT_TRUE(encoded_frame); + }))); + RunTasksAndAdvanceClock(); +} + // Verify that everything goes well even if ExternalVideoEncoder is destroyed // before it has a chance to receive the VEA creation callback. For all other // encoders, this tests that the encoder can be safely destroyed before the task diff --git a/media/cast/encoding/vpx_encoder.cc b/media/cast/encoding/vpx_encoder.cc index 7226784..1f467484 100644 --- a/media/cast/encoding/vpx_encoder.cc +++ b/media/cast/encoding/vpx_encoder.cc @@ -108,14 +108,17 @@ void VpxEncoder::ConfigureForNewFrameSize(const gfx::Size& frame_size) { if (is_initialized()) { - // NOTE: Do we need this workaround for VP9? - // Workaround for VP8 bug: If the new size is strictly less-than-or-equal to - // the old size, in terms of area, the existing encoder instance can - // continue. Otherwise, completely tear-down and re-create a new encoder to - // avoid a shutdown crash. - if (frame_size.GetArea() <= gfx::Size(config_.g_w, config_.g_h).GetArea()) { + // Workaround for libvpx bug: If the new size is strictly less-than-or-equal + // to the old size, in terms of both dimensions, the existing encoder + // instance can continue. Otherwise, completely tear-down and re-create a + // new encoder to avoid a shutdown crash or OOB read/write. + // More info can be found here: + // https://bugs.chromium.org/p/webm/issues/detail?id=1642 + // https://bugs.chromium.org/p/webm/issues/detail?id=912 + if (frame_size.width() <= last_init_size_->width() && + frame_size.height() <= last_init_size_->height()) { DVLOG(1) << "Continuing to use existing encoder at smaller frame size: " - << gfx::Size(config_.g_w, config_.g_h).ToString() << " --> " + << last_init_size_->ToString() << " --> " << frame_size.ToString(); config_.g_w = frame_size.width(); config_.g_h = frame_size.height(); @@ -128,9 +131,9 @@ } DVLOG(1) << "Destroying/Re-Creating encoder for larger frame size: " - << gfx::Size(config_.g_w, config_.g_h).ToString() << " --> " - << frame_size.ToString(); + << last_init_size_->ToString() << " --> " << frame_size.ToString(); vpx_codec_destroy(&encoder_); + last_init_size_.reset(); } else { DVLOG(1) << "Creating encoder for the first frame; size: " << frame_size.ToString(); @@ -186,6 +189,8 @@ {media::EncoderStatus::Codes::kEncoderInitializationError, base::StrCat( {"libvpx failed to initialize: ", vpx_codec_err_to_string(ret)})}); + } else { + last_init_size_ = frame_size; } // Raise the threshold for considering macroblocks as static. The default is @@ -238,7 +243,7 @@ // Initialize on-demand. Later, if the video frame size has changed, update // the encoder configuration. const gfx::Size frame_size = video_frame->visible_rect().size(); - if (!is_initialized() || gfx::Size(config_.g_w, config_.g_h) != frame_size) { + if (!is_initialized() || *last_init_size_ != frame_size) { ConfigureForNewFrameSize(frame_size); } diff --git a/media/cast/encoding/vpx_encoder.h b/media/cast/encoding/vpx_encoder.h index 7053c946..7eda7c1 100644 --- a/media/cast/encoding/vpx_encoder.h +++ b/media/cast/encoding/vpx_encoder.h @@ -7,6 +7,8 @@ #include <stdint.h> +#include <optional> + #include "base/memory/raw_ref.h" #include "base/threading/thread_checker.h" #include "base/time/time.h" @@ -45,12 +47,7 @@ void GenerateKeyFrame() final; private: - bool is_initialized() const { - // ConfigureForNewFrameSize() sets the timebase denominator value to - // non-zero if the encoder is successfully initialized, and it is zero - // otherwise. - return config_.g_timebase.den != 0; - } + bool is_initialized() const { return last_init_size_.has_value(); } // If the |encoder_| is live, attempt reconfiguration to allow it to encode // frames at a new |frame_size|. Otherwise, tear it down and re-create a new @@ -91,6 +88,9 @@ // The higher the speed, the less CPU usage, and the lower quality. int encoding_speed_; + + // The size used for the last initialization of the encoder. + std::optional<gfx::Size> last_init_size_; }; } // namespace cast
Regression Test / PoC
diff --git a/media/cast/encoding/video_encoder_unittest.cc b/media/cast/encoding/video_encoder_unittest.cc
index 7957ac9..897614e 100644
--- a/media/cast/encoding/video_encoder_unittest.cc
+++ b/media/cast/encoding/video_encoder_unittest.cc
@@ -375,6 +375,39 @@
}
}
+// Tests that the encoder can handle a frame size change that keeps the area the
+// same but increases one of the dimensions (e.g., a rotation). This is a
+// regression test for crbug.com/504587797.
+TEST_P(VideoEncoderTest, EncodesRotatedFrameSize) {
+ if (is_testing_external_video_encoder() ||
+ (GetParam().codec != VideoCodec::kVP8 &&
+ GetParam().codec != VideoCodec::kVP9)) {
+ GTEST_SKIP() << "Skipping test for non-VP8/VP9 or external encoders.";
+ }
+
+ CreateEncoder();
+ SetVEAFactoryAutoRespond(true);
+
+ const gfx::Size size1(128, 72);
+ const gfx::Size size2(72, 128); // Same area, but larger height.
+
+ auto video_frame1 = CreateTestVideoFrame(size1);
+ EXPECT_TRUE(video_encoder()->EncodeVideoFrame(
+ std::move(video_frame1), NowTicks(),
+ base::BindOnce([](std::unique_ptr<SenderEncodedFrame> encoded_frame) {
+ EXPECT_TRUE(encoded_frame);
+ })));
+ RunTasksAndAdvanceClock();
+
+ auto video_frame2 = CreateTestVideoFrame(size2);
+ EXPECT_TRUE(video_encoder()->EncodeVideoFrame(
+ std::move(video_frame2), NowTicks(),
+ base::BindOnce([](std::unique_ptr<SenderEncodedFrame> encoded_frame) {
+ EXPECT_TRUE(encoded_frame);
+ })));
+ RunTasksAndAdvanceClock();
+}
+
// Verify that everything goes well even if ExternalVideoEncoder is destroyed
// before it has a chance to receive the VEA creation callback. For all other
// encoders, this tests that the encoder can be safely destroyed before the task
Original Bug Report
Potential Heap OOB Read/Write in libvpx VP9 Encoder via Cast Mirroring
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 potential heap out-of-bounds read and write exists in the libvpx VP9 encoder when frame dimensions change. This occurs because an internal state buffer is not resized when the macroblock grid expands, but is subsequently accessed using offsets from the new dimensions. An attacker can trigger this via Cast Mirroring by manipulating the capture aspect ratio to bypass Chromium’s area-based frame size checks.
Affected files:
third_party/libvpx/source/libvpx/vp9/encoder/vp9_speed_features.cthird_party/libvpx/source/libvpx/vp9/encoder/vp9_encoder.cthird_party/libvpx/source/libvpx/vp9/encoder/vp9_encodeframe.cmedia/cast/encoding/vpx_encoder.cc
Estimated timestamp from git blame: 2024-12-23
Description
A potential heap out-of-bounds (OOB) memory corruption vulnerability exists in the libvpx VP9 encoder. The issue is reachable from web content via Chromium’s Cast Mirroring feature, leading to potential Remote Code Execution (RCE) in the sandboxed Mirroring utility process.
The vulnerability is the result of two interacting flaws:
-
Chromium Cast Guard Bypass: In
media/cast/encoding/vpx_encoder.cc, theVpxEncoder::ConfigureForNewFrameSizefunction attempts to reuse an existing encoder instance if the new frame size is smaller or equal in total area to the current frame size:if (frame_size.GetArea() <= gfx::Size(config_.g_w, config_.g_h).GetArea())This allows an attacker to radically increase the frame’s width (e.g., from 640x480 to 3840x80) without triggering an encoder restart, as long as the total pixel area does not increase. -
libvpx Reallocation Flaw: When libvpx receives a configuration update (
vp9_change_configinvp9_encoder.c) where the new macroblock grid size is larger than the allocated grid (cm->mi_alloc_size < new_mi_size), it partially reallocates context buffers. However, it fails to freecpi->content_state_sb_fd. At the end of this reallocation block,cpi->external_resizeis erroneously reset to0.
On the next frame, vp9_set_speed_features_framesize_dependent sees external_resize == 0 and re-enables use_source_sad = 1. It checks if cpi->content_state_sb_fd == NULL to determine if allocation is needed. Because the buffer was never freed, the pointer is non-null, and reallocation is skipped. The buffer remains sized for the original dimensions (e.g., 640x480).
During frame encoding, avg_source_sad (in vp9_encodeframe.c) processes 64x64 superblocks and calculates a buffer index (sb_offset) based on the new wider dimensions. This results in an index that exceeds the bounds of the stale content_state_sb_fd buffer, triggering a precise 1-byte increment (cpi->content_state_sb_fd[sb_offset]++) or zeroing operation out of bounds. A corresponding OOB read occurs in choose_partitioning.
Suggested Attacker Steps
(Note: These are potential steps based on code analysis; our tooling has not executed a working proof of concept.)
- An attacker hosts a malicious web page that prompts the user to initiate a Tab or Desktop Cast Mirroring session.
- The attacker initializes a video stream with a standard resolution (e.g., 640x480). Chromium allocates an 88-byte
content_state_sb_fdbuffer in libvpx. - The attacker uses JavaScript (
window.resizeTo(3840, 80)) or similar video-element manipulation to rapidly change the aspect ratio. - The Cast pipeline detects the size change. Since the area of 3840x80 is equal to 640x480, Chromium bypasses the encoder restart and forwards the new configuration to libvpx.
- libvpx expands its macroblock grid but leaves
content_state_sb_fdat its original 88-byte size. - By feeding carefully crafted video frames, the attacker controls the Sum of Absolute Differences (
tmp_sad) calculations during encoding. This dictates exactly which out-of-bounds bytes are incremented or zeroed by the encoder. - The attacker repeatedly exploits this primitive to manipulate adjacent heap metadata or object pointers (e.g., vtables), eventually achieving arbitrary code execution in the Mirroring utility process.
Suggested Fix
This vulnerability requires fixes in both Chromium and libvpx for defense-in-depth.
libvpx:
In third_party/libvpx/source/libvpx/vp9/encoder/vp9_encoder.c, within the if (cm->mi_alloc_size < new_mi_size) reallocation block (around line 2159), explicitly free the content_state_sb_fd buffer so it can be safely reallocated during the speed features check:
if (cm->mi_alloc_size < new_mi_size) {
vp9_free_context_buffers(cm);
// ...
vpx_free(cpi->content_state_sb_fd);
cpi->content_state_sb_fd = NULL;
// ...
}
Chromium:
In media/cast/encoding/vpx_encoder.cc, update VpxEncoder::ConfigureForNewFrameSize to ensure that neither dimension grows larger than the currently allocated configuration. Using GetArea() alone is unsafe when underlying memory structures rely on grid width/height independently.
if (frame_size.width() <= config_.g_w &&
frame_size.height() <= config_.g_h &&
frame_size.GetArea() <= gfx::Size(config_.g_w, config_.g_h).GetArea()) {
// Continue using existing encoder
}
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.