Medium chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in WebCodecs
DescriptionUninitialized Use in WebCodecs
ComponentWebCodecs
Bug ClassUninitialized Memory
Tracker497952533
Fix commit088809721d6d (chromium/src) +111/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Changed Functions

FunctionChangeNotes
TEST_P
media/video/software_video_encoder_test.cc
modified
if
media/video/software_video_encoder_test.cc
modified
BindLambdaForTesting
media/video/software_video_encoder_test.cc
modified

Files Changed

  • media/video/openh264_video_encoder.cc
  • media/video/software_video_encoder_test.cc
From 088809721d6dcbae6d63e23b28873ae96243c45e Mon Sep 17 00:00:00 2001
From: Eugene Zemtsov <eugene@chromium.org>
Date: Wed, 01 Apr 2026 12:48:27 -0700
Subject: [PATCH] media: Fix OpenH264 encoder silent failure on oversized frames

OpenH264 silently fails during preprocessing when a frame's area
exceeds its internal macroblock limit (MAX_MBS_PER_FRAME). This
can result in the encoder reading and encoding uninitialized memory.

This change updates NeedsManualResizing() to force manual downscaling
for frames that exceed this limit before passing them to OpenH264.

Bug: 497952533
Change-Id: I5e042ac380ee79be5a7a6e515ccc88948cf19c26
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7718887
Commit-Queue: Eugene Zemtsov <eugene@chromium.org>
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1608727}
---

diff --git a/media/video/openh264_video_encoder.cc b/media/video/openh264_video_encoder.cc
index c29a9fce..f83ab4a 100644
--- a/media/video/openh264_video_encoder.cc
+++ b/media/video/openh264_video_encoder.cc
@@ -148,6 +148,18 @@
   }
 }
 
+// OpenH264 silently fails during preprocessing when a frame's area
+// exceeds its internal macroblock limit (MAX_MBS_PER_FRAME in
+// third_party/openh264). We must manually resize such frames.
+// MAX_MBS_PER_FRAME is 36864.
+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;
+  return mb_width * mb_height > kOpenH264MaxMBs;
+}
+
 // OpenH264 can resize frames automatically as long as
 // - the input and output aspect ratios are the same and
 // - the input is larger than the output in both dimensions.
@@ -156,6 +168,10 @@
     return true;
   }
 
+  if (IsFrameSizeTooLarge(src)) {
+    return true;
+  }
+
   if (dst.width() > src.width() || dst.height() > src.height()) {
     return true;
   }
@@ -249,6 +265,14 @@
                       "Unsupported frame size which is less than 16"));
     return;
   }
+
+  if (IsFrameSizeTooLarge(options.frame_size)) {
+    std::move(done_cb).Run(EncoderStatus(
+        EncoderStatus::Codes::kEncoderUnsupportedConfig,
+        "Configured frame size exceeds OpenH264 max macroblocks"));
+    return;
+  }
+
   SetUpOpenH264Params(
       profile_, options,
       VideoColorSpace::FromGfxColorSpace(last_frame_color_space_), &params);
@@ -514,6 +538,20 @@
     return;
   }
 
+  if (options.frame_size.width() < 16 || options.frame_size.height() < 16) {
+    std::move(done_cb).Run(
+        EncoderStatus(EncoderStatus::Codes::kEncoderUnsupportedConfig,
+                      "Unsupported frame size which is less than 16"));
+    return;
+  }
+
+  if (IsFrameSizeTooLarge(options.frame_size)) {
+    std::move(done_cb).Run(EncoderStatus(
+        EncoderStatus::Codes::kEncoderUnsupportedConfig,
+        "Configured frame size exceeds OpenH264 max macroblocks"));
+    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 3e879b8..fc86111f 100644
--- a/media/video/software_video_encoder_test.cc
+++ b/media/video/software_video_encoder_test.cc
@@ -1372,6 +1372,79 @@
   EXPECT_EQ(outputs_count, 3);
 }
 
