Medium watch CVSS 6.5 chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in WebCodecs
DescriptionUninitialized Use in WebCodecs
ComponentWebCodecs
Sub-componentMedia
Bug ClassUninitialized Memory
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
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.
Tracker497952533
Fix commit088809721d6d (chromium/src) +111/-0
CWECWE-457 (Use of uninitialized)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-30 (per vendor advisory)
Fix landed2026-04-01 (public git)
Disclosed2026-05-05
Patch gap34 days — fix public in source before the advisory

Background

OpenH264 encoder
the software H.264 encoder Chromium wraps in openh264_video_encoder.cc for WebCodecs/VideoEncoder.
`MAX_MBS_PER_FRAME` / macroblocks
OpenH264’s internal cap (36864) on the number of 16x16 macroblocks per frame; a frame whose macroblock area exceeds it is rejected internally during preprocessing.
`NeedsManualResizing`
a helper deciding whether Chromium must downscale a frame itself before handing it to OpenH264 (e.g. when OpenH264’s own auto-resize preconditions are not met).
Silent failure
OpenH264 returns without encoding the oversized frame but leaves the output surface untouched, so whatever uninitialized bytes were in the buffer are treated as encoded output.

Root Cause Analysis

When a frame’s macroblock area exceeds OpenH264’s MAX_MBS_PER_FRAME, OpenH264 silently fails during preprocessing instead of encoding, and the encoder path consumes/emits the resulting output as if encoding had succeeded — causing uninitialized memory to be read and encoded, per the commit message and the regression test comment “didn’t just read uninitialized memory.” The pre-patch code never tested frame area against this macroblock limit: NeedsManualResizing only forced manual resizing for aspect/upscaling reasons, and the configuration validators only rejected frames smaller than 16.

The fix adds IsFrameSizeTooLarge (computing mb_width * mb_height from the 16-pixel-rounded dimensions and comparing to kOpenH264MaxMBs = 36864) and wires it in at three points: NeedsManualResizing now returns true for oversized frames so Chromium downscales them before OpenH264 sees them, and two configuration paths now reject oversized options.frame_size with EncoderStatus::Codes::kEncoderUnsupportedConfig. This works because an oversized frame is either shrunk below the macroblock cap or rejected outright, so OpenH264 never reaches the silent-preprocessing-failure state that leaked uninitialized bytes.

Key insight
The code assumed OpenH264 would either encode or hard-fail, but it silently no-ops on frames above MAX_MBS_PER_FRAME, leaving the output buffer uninitialized; the fix adds an explicit IsFrameSizeTooLarge macroblock check that forces manual downscaling or rejects the configuration before OpenH264 is invoked.

Attack Path

  1. Configure or submit an oversized frame A page using WebCodecs configures an H.264 VideoEncoder with, or feeds it, a frame whose area exceeds 36864 macroblocks (the test uses 4096x4096 against a 1024x1024 configured size).
  2. Bypass existing checks The frame passes the only pre-patch guards (dimensions >= 16 and OpenH264 auto-resize eligibility), since no macroblock-area limit was enforced.
  3. Silent preprocessing failure OpenH264 aborts preprocessing without encoding, leaving the output buffer with uninitialized contents.
  4. Uninitialized memory encoded out The encoder emits that output as a coded frame, so uninitialized process memory is read and encoded into the output bitstream, which the application can read back (e.g. by decoding the produced buffer).

Impact Assessment

An attacker-controlled page can cause the encoder to read and emit uninitialized memory from the process hosting the software H.264 encoder (renderer/GPU-adjacent media code), potentially disclosing residual heap contents in the encoded output. The precondition is driving the WebCodecs H.264 encoder with a frame whose macroblock count exceeds OpenH264’s limit. The vendor advisory rates this medium severity (“Uninitialized Use in WebCodecs”) and does not report in-the-wild exploitation; information disclosure beyond uninitialized-byte leakage is (inferred).

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

Audit Directions

  • Codec library silent-failure contracts
    Audit every wrapper around a third-party codec for input limits (macroblock/pixel/dimension caps) that the library enforces by silently no-opping rather than returning an error, then reading the untouched output buffer anyway.
  • Output-buffer initialization on error paths
    Review encoder/decoder paths that emit a buffer whenever the library “returns,” confirming the buffer is only emitted when the library actually wrote it.
  • Dimension validation completeness
    Where only minimum dimensions are validated, add the corresponding maximum/area checks, since asymmetric validation (min-only) is a recurring gap.
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