Chrome · WebCodecs
CVE-2026-5292
OOB in WebCodecs
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
switchmedia/video/av1_video_encoder.cc |
modified | |
ifmedia/video/av1_video_encoder.cc |
modified | |
ifmedia/video/openh264_video_encoder.cc |
modified | |
TEST_Pmedia/video/software_video_encoder_test.cc |
modified | |
switchmedia/video/vpx_video_encoder.cc |
modified | |
ifmedia/video/vpx_video_encoder.cc |
modified |
Files Changed
media/video/av1_video_encoder.ccmedia/video/openh264_video_encoder.ccmedia/video/software_video_encoder_test.ccmedia/video/vpx_video_encoder.cc
Patch
From c177cadab426b31ccc2d3a1bca86a990004b2709 Mon Sep 17 00:00:00 2001 From: Eugene Zemtsov <[email protected]> Date: Thu, 12 Mar 2026 17:08:27 -0700 Subject: [PATCH] media: Fix OOB read in software encoders when U-stride != V-stride When an I420 VideoFrame is created with different strides for the U and V planes (e.g., via WebCodecs), passing it to software encoders (AV1, VPX, OpenH264) can cause an out-of-bounds read. These encoder libraries often assume or internally convert to a representation with a single chroma stride, ignoring the V plane stride. This change forces a manual copy of the frame before encoding if the U and V strides do not match, ensuring safe processing. Bug: 492213293, 491655161 Change-Id: Ifcf324ff2201fbb56d53e65cc98261790b9b170b Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7664025 Commit-Queue: Eugene Zemtsov <[email protected]> Reviewed-by: Erik Språng <[email protected]> Reviewed-by: Thomas Guilbert <[email protected]> Cr-Commit-Position: refs/heads/main@{#1598780} --- diff --git a/media/video/av1_video_encoder.cc b/media/video/av1_video_encoder.cc index bba5e59fb..c91d30a 100644 --- a/media/video/av1_video_encoder.cc +++ b/media/video/av1_video_encoder.cc @@ -49,16 +49,16 @@ // pixel format. If no conversion is needed returns nullopt. std::optional<VideoPixelFormat> GetConversionFormat(VideoCodecProfile profile, VideoPixelFormat format, - bool needs_resize) { + bool needs_copy) { switch (profile) { case AV1PROFILE_PROFILE_MAIN: if ((format != PIXEL_FORMAT_NV12 && format != PIXEL_FORMAT_I420) || - needs_resize) { + needs_copy) { return PIXEL_FORMAT_I420; } break; case AV1PROFILE_PROFILE_HIGH: - if (format != PIXEL_FORMAT_I444 || needs_resize) { + if (format != PIXEL_FORMAT_I444 || needs_copy) { return PIXEL_FORMAT_I444; } break; @@ -465,12 +465,22 @@ return; } + bool requires_copy = frame->visible_rect().size() != options_.frame_size || + (IsYuvPlanar(frame->format()) && + VideoFrame::NumPlanes(frame->format()) >= 3 && + frame->stride(VideoFrame::Plane::kU) != + frame->stride(VideoFrame::Plane::kV)); + // Format conversion or resizing may be necessary to get the frame into the // form needed by libaom for encoding. if (auto conversion_format = - GetConversionFormat(profile_, frame->format(), - /*needs_resize=*/frame->visible_rect().size() != - options_.frame_size)) { + GetConversionFormat(profile_, frame->format(), requires_copy)) { + // In cases where we need to + // - enlarge the frame + // - change the pixel format + // - change the aspect ratio or + // - use matching U and V strides + // we are forced to convert and rescale manually. auto temp_frame = frame_pool_.CreateFrame( *conversion_format, options_.frame_size, gfx::Rect(options_.frame_size), options_.frame_size, frame->timestamp()); diff --git a/media/video/openh264_video_encoder.cc b/media/video/openh264_video_encoder.cc index d949d3d5..c29a9fce 100644 --- a/media/video/openh264_video_encoder.cc +++ b/media/video/openh264_video_encoder.cc @@ -427,12 +427,18 @@ } } - if (frame->format() != PIXEL_FORMAT_I420 || - NeedsManualResizing(frame->visible_rect().size(), options_.frame_size)) { + bool requires_copy = + frame->format() != PIXEL_FORMAT_I420 || + NeedsManualResizing(frame->visible_rect().size(), options_.frame_size) || + frame->stride(VideoFrame::Plane::kU) != + frame->stride(VideoFrame::Plane::kV); + + if (requires_copy) { // In cases where we need to // - enlarge the frame - // - change the pixel format or - // - change the aspect ratio + // - change the pixel format + // - change the aspect ratio or + // - use matching U and V strides // we are forced to convert and rescale manually. auto i420_frame = frame_pool_.CreateFrame( PIXEL_FORMAT_I420, options_.frame_size, gfx::Rect(options_.frame_size), diff --git a/media/video/software_video_encoder_test.cc b/media/video/software_video_encoder_test.cc index fc82aab2..4ec61cd 100644 --- a/media/video/software_video_encoder_test.cc +++ b/media/video/software_video_encoder_test.cc @@ -7,6 +7,7 @@ #include <memory> #include <string> +#include "base/containers/heap_array.h" #include "base/feature_list.h" #include "base/functional/callback_helpers.h" #include "base/logging.h" @@ -1092,6 +1093,42 @@ EXPECT_EQ(chunks.size(), total_frames_count); } +TEST_P(SoftwareVideoEncoderTest, EncodeFrameWithMismatchedStrides) { + VideoEncoder::Options options = CreateDefaultOptions(); + options.frame_size = gfx::Size(64, 64); + + encoder_->Initialize(profile_, options, /*info_cb=*/base::DoNothing(), + /*output_cb=*/base::DoNothing(), + ValidateStatusThenQuitCB()); + RunUntilQuit(); + + // Create a frame with mismatched strides + gfx::Size size(64, 64); + size_t y_stride = 64; + size_t u_stride = 65536; // Large U stride + size_t v_stride = 32; + + // We allocate memory for the data. To cause an OOB read crash if the U + // stride is used for the V plane, we allocate a small buffer for the V plane. + auto y_data = base::HeapArray<uint8_t>::WithSize(y_stride * size.height()); + auto u_data = + base::HeapArray<uint8_t>::WithSize(u_stride * (size.height() / 2)); + auto v_data = + base::HeapArray<uint8_t>::WithSize(v_stride * (size.height() / 2)); + + auto frame = VideoFrame::WrapExternalYuvData( + PIXEL_FORMAT_I420, size, gfx::Rect(size), size, y_stride, u_stride, + v_stride, y_data, u_data, v_data, base::TimeDelta()); + frame->AddDestructionObserver( + base::BindOnce([](base::HeapArray<uint8_t>, base::HeapArray<uint8_t>, + base::HeapArray<uint8_t>) {}, + std::move(y_data), std::move(u_data), std::move(v_data))); + + encoder_->Encode(std::move(frame), VideoEncoder::EncodeOptions(false), + ValidateStatusThenQuitCB()); + RunUntilQuit(); +} + TEST_P(SoftwareVideoEncoderTest, ReconfigureWithResizingNumberOfThreads) { int outputs_count = 0; VideoEncoder::Options options = CreateDefaultOptions(); diff --git a/media/video/vpx_video_encoder.cc b/media/video/vpx_video_encoder.cc index 681b1b6..a661729e 100644 --- a/media/video/vpx_video_encoder.cc +++ b/media/video/vpx_video_encoder.cc @@ -258,29 +258,29 @@ // pixel format. If no conversion is needed returns nullopt. std::optional<VideoPixelFormat> GetConversionFormat(VideoCodecProfile profile, VideoPixelFormat format, - bool needs_resize) { + bool needs_copy) { switch (profile) { case VP8PROFILE_ANY: case VP9PROFILE_PROFILE0: if ((format != PIXEL_FORMAT_NV12 && format != PIXEL_FORMAT_I420) || - needs_resize) { + needs_copy) { return PIXEL_FORMAT_I420; } break; case VP9PROFILE_PROFILE1: - if (format != PIXEL_FORMAT_I444 || needs_resize) { + if (format != PIXEL_FORMAT_I444 || needs_copy) { return PIXEL_FORMAT_I444; } break; case VP9PROFILE_PROFILE2: - if (format != PIXEL_FORMAT_YUV420P10 || needs_resize) { + if (format != PIXEL_FORMAT_YUV420P10 || needs_copy) { // VideoFrameConverter doesn't support 10bit yet, so output I420 then // convert to I010. return PIXEL_FORMAT_I420; } break; case VP9PROFILE_PROFILE3: - if (format != PIXEL_FORMAT_YUV444P10 || needs_resize) { + if (format != PIXEL_FORMAT_YUV444P10 || needs_copy) { // VideoFrameConverter doesn't support 10bit yet, so output I444 then // convert to I410. return PIXEL_FORMAT_I444; @@ -596,12 +596,22 @@ return; } + bool requires_copy = frame->visible_rect().size() != options_.frame_size || + (IsYuvPlanar(frame->format()) &&
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 fc82aab2..4ec61cd 100644
--- a/media/video/software_video_encoder_test.cc
+++ b/media/video/software_video_encoder_test.cc
@@ -7,6 +7,7 @@
#include <memory>
#include <string>
+#include "base/containers/heap_array.h"
#include "base/feature_list.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
@@ -1092,6 +1093,42 @@
EXPECT_EQ(chunks.size(), total_frames_count);
}
+TEST_P(SoftwareVideoEncoderTest, EncodeFrameWithMismatchedStrides) {
+ VideoEncoder::Options options = CreateDefaultOptions();
+ options.frame_size = gfx::Size(64, 64);
+
+ encoder_->Initialize(profile_, options, /*info_cb=*/base::DoNothing(),
+ /*output_cb=*/base::DoNothing(),
+ ValidateStatusThenQuitCB());
+ RunUntilQuit();
+
+ // Create a frame with mismatched strides
+ gfx::Size size(64, 64);
+ size_t y_stride = 64;
+ size_t u_stride = 65536; // Large U stride
+ size_t v_stride = 32;
+
+ // We allocate memory for the data. To cause an OOB read crash if the U
+ // stride is used for the V plane, we allocate a small buffer for the V plane.
+ auto y_data = base::HeapArray<uint8_t>::WithSize(y_stride * size.height());
+ auto u_data =
+ base::HeapArray<uint8_t>::WithSize(u_stride * (size.height() / 2));
+ auto v_data =
+ base::HeapArray<uint8_t>::WithSize(v_stride * (size.height() / 2));
+
+ auto frame = VideoFrame::WrapExternalYuvData(
+ PIXEL_FORMAT_I420, size, gfx::Rect(size), size, y_stride, u_stride,
+ v_stride, y_data, u_data, v_data, base::TimeDelta());
+ frame->AddDestructionObserver(
+ base::BindOnce([](base::HeapArray<uint8_t>, base::HeapArray<uint8_t>,
+ base::HeapArray<uint8_t>) {},
+ std::move(y_data), std::move(u_data), std::move(v_data)));
+
+ encoder_->Encode(std::move(frame), VideoEncoder::EncodeOptions(false),
+ ValidateStatusThenQuitCB());
+ RunUntilQuit();
+}
+
TEST_P(SoftwareVideoEncoderTest, ReconfigureWithResizingNumberOfThreads) {
int outputs_count = 0;
VideoEncoder::Options options = CreateDefaultOptions();
Loading diff…
Original Bug Report
reported by [email protected]
WebCodecs libaom, libvpx: OOB Read in AV1 and VP9 encoder when encoding I420 frames with mismatched U and V strides
Description:
Providing an I420 VideoFrame with different strides for the U and V planes to an AV1 VideoEncoder causes a severe out-of-bounds memory read (EXCEPTION_ACCESS_VIOLATION) in the underlying software encoder (libaom).
This can be triggered directly from JavaScript using the WebCodecs API by manually specifying the plane layouts and strides during VideoFrame construction.
Reproduction Steps:
- Open the attached HTML file (poc.html) in Chrome.
- The page uses WebCodecs to construct an I420 VideoFrame with a massive U stride (65536) and a normal V stride (32), then passes it to an AV1 VideoEncoder.
- Observe the renderer crash due to an Access Violation.
Crash Stack :
1 Received fatal exception EXCEPTION_ACCESS_VIOLATION
2 media!copy_and_extend_plane [0x7fff7b5c82ed+4ed] (third_party\libaom\source\libaom\av1\encoder\extend.c:34)
3 media!av1_copy_and_extend_frame [0x7fff7b5c7502+5c2] (third_party\libaom\source\libaom\av1\encoder\extend.c:158)
4 media!av1_lookahead_push [0x7fff7b5c8b96+1a6] (third_party\libaom\source\libaom\av1\encoder\lookahead.c:153)
5 media!av1_receive_raw_frame [0x7fff7b5dacd0+c0] (third_party\libaom\source\libaom\av1\encoder\encoder.c:4765)
6 media!encoder_encode [0x7fff7b5f3f39+a69] (third_party\libaom\source\libaom\av1\av1_cx_iface.c:3475)
7 media!aom_codec_encode [0x7fff7b601ea9+b9] (third_party\libaom\source\libaom\aom\src\aom_encoder.c:191)
8 media!media::Av1VideoEncoder::Encode [0x7fff7af43835+cf5] (media\video\av1_video_encoder.cc:602)
Root Cause:
- media::Av1VideoEncoder::Encode correctly maps the three independent VideoFrame strides into the aom_image_t struct (which supports independent strides for all planes).
- Deep inside aom_codec_encode, libaom converts the aom_image_t to its internal YV12_BUFFER_CONFIG representation using image2yuvconfig().
- YV12_BUFFER_CONFIG does not support independent chroma strides; it only has a single uv_stride field. image2yuvconfig() populates this field solely using the U plane
stride (
yv12->uv_stride = img->stride[AOM_PLANE_U];), silently ignoring the V plane stride. - When av1_copy_and_extend_frame is called, it uses the inflated uv_stride (from the U plane) to read from the V buffer, resulting in an OOB read.
References
On This Page