+TEST_P(H264VideoEncoderTest, OversizedFrameSilentFailure) {
+  VideoEncoder::Options options = CreateDefaultOptions();
+  options.frame_size = gfx::Size(1024, 1024);
+  options.bitrate = Bitrate::ConstantBitrate(1000000u);
+  options.framerate = 25;
+  if (codec_ == VideoCodec::kH264) {
+    options.avc.produce_annexb = true;
+  }
+
+  int total_decoded_frames = 0;
+
+  scoped_refptr<VideoFrame> reference_frame;
+
+  VideoEncoder::OutputCB encoder_output_cb = base::BindLambdaForTesting(
+      [&, this](VideoEncoderOutput output,
+                std::optional<VideoEncoder::CodecDescription> desc) {
+        auto buffer = DecoderBuffer::FromArray(std::move(output.data));
+        buffer->set_timestamp(output.timestamp);
+        buffer->set_is_key_frame(output.key_frame);
+        decoder_->Decode(std::move(buffer), DecoderStatusCB());
+      });
+
+  VideoDecoder::OutputCB decoder_output_cb =
+      base::BindLambdaForTesting([&](scoped_refptr<VideoFrame> decoded_frame) {
+        ASSERT_TRUE(reference_frame);
+
+        EXPECT_EQ(decoded_frame->timestamp(), reference_frame->timestamp());
+        EXPECT_EQ(decoded_frame->visible_rect().size(),
+                  reference_frame->visible_rect().size());
+
+        // This validates that the encoder actually encoded our input frame,
+        // and didn't just read uninitialized memory.
+        if (decoded_frame->format() == reference_frame->format()) {
+          // Allow up to 1% of pixels to be different due to compression
+          // artifacts.
+          const int kAllowedDifferentPixels =
+              reference_frame->visible_rect().size().GetArea() / 100;
+          EXPECT_LE(CountDifferentPixels(*decoded_frame, *reference_frame, 10),
+                    kAllowedDifferentPixels);
+        }
+        ++total_decoded_frames;
+      });
+
+  PrepareDecoder(options.frame_size, std::move(decoder_output_cb));
+
+  encoder_->Initialize(profile_, options, /*info_cb=*/base::DoNothing(),
+                       std::move(encoder_output_cb),
+                       ValidateStatusThenQuitCB());
+  RunUntilQuit();
+
+  // Create frames with identical colors but vastly different sizes.
+  constexpr uint32_t kXorMask = 0x123456;
+  auto oversized_frame = CreateFrame(gfx::Size(4096, 4096), pixel_format_,
+                                     base::TimeDelta(), kXorMask);
+  ASSERT_TRUE(oversized_frame) << "Failed to allocate oversized frame";
+  oversized_frame->set_color_space(gfx::ColorSpace::CreateREC709());
+
+  reference_frame = CreateFrame(options.frame_size, pixel_format_,
+                                base::TimeDelta(), kXorMask);
+  ASSERT_TRUE(reference_frame) << "Failed to allocate reference frame";
+
+  encoder_->Encode(std::move(oversized_frame),
+                   VideoEncoder::EncodeOptions(false),
+                   ValidateStatusThenQuitCB());
+  RunUntilQuit();
+
+  encoder_->Flush(ValidateStatusThenQuitCB());
+  RunUntilQuit();
+  DecodeAndWaitForStatus(DecoderBuffer::CreateEOSBuffer());
+
+  EXPECT_EQ(total_decoded_frames, 1);
+}
+
 TEST_P(H264VideoEncoderTest, AnnexB) {
   int outputs_count = 0;
   VideoEncoder::Options options = CreateDefaultOptions();
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 3e879b8..fc86111f 100644
--- a/media/video/software_video_encoder_test.cc
+++ b/media/video/software_video_encoder_test.cc
@@ -1372,6 +1372,79 @@
   EXPECT_EQ(outputs_count, 3);
 }
 
