Medium patch-now ✔︎ verified CVSS 8.8 chrome OOB 🔧 Commit mapped 🧬 Front-end validation exempts a record the backend still uses

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactBuffer overflow in WebRTC
DescriptionBuffer overflow in WebRTC
ComponentWebRTC
Sub-componentWebCodecs
Bug ClassOOB
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
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker504690157
Fix commit96c76fee15bb (chromium/src) +138/-2
CWECWE-122 (Heap buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-20 (per vendor advisory)
Fix landed2026-07-14 (public git)
Disclosed2026-09-08
Patch gap56 days — fix public in source before the advisory

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.h helper returning the MaxFS for a given level; for H264SPS::kLevelIDC6p1 it returns 139264, from which the per-side cap sqrt(MaxFS * 8) ≈ 1055 macroblocks 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.

Key insight
The encoder validated only the total macroblock count and never the individual width and height, so a lawful-total but pathologically-shaped resolution slipped through into OpenH264; the fix adds an explicit per-dimension aspect-ratio check (mb_width/mb_height ≤ sqrt(MaxFS * 8)) plus overflow-safe uint64_t arithmetic.

Attack Path

  1. 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, reaching OpenH264VideoEncoder’s configuration path.
  2. 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.
  3. Bypass the only guard IsFrameSizeTooLarge checks only the product and passes, so pre-patch the size flows unchecked into SetUpOpenH264Params and the OpenH264 encoder.
  4. 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

An attacker who can drive H.264 software encoding with a chosen resolution can cause a heap buffer overflow in the OpenH264 encoder, corrupting memory in the process that hosts the software encoder (a renderer or media/utility process, exact context inferred). Preconditions are the ability to instantiate an OpenH264-backed encoder and set an out-of-spec frame size, which is reachable from web content via WebCodecs/WebRTC. The advisory classifies this as an OOB buffer overflow in WebRTC; escalation beyond memory corruption to code execution is (inferred) and not demonstrated by the patch, and no exploited-in-the-wild status is stated.

Changed Functions

FunctionChangeNotes
if
media/video/openh264_video_encoder.cc
modified
OpenH264VideoEncoderResolutionTest
media/video/software_video_encoder_test.cc
modified
TEST_F
media/video/software_video_encoder_test.cc
modified
BindLambdaForTesting
media/video/software_video_encoder_test.cc
modified
if
media/video/BUILD.gn
modified

Files Changed

  • media/video/openh264_video_encoder.cc
  • media/video/BUILD.gn
  • media/video/software_video_encoder_test.cc

Audit Directions

  • Total-vs-dimension validation gaps
    Audit 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 arithmetic
    Look for int-typed products of (dim + 15) / 16 or similar dimension math feeding buffer sizing, which can wrap; the same uint64_t hardening should be applied wherever attacker-influenced dimensions are multiplied.
  • Third-party encoder/decoder wrappers
    Review 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.
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_), &params);
@@ -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(&params)) {
     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) {
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.