CVE-2026-87579
Overview
Background
- Macroblock (MB)
- a 16×16 pixel unit; H.264 measures frame dimensions in whole macroblocks (
PicWidthInMbs,FrameHeightInMbs), computed by rounding each pixel dimension up to a multiple of 16. - H.264 Level
- a profile constraint set that caps decoder/encoder resource use; each level defines
MaxFS(maximum frame size in macroblocks) plus per-dimension limits, so a valid stream must satisfy both total-count and width/height bounds. - `H264LevelToMaxFS`
- a
media/parsers/h264_level_limits.hhelper returning theMaxFSfor a given level; forH264SPS::kLevelIDC6p1it returns 139264, from which the per-side capsqrt(MaxFS * 8) ≈ 1055macroblocks is derived. - `OpenH264VideoEncoder`
- Chromium’s wrapper (
media/video/openh264_video_encoder.cc) around Cisco’s OpenH264 software encoder, driven by WebCodecs and WebRTC to encode frames of a caller-chosen resolution.
Root Cause Analysis
Before the patch, OpenH264VideoEncoder gated the configured resolution only through IsFrameSizeTooLarge, which tested the total macroblock count against kOpenH264MaxMBs (36864) and never checked either dimension independently. A resolution with a benign total but an extreme aspect ratio — e.g. 17280×256, which is only 1080×16 = 17280 macroblocks yet 1080 MBs wide — passed that check and was handed straight to SetUpOpenH264Params and the underlying OpenH264 library, violating the per-dimension limit (PicWidthInMbs/FrameHeightInMbs ≤ sqrt(MaxFS * 8)) that internal encoder buffers assume, leading to a buffer overflow.
The fix adds ValidateH264Resolution, invoked at both encoder-configuration call sites before SetUpOpenH264Params, which re-derives the macroblock dimensions in uint64_t, rejects a count exceeding Level 6.1 MaxFS, and — critically — rejects either dimension exceeding sqrt(MaxFS * 8), returning kEncoderUnsupportedConfig and bailing out before any encoder buffer is allocated. Separately, IsFrameSizeTooLarge was hardened to compute mb_width and mb_height as uint64_t so the mb_width * mb_height product can no longer wrap around a signed 32-bit int and spuriously appear small.
mb_width/mb_height ≤ sqrt(MaxFS * 8)) plus overflow-safe uint64_t arithmetic.Attack Path
- Reach the encoder
Attacker-controlled JavaScript configures a WebCodecs
VideoEncoder(or a WebRTC sender) for an OpenH264-backed H.264 codec and supplies a chosen frame size, reachingOpenH264VideoEncoder’s configuration path. - Choose a lawful-total but extreme shape
The attacker picks a resolution such as 17280×256 whose macroblock total (17280) sits comfortably under
kOpenH264MaxMBs(36864) but whose width (1080 MBs) exceeds the Level 6.1 per-side cap of ~1055. - Bypass the only guard
IsFrameSizeTooLargechecks only the product and passes, so pre-patch the size flows unchecked intoSetUpOpenH264Paramsand the OpenH264 encoder. - Trigger the overflow (inferred) OpenH264 allocates or indexes internal per-row/per-column buffers sized on the assumption that dimensions respect level limits; the out-of-spec dimension drives an out-of-bounds write. The specific overflowed buffer is inside the OpenH264 library and is not shown by the diff.
Impact Assessment
(inferred) and not demonstrated by the patch, and no exploited-in-the-wild status is stated.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmedia/video/openh264_video_encoder.cc |
modified | |
OpenH264VideoEncoderResolutionTestmedia/video/software_video_encoder_test.cc |
modified | |
TEST_Fmedia/video/software_video_encoder_test.cc |
modified | |
BindLambdaForTestingmedia/video/software_video_encoder_test.cc |
modified | |
ifmedia/video/BUILD.gn |
modified |
Files Changed
media/video/openh264_video_encoder.ccmedia/video/BUILD.gnmedia/video/software_video_encoder_test.cc
Audit Directions
- Total-vs-dimension validation gapsAudit every codec/image path that validates only an aggregate (pixel count, macroblock total, byte size) while a downstream consumer assumes per-dimension caps; search for size checks that multiply width×height but never bound width or height alone.
- Signed macroblock/dimension arithmeticLook for
int-typed products of(dim + 15) / 16or similar dimension math feeding buffer sizing, which can wrap; the sameuint64_thardening should be applied wherever attacker-influenced dimensions are multiplied. - Third-party encoder/decoder wrappersReview other wrappers around external media libraries (VP8/VP9, AV1, hardware paths) to confirm Chromium enforces the codec’s level/dimension constraints itself rather than trusting the library to validate before allocating buffers.
Patch
From 96c76fee15bb6ea844fa8b3684ee23b49e4011f0 Mon Sep 17 00:00:00 2001
From: Erik Språng <sprang@chromium.org>
Date: Tue, 14 Jul 2026 02:55:58 -0700
Subject: [PATCH] WebCodecs: Validate H.264 resolution against configured level.
This CL adds validation that the configured resolution conforms to the
the AVC specification for that selected level used (see e.g.
https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels).
In particular, we validate sections A.3.1 (e), (f) and (g) of the
ITU-T H.264 specification which limits both the max total number of
macroblocks as well as the maximum width and height.
Bug: 504690157
Change-Id: I3ec3d75b1f21b44f299431b540a3d68c4864127a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8037024
Reviewed-by: Eugene Zemtsov <eugene@chromium.org>
Auto-Submit: Erik Språng <sprang@chromium.org>
Commit-Queue: Erik Språng <sprang@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1661715}
---
diff --git a/media/video/openh264_video_encoder.cc b/media/video/openh264_video_encoder.cc
index 3d158ba1..599a66d 100644
--- a/media/video/openh264_video_encoder.cc
+++ b/media/video/openh264_video_encoder.cc
@@ -5,6 +5,7 @@
#include "media/video/openh264_video_encoder.h"
#include <algorithm>
+#include <cmath>
#include <limits>
#include <numeric>
@@ -18,6 +19,8 @@
#include "media/base/video_aspect_ratio.h"
#include "media/base/video_frame.h"
#include "media/base/video_util.h"
+#include "media/parsers/h264_level_limits.h"
+#include "media/parsers/h264_parser.h"
#include "media/video/video_encoder_info.h"
namespace media {
@@ -155,8 +158,8 @@
constexpr int kOpenH264MaxMBs = 36864;
bool IsFrameSizeTooLarge(const gfx::Size& frame_size) {
- int mb_width = (frame_size.width() + 15) / 16;
- int mb_height = (frame_size.height() + 15) / 16;
+ uint64_t mb_width = (static_cast<uint64_t>(frame_size.width()) + 15) / 16;
+ uint64_t mb_height = (static_cast<uint64_t>(frame_size.height()) + 15) / 16;
return mb_width * mb_height > kOpenH264MaxMBs;
}
@@ -180,6 +183,38 @@
VideoAspectRatio::PAR(dst.width(), dst.height());
}
+// Validates that the frame size is within the limits defined by H.264
+// Level 6.1.
+EncoderStatus ValidateH264Resolution(const gfx::Size& frame_size) {
+ uint8_t level = H264SPS::kLevelIDC6p1;
+ uint32_t max_fs = H264LevelToMaxFS(level);
+ if (max_fs == 0) {
+ return EncoderStatus(EncoderStatus::Codes::kEncoderUnsupportedConfig,
+ "Invalid H.264 level");
+ }
+
+ uint64_t mb_width = (static_cast<uint64_t>(frame_size.width()) + 15) / 16;
+ uint64_t mb_height = (static_cast<uint64_t>(frame_size.height()) + 15) / 16;
+ uint64_t mb_count = mb_width * mb_height;
+
+ if (mb_count > max_fs) {
+ return EncoderStatus(
+ EncoderStatus::Codes::kEncoderUnsupportedConfig,
+ "Configured frame size exceeds H.264 level max macroblocks");
+ }
+
+ // Aspect ratio constraint from H.264 standard Annex A:
+ // PicWidthInMbs <= Sqrt(MaxFS * 8)
+ // FrameHeightInMbs <= Sqrt(MaxFS * 8)
+ double max_mb_dim = std::sqrt(static_cast<double>(max_fs) * 8.0);
+ if (mb_width > max_mb_dim || mb_height > max_mb_dim) {
+ return EncoderStatus(EncoderStatus::Codes::kEncoderUnsupportedConfig,
+ "Configured aspect ratio exceeds H.264 level limits");
+ }
+
+ return EncoderStatus::Codes::kOk;
+}
+
} // namespace
OpenH264VideoEncoder::ISVCEncoderDeleter::ISVCEncoderDeleter() = default;
@@ -273,6 +308,12 @@
return;
}
+ if (auto status = ValidateH264Resolution(options.frame_size);
+ !status.is_ok()) {
+ std::move(done_cb).Run(status);
+ return;
+ }
+
SetUpOpenH264Params(
profile_, options,
VideoColorSpace::FromGfxColorSpace(last_frame_color_space_), ¶ms);
@@ -552,6 +593,12 @@
return;
}
+ if (auto status = ValidateH264Resolution(options.frame_size);
+ !status.is_ok()) {
+ std::move(done_cb).Run(status);
+ return;
+ }
+
SEncParamExt params = {};
if (int err = codec_->GetDefaultParams(¶ms)) {
std::move(done_cb).Run(
diff --git a/media/video/software_video_encoder_test.cc b/media/video/software_video_encoder_test.cc
index d25df6ba0..e092c6a 100644
--- a/media/video/software_video_encoder_test.cc
+++ b/media/video/software_video_encoder_test.cc
@@ -1739,4 +1739,92 @@
EXPECT_EQ(GetDefaultVideoEncodeBitrate({1280, 720}, 1000u), 20'000'000u);
}
+#if BUILDFLAG(ENABLE_OPENH264)
+class OpenH264VideoEncoderResolutionTest : public ::testing::Test {
+ public:
+ OpenH264VideoEncoderResolutionTest() = default;
+
+ void SetUp() override { encoder_ = std::make_unique<OpenH264VideoEncoder>(); }
+
+ void TearDown() override { encoder_.reset(); }
+
+ protected:
+ base::test::TaskEnvironment task_environment_;
+ std::unique_ptr<OpenH264VideoEncoder> encoder_;
+};
+
+TEST_F(OpenH264VideoEncoderResolutionTest, HighestValidResolution) {
+ // 4096x2304 is exactly 36864 macroblocks, which is the OpenH264 limit.
+ VideoEncoder::Options options;
+ options.frame_size = gfx::Size(4096, 2304);
+
+ base::RunLoop run_loop;
+ encoder_->Initialize(H264PROFILE_BASELINE, options,
+ /*info_cb=*/base::DoNothing(),
+ /*output_cb=*/base::DoNothing(),
+ base::BindLambdaForTesting([&](EncoderStatus status) {
+ EXPECT_TRUE(status.is_ok());
+ run_loop.Quit();
+ }));
+ run_loop.Run();
+}
+
+TEST_F(OpenH264VideoEncoderResolutionTest, ResolutionExceedingMaxMBs) {
+ // 4097x2304 is 37008 macroblocks, exceeding the OpenH264 limit of 36864.
+ VideoEncoder::Options options;
+ options.frame_size = gfx::Size(4097, 2304);
+
+ base::RunLoop run_loop;
+ encoder_->Initialize(
+ H264PROFILE_BASELINE, options, /*info_cb=*/base::DoNothing(),
+ /*output_cb=*/base::DoNothing(),
+ base::BindLambdaForTesting([&](EncoderStatus status) {
+ EXPECT_EQ(status.code(),
+ EncoderStatus::Codes::kEncoderUnsupportedConfig);
+ run_loop.Quit();
+ }));
+ run_loop.Run();
+}
+
+TEST_F(OpenH264VideoEncoderResolutionTest, WidthExceeding6p1AspectRatioLimit) {
+ // A resolution that is extremely wide (e.g. 17280x256) has 1080x16
+ // macroblocks. This is only 17280 macroblocks (well within OpenH264's 36864
+ // limit), but its width (1080 MBs) exceeds the Level 6.1 limit of Sqrt(139264
+ // * 8) = 1055.
+ VideoEncoder::Options options;
+ options.frame_size = gfx::Size(17280, 256);
+
+ base::RunLoop run_loop;
+ encoder_->Initialize(
+ H264PROFILE_BASELINE, options, /*info_cb=*/base::DoNothing(),
+ /*output_cb=*/base::DoNothing(),
+ base::BindLambdaForTesting([&](EncoderStatus status) {
+ EXPECT_EQ(status.code(),
+ EncoderStatus::Codes::kEncoderUnsupportedConfig);
+ run_loop.Quit();
+ }));
+ run_loop.Run();
+}
+
+TEST_F(OpenH264VideoEncoderResolutionTest, HeightExceeding6p1AspectRatioLimit) {
+ // A resolution that is extremely tall (e.g. 256x17280) has 16x1080
+ // macroblocks. This is only 17280 macroblocks (well within OpenH264's 36864
+ // limit), but its height (1080 MBs) exceeds the Level 6.1 limit of
+ // Sqrt(139264 * 8) = 1055.
+ VideoEncoder::Options options;
+ options.frame_size = gfx::Size(256, 17280);
+
+ base::RunLoop run_loop;
+ encoder_->Initialize(
+ H264PROFILE_BASELINE, options, /*info_cb=*/base::DoNothing(),
+ /*output_cb=*/base::DoNothing(),
+ base::BindLambdaForTesting([&](EncoderStatus status) {
+ EXPECT_EQ(status.code(),
+ EncoderStatus::Codes::kEncoderUnsupportedConfig);
+ run_loop.Quit();
+ }));
+ run_loop.Run();
+}
+#endif
+
} // namespace media
diff --git a/media/video/BUILD.gn b/media/video/BUILD.gn
index 2b2771c7..b45f3f5b 100644
--- a/media/video/BUILD.gn
+++ b/media/video/BUILD.gn
@@ -80,6 +80,7 @@
"openh264_video_encoder.h",
]
public_deps += [ "//third_party/openh264:encoder" ]
+ deps += [ "//media/parsers" ]
}
if (is_apple) {
Regression Test / PoC
diff --git a/media/video/software_video_encoder_test.cc b/media/video/software_video_encoder_test.cc
index d25df6ba0..e092c6a 100644
--- a/media/video/software_video_encoder_test.cc
+++ b/media/video/software_video_encoder_test.cc
@@ -1739,4 +1739,92 @@
EXPECT_EQ(GetDefaultVideoEncodeBitrate({1280, 720}, 1000u), 20'000'000u);
}
+#if BUILDFLAG(ENABLE_OPENH264)
+class OpenH264VideoEncoderResolutionTest : public ::testing::Test {
+ public:
+ OpenH264VideoEncoderResolutionTest() = default;
+
+ void SetUp() override { encoder_ = std::make_unique<OpenH264VideoEncoder>(); }
+
+ void TearDown() override { encoder_.reset(); }
+
+ protected:
+ base::test::TaskEnvironment task_environment_;
+ std::unique_ptr<OpenH264VideoEncoder> encoder_;
+};
+
+TEST_F(OpenH264VideoEncoderResolutionTest, HighestValidResolution) {
+ // 4096x2304 is exactly 36864 macroblocks, which is the OpenH264 limit.
+ VideoEncoder::Options options;
+ options.frame_size = gfx::Size(4096, 2304);
+
+ base::RunLoop run_loop;
+ encoder_->Initialize(H264PROFILE_BASELINE, options,
+ /*info_cb=*/base::DoNothing(),
+ /*output_cb=*/base::DoNothing(),
+ base::BindLambdaForTesting([&](EncoderStatus status) {
+ EXPECT_TRUE(status.is_ok());
+ run_loop.Quit();
+ }));
+ run_loop.Run();
+}
+
+TEST_F(OpenH264VideoEncoderResolutionTest, ResolutionExceedingMaxMBs) {
+ // 4097x2304 is 37008 macroblocks, exceeding the OpenH264 limit of 36864.
+ VideoEncoder::Options options;
+ options.frame_size = gfx::Size(4097, 2304);
+
+ base::RunLoop run_loop;
+ encoder_->Initialize(
+ H264PROFILE_BASELINE, options, /*info_cb=*/base::DoNothing(),
+ /*output_cb=*/base::DoNothing(),
+ base::BindLambdaForTesting([&](EncoderStatus status) {
+ EXPECT_EQ(status.code(),
+ EncoderStatus::Codes::kEncoderUnsupportedConfig);
+ run_loop.Quit();
+ }));
+ run_loop.Run();
+}
+
+TEST_F(OpenH264VideoEncoderResolutionTest, WidthExceeding6p1AspectRatioLimit) {
+ // A resolution that is extremely wide (e.g. 17280x256) has 1080x16
+ // macroblocks. This is only 17280 macroblocks (well within OpenH264's 36864
+ // limit), but its width (1080 MBs) exceeds the Level 6.1 limit of Sqrt(139264
+ // * 8) = 1055.
+ VideoEncoder::Options options;
+ options.frame_size = gfx::Size(17280, 256);
+
+ base::RunLoop run_loop;
+ encoder_->Initialize(
+ H264PROFILE_BASELINE, options, /*info_cb=*/base::DoNothing(),
+ /*output_cb=*/base::DoNothing(),
+ base::BindLambdaForTesting([&](EncoderStatus status) {
+ EXPECT_EQ(status.code(),
+ EncoderStatus::Codes::kEncoderUnsupportedConfig);
+ run_loop.Quit();
+ }));
+ run_loop.Run();
+}
+
+TEST_F(OpenH264VideoEncoderResolutionTest, HeightExceeding6p1AspectRatioLimit) {
+ // A resolution that is extremely tall (e.g. 256x17280) has 16x1080
+ // macroblocks. This is only 17280 macroblocks (well within OpenH264's 36864
+ // limit), but its height (1080 MBs) exceeds the Level 6.1 limit of
+ // Sqrt(139264 * 8) = 1055.
+ VideoEncoder::Options options;
+ options.frame_size = gfx::Size(256, 17280);
+
+ base::RunLoop run_loop;
+ encoder_->Initialize(
+ H264PROFILE_BASELINE, options, /*info_cb=*/base::DoNothing(),
+ /*output_cb=*/base::DoNothing(),
+ base::BindLambdaForTesting([&](EncoderStatus status) {
+ EXPECT_EQ(status.code(),
+ EncoderStatus::Codes::kEncoderUnsupportedConfig);
+ run_loop.Quit();
+ }));
+ run_loop.Run();
+}
+#endif
+
} // namespace media