+TEST_P(H264VideoEncoderTest, OversizedFrameSilentFailure) {
+  VideoEncoder::Options options = CreateDefaultOptions();
+  options.frame_size = gfx::Size(1024, 1024);
+  options.bitrate = Bitrate::ConstantBitrate(1000000u);
+  options.framerate = 25;
+  if (codec_ == VideoCodec::kH264) {
+    options.avc.produce_annexb = true;
+  }
+
+  int total_decoded_frames = 0;
+
+  scoped_refptr<VideoFrame> reference_frame;
+
+  VideoEncoder::OutputCB encoder_output_cb = base::BindLambdaForTesting(
+      [&, this](VideoEncoderOutput output,
+                std::optional<VideoEncoder::CodecDescription> desc) {
+        auto buffer = DecoderBuffer::FromArray(std::move(output.data));
+        buffer->set_timestamp(output.timestamp);
+        buffer->set_is_key_frame(output.key_frame);
+        decoder_->Decode(std::move(buffer), DecoderStatusCB());
+      });
+
+  VideoDecoder::OutputCB decoder_output_cb =
+      base::BindLambdaForTesting([&](scoped_refptr<VideoFrame> decoded_frame) {
+        ASSERT_TRUE(reference_frame);
+
+        EXPECT_EQ(decoded_frame->timestamp(), reference_frame->timestamp());
+        EXPECT_EQ(decoded_frame->visible_rect().size(),
+                  reference_frame->visible_rect().size());
+
+        // This validates that the encoder actually encoded our input frame,
+        // and didn't just read uninitialized memory.
+        if (decoded_frame->format() == reference_frame->format()) {
+          // Allow up to 1% of pixels to be different due to compression
+          // artifacts.
+          const int kAllowedDifferentPixels =
+              reference_frame->visible_rect().size().GetArea() / 100;
+          EXPECT_LE(CountDifferentPixels(*decoded_frame, *reference_frame, 10),
+                    kAllowedDifferentPixels);
+        }
+        ++total_decoded_frames;
+      });
+
+  PrepareDecoder(options.frame_size, std::move(decoder_output_cb));
+
+  encoder_->Initialize(profile_, options, /*info_cb=*/base::DoNothing(),
+                       std::move(encoder_output_cb),
+                       ValidateStatusThenQuitCB());
+  RunUntilQuit();
+
+  // Create frames with identical colors but vastly different sizes.
+  constexpr uint32_t kXorMask = 0x123456;
+  auto oversized_frame = CreateFrame(gfx::Size(4096, 4096), pixel_format_,
+                                     base::TimeDelta(), kXorMask);
+  ASSERT_TRUE(oversized_frame) << "Failed to allocate oversized frame";
+  oversized_frame->set_color_space(gfx::ColorSpace::CreateREC709());
+
+  reference_frame = CreateFrame(options.frame_size, pixel_format_,
+                                base::TimeDelta(), kXorMask);
+  ASSERT_TRUE(reference_frame) << "Failed to allocate reference frame";
+
+  encoder_->Encode(std::move(oversized_frame),
+                   VideoEncoder::EncodeOptions(false),
+                   ValidateStatusThenQuitCB());
+  RunUntilQuit();
+
+  encoder_->Flush(ValidateStatusThenQuitCB());
+  RunUntilQuit();
+  DecodeAndWaitForStatus(DecoderBuffer::CreateEOSBuffer());
+
+  EXPECT_EQ(total_decoded_frames, 1);
+}
+
 TEST_P(H264VideoEncoderTest, AnnexB) {
   int outputs_count = 0;
   VideoEncoder::Options options = CreateDefaultOptions();
Loading diff…

Original Bug Report

reported by vm...@google.com

Uninitialized heap memory disclosure in OpenH264 encoder via oversized frames

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A vulnerability in Chromium’s OpenH264VideoEncoder allows oversized VideoFrames to bypass manual resizing logic when using WebCodecs. This causes silent failures in OpenH264’s preprocessing stage, resulting in the encoder compressing uninitialized heap memory into a valid H.264 bitstream. An attacker can decode this bitstream to systematically leak renderer heap contents, defeating ASLR.

Affected files:

  • media/video/openh264_video_encoder.cc
  • third_party/openh264/src/codec/encoder/core/src/wels_preprocess.cpp
  • third_party/openh264/src/codec/processing/src/common/WelsFrameWork.cpp
  • third_party/openh264/src/codec/encoder/core/src/picture_handle.cpp
  • third_party/openh264/src/codec/common/src/memory_align.cpp
  • third_party/openh264/src/codec/encoder/core/src/encoder_ext.cpp

Estimated timestamp from git blame: 2026-03-12

Summary

A critical information disclosure vulnerability exists in the integration between Chromium’s WebCodecs API and the software OpenH264 encoder. By providing an oversized VideoFrame that maintains the exact aspect ratio of the configured target resolution, an attacker can bypass Chromium’s manual resizing checks. The oversized frame is passed directly to OpenH264, which silently fails during preprocessing due to internal maximum macroblock limits. Consequently, the encoder uses an uninitialized heap buffer allocated via malloc as the source picture for compression. The resulting H.264 bitstream, containing raw renderer heap data, is then delivered to the calling JavaScript context.

Root Cause Analysis

1. Chromium Resizing Bypass (media/video/openh264_video_encoder.cc)

The Encode() method determines if a frame requires manual resizing before being passed to OpenH264 by calling NeedsManualResizing(). This function contains a logical flaw:

// media/video/openh264_video_encoder.cc:154-165
bool NeedsManualResizing(const gfx::Size& src, const gfx::Size& dst) {
  // ...
  if (dst.width() > src.width() || dst.height() > src.height()) {
    return true;
  }
  return VideoAspectRatio::PAR(src.width(), src.height()) !=
         VideoAspectRatio::PAR(dst.width(), dst.height());
}

If an attacker configures the encoder for a 1024x1024 resolution (dst) and provides a 4096x4096 VideoFrame (src), dst > src evaluates to false (bypassing the upscaling check), and the aspect ratios match perfectly (1:1). The function incorrectly returns false, causing the oversized frame to be passed directly to codec_->EncodeFrame() without being downscaled.

2. OpenH264 Silent Preprocessing Failures (third_party/openh264/...)

When OpenH264 receives the oversized 4096x4096 frame, it detects a dimension mismatch against its 1024x1024 configuration and triggers a preprocessing reset (WelsPreprocessReset).

  1. Uninitialized Allocation: WelsInitScaledPic allocates a new pScaledInputPicture buffer using WelsMalloc (memory_align.cpp:72). Crucially, WelsMalloc wraps the standard C malloc function and does not zero-initialize the memory. The buffer contains raw heap data.
  2. Silent Failure in Memory Copy: SingleLayerPreprocess calls WelsMoveMemoryWrapper (wels_preprocess.cpp:383) to copy the oversized input frame into the pre-processing pipeline. Inside WelsMoveMemoryWrapper (wels_preprocess.cpp:1425), a safety check evaluates: if (iSrcWidth * iSrcHeight > (MAX_MBS_PER_FRAME << 8)) return;. The total pixels (4096 * 4096 = 16,777,216) exceed the maximum threshold (36864 * 256 = 9,437,184). The function silently returns void early without copying any pixel data.
  3. Silent Error in Downsampling: SingleLayerPreprocess then attempts to downsample the frame by calling DownsamplePadding (wels_preprocess.cpp:398). This invokes m_pInterfaceVp->Process, which maps to CVpFrameWork::Process (WelsFrameWork.cpp:169). This function calls CheckValid, which evaluates a similar pixel count limit check: if ((iRectWidth * iRectHeight) > (MAX_MBS_PER_FRAME << 8)) return RET_INVALIDPARAM;. This check also fails for the 4096x4096 dimensions, returning the error RET_INVALIDPARAM back to DownsamplePadding.
  4. Error Discarded: DownsamplePadding returns this error code, but SingleLayerPreprocess explicitly discards it (wels_preprocess.cpp:398).

3. Data Leakage

As a result of both preprocessing steps failing silently, the output spatial layer destination buffer (pDstPic) remains completely uninitialized. SingleLayerPreprocess assigns this uninitialized buffer as the spatial layer’s source (pSrc). The core encoder (WelsCodeOneSlice) then compresses this raw heap data as if it were valid YUV pixel data. The resulting H.264 bitstream is passed back to the attacker’s JavaScript via the WebCodecs EncodedVideoChunk output callback.

By systematically alternating the colorSpace property of the VideoFrame between encode calls, an attacker can force media::OpenH264VideoEncoder::Encode to call UpdateEncoderColorSpace() (openh264_video_encoder.cc:464). This triggers a full OpenH264 codec re-initialization and fresh WelsMalloc allocations, allowing the attacker to continuously leak new, distinct regions of the renderer heap.

Suggested Exploitation Steps

(Note: These are potential steps based on code analysis; a working PoC has not been executed yet.)

  1. In a malicious web page, instantiate a VideoEncoder using the WebCodecs API, forcing the OpenH264 software implementation.
  2. Configure the encoder with a standard target resolution, e.g., 1024x1024.
  3. Create a VideoFrame with dimensions significantly larger than the configuration (e.g., 4096x4096) that maintains the exact same aspect ratio.
  4. Call encode() on the oversized frame.
  5. In the output callback, receive the EncodedVideoChunk containing the H.264 bitstream.
  6. Decode the bitstream using a standard H.264 decoder to retrieve the uninitialized renderer heap memory (pixel data).
  7. To leak continuous memory regions, alternate the colorSpace of the input VideoFrame between encodes to force codec re-initialization.

Suggested Fix

  1. Chromium: Update NeedsManualResizing in media/video/openh264_video_encoder.cc to ensure that frames larger than the target resolution in either dimension (downscaling) are manually resized if OpenH264’s internal downscaler is deemed unsafe for extremely large frames. Alternatively, add explicit bounds checking (e.g., against MAX_MBS_PER_FRAME) before passing frames to EncodeFrame().
  2. OpenH264:
    • Modify WelsMalloc to use calloc or explicitly memset the allocated memory to zero to prevent uninitialized memory disclosures.
    • Update SingleLayerPreprocess in wels_preprocess.cpp to properly handle and propagate the error codes returned by DownsamplePadding and WelsMoveMemoryWrapper instead of silently ignoring them.

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from 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