CVE-2025-11211
Overview
Background
- Coded size vs. visible rect
- a
media::VideoFrameallocates planes for itscoded_size(), which may be padded, whilevisible_rect()describes the meaningful pixels.row_bytes(plane)androws(plane)are derived from the coded size. - Odd-size alignment in `CreateFrame()`
- for subsampled formats such as
PIXEL_FORMAT_I420A,VideoFrame::CreateFramerounds the coded size up to even dimensions; the new test shows 677x287 becoming 678x288. The full-resolution Y and A planes then have morerow_bytes/rowsthan the visible area. - `libyuv::CopyPlane(src, src_stride, dst, dst_stride, width, height)`
- copies
heightrows ofwidthbytes. It trusts the caller that both the source and destination spans coverwidthxheightat their strides. - WebCodecs `VideoFrame` from a buffer
- the Blink constructor that builds a
VideoFramefrom a script-suppliedArrayBufferplusVideoFrameBufferInit(codedWidth,codedHeight,format). In the non-transfer case it copies the pixels into a pooled frame.
Root Cause Analysis
In third_party/blink/renderer/modules/webcodecs/video_frame.cc, the non-transfer path builds the destination with frame_pool.CreateFrame(media_fmt, crop.size(), ...). It then copied every plane with libyuv::CopyPlane(src_frame->visible_data(i), src_frame->stride(i), ..., frame->row_bytes(i), frame->rows(i)), using the destination’s coded row bytes and row count as the copy extent. The surrounding comment assumes coded_size == visible_size, but the patch’s new note says “CreateFrame() may expand the coded size for odd sized frames”. With an odd-sized I420A frame (the regression test uses 61x21 backed by a tightly sized DOMArrayBuffer), the destination Y and A planes are one column and one row larger than the source. CopyPlane therefore reads past the end of each source plane, and for the last (A) plane past the end of the script-supplied buffer. That is the out-of-bounds read.
The fix sizes the copy from the source, src_frame->GetVisibleRowBytes(i) and src_frame->GetVisibleRows(i), using new VideoFrame helpers computed from visible_rect(). The same coded-vs-visible confusion is fixed in the vpx_video_decoder.cc copy, which now uses GetVisibleRowBytes/GetVisibleRows.
CreateFrame() became an over-read of the source. The fix bounds each CopyPlane by the source’s visible rows and row bytes.Attack Path
- Construct an odd-sized frame
Script calls the WebCodecs
VideoFrameconstructor with aBufferSourceand aVideoFrameBufferInitdeclaring an oddcodedWidth/codedHeight(for example 61x21) in a subsampled format such asI420A, with a buffer sized exactly for that layout. - Destination padded
CachedVideoFramePool::CreateFramecreates the destination with coded size rounded up to even (62x22 for the Y and A planes), larger than the source planes. - Over-read during copy
The per-plane
libyuv::CopyPlaneloop usesframe->row_bytes(i)andframe->rows(i), reading one extra byte per row and one extra row from each full-resolution source plane. On the A plane this runs past the end of the caller’sArrayBufferinto adjacent renderer heap. - Data surfaced to script
The over-read bytes land in the new frame’s padded area, and the frame’s pixels are script-readable (for example via
copyTo), which could expose adjacent heap contents (inferred; the diff shows the read, not a read-back path).
Impact Assessment
(inferred). The vpx_video_decoder.cc copy has the same coded-vs-visible pattern, but whether it overruns real libvpx buffers is not shown. The advisory rates this medium severity and does not report in-the-wild exploitation.Changed Functions
| Function | Change | Notes |
|---|---|---|
formedia/base/video_frame.cc |
modified | |
ifthird_party/blink/renderer/modules/webcodecs/video_frame.cc |
modified | |
TESTmedia/base/video_frame_unittest.cc |
modified | |
formedia/base/video_frame_unittest.cc |
modified | |
ifmedia/base/video_frame_unittest.cc |
modified | |
TEST_Fthird_party/blink/renderer/modules/webcodecs/video_frame_test.cc |
modified | |
forthird_party/blink/renderer/modules/webcodecs/video_frame_test.cc |
modified | |
ifthird_party/blink/renderer/modules/webcodecs/video_frame_test.cc |
modified |
Files Changed
media/base/video_frame.ccmedia/base/video_frame.hmedia/base/video_frame_unittest.ccmedia/filters/vpx_video_decoder.ccmedia/test/pipeline_integration_test.ccthird_party/blink/renderer/modules/webcodecs/video_frame.ccthird_party/blink/renderer/modules/webcodecs/video_frame_test.cc
Audit Directions
- Destination-derived copy extentsGrep for
CopyPlane/memcpy/CopyRowcalls whose width or height come from the destination frame’srow_bytes()/rows()/coded_size()while the source is a different frame or a user buffer. Each must be bounded by the smaller of the source and destination extents. - Alignment that grows allocationsAnywhere
CreateFrame, frame pools, orVideoFrameLayoutround dimensions up (odd sizes,kFrameSizeAlignment, subsampling), check every follow-on copy, conversion, or readback (copyTo,ImageBitmap, canvas upload) for coded-vs-visible confusion. - Script-sized buffers wrapped as framesReview the WebCodecs
VideoFrame/AudioDataconstructors andcopyTopaths that wrap aBufferSource(WrapExternalYuvaDataand similar). Make sure the computed plane layout is validated against the actual buffer length for every format, including the A plane inI420A.
Patch
diff --git a/media/base/video_frame.cc b/media/base/video_frame.cc
index 8fb80072..ac36168 100644
--- a/media/base/video_frame.cc
+++ b/media/base/video_frame.cc
@@ -1270,20 +1270,26 @@
// static
void VideoFrame::UpdateHashWithFrameForTesting(crypto::hash::Hasher& hasher,
- const VideoFrame& frame) {
+ const VideoFrame& frame,
+ bool visible_data_only) {
for (size_t plane = 0; plane < NumPlanes(frame.format()); ++plane) {
- for (int row = 0; row < frame.rows(plane); ++row) {
- hasher.Update(frame.data_[plane].subspan(
- base::checked_cast<size_t>(frame.stride(plane) * row),
- base::checked_cast<size_t>(frame.row_bytes(plane))));
+ const size_t rows = base::checked_cast<size_t>(
+ visible_data_only ? frame.GetVisibleRows(plane) : frame.rows(plane));
+ const size_t row_bytes = base::checked_cast<size_t>(
+ visible_data_only ? frame.GetVisibleRowBytes(plane)
+ : frame.row_bytes(plane));
+ const auto& plane_data = frame.data_[plane];
+ for (size_t row = 0; row < rows; ++row) {
+ hasher.Update(plane_data.subspan(frame.stride(plane) * row, row_bytes));
}
}
}
// static
-std::string VideoFrame::HexHashOfFrameForTesting(const VideoFrame& frame) {
+std::string VideoFrame::HexHashOfFrameForTesting(const VideoFrame& frame,
+ bool visible_data_only) {
crypto::hash::Hasher hasher(crypto::hash::HashKind::kSha256);
- UpdateHashWithFrameForTesting(hasher, frame); // IN-TEST
+ UpdateHashWithFrameForTesting(hasher, frame, visible_data_only); // IN-TEST
std::array<uint8_t, crypto::hash::kSha256Size> hash;
hasher.Finish(hash);
return base::ToLowerASCII(base::HexEncode(hash));
@@ -1462,6 +1468,14 @@
return Rows(plane, format(), coded_size().height());
}
+int VideoFrame::GetVisibleRowBytes(size_t plane) const {
+ return RowBytes(plane, format(), visible_rect().width());
+}
+
+int VideoFrame::GetVisibleRows(size_t plane) const {
+ return Rows(plane, format(), visible_rect().height());
+}
+
int VideoFrame::columns(size_t plane) const {
return Columns(plane, format(), coded_size().width());
}
@@ -1482,7 +1496,6 @@
base::bits::AlignDownDeprecatedDoNotUse(
visible_rect_.y(), alignment.height()));
- const int visible_plane_rows = Rows(plane, format(), visible_rect_.height());
const int plane_stride = stride(plane);
const gfx::Size subsample = SampleSize(format(), plane);
DCHECK(offset.x() % subsample.width() == 0);
@@ -1494,8 +1507,8 @@
BytesPerElement(format(), plane) * (offset.x() / subsample.width()));
// In the last row, bytes between visible width and the full stride are not
// the part of the visible plane.
- size_t visible_plane_size = plane_stride * (visible_plane_rows - 1) +
- RowBytes(plane, format(), visible_rect_.width());
+ size_t visible_plane_size =
+ plane_stride * (GetVisibleRows(plane) - 1) + GetVisibleRowBytes(plane);
return data.subspan(visible_plane_offset, visible_plane_size);
}
diff --git a/media/base/video_frame.h b/media/base/video_frame.h
index 72b870c..56d47377 100644
--- a/media/base/video_frame.h
+++ b/media/base/video_frame.h
@@ -478,12 +478,16 @@
static size_t Columns(size_t plane, VideoPixelFormat format, int width);
// Given a crypto/hash Hasher, hash in the pixels from a single VideoFrame.
+ // If `visible_data_only` is true only the frame's visible area will be
+ // hashed, if false then the entire coded frame area will be hashed.
static void UpdateHashWithFrameForTesting(crypto::hash::Hasher& hasher,
- const VideoFrame& frame);
+ const VideoFrame& frame,
+ bool visible_data_only = true);
// Convenience wrapper around UpdateHashWithFrameForTesting(): produces the
// SHA-256 hash of a single video frame's pixels, as a lowercase hex string.
- static std::string HexHashOfFrameForTesting(const VideoFrame& frame);
+ static std::string HexHashOfFrameForTesting(const VideoFrame& frame,
+ bool visible_data_only = true);
// Returns true if |frame| is accessible mapped in the VideoFrame memory
// space.
@@ -627,6 +631,10 @@
int row_bytes(size_t plane) const;
int rows(size_t plane) const;
+ // Similar to row_bytes() and rows(), but instead refers to the visible area.
+ int GetVisibleRowBytes(size_t plane) const;
+ int GetVisibleRows(size_t plane) const;
+
// Returns the number of columns for a given plane.
int columns(size_t plane) const;
diff --git a/media/filters/vpx_video_decoder.cc b/media/filters/vpx_video_decoder.cc
index 69481802..55e269d 100644
--- a/media/filters/vpx_video_decoder.cc
+++ b/media/filters/vpx_video_decoder.cc
@@ -620,8 +620,8 @@
libyuv::CopyPlane(planes[plane], strides[plane],
(*video_frame)->GetWritableVisibleData(plane),
(*video_frame)->stride(plane),
- (*video_frame)->row_bytes(plane),
- (*video_frame)->rows(plane));
+ (*video_frame)->GetVisibleRowBytes(plane),
+ (*video_frame)->GetVisibleRows(plane));
}
return true;
diff --git a/third_party/blink/renderer/modules/webcodecs/video_frame.cc b/third_party/blink/renderer/modules/webcodecs/video_frame.cc
index c3d072b..7815456 100644
--- a/third_party/blink/renderer/modules/webcodecs/video_frame.cc
+++ b/third_party/blink/renderer/modules/webcodecs/video_frame.cc
@@ -1035,22 +1035,23 @@
// Set up the copy to be minimally-sized. Note: The parameters to the
// CopyPlane() method below depend on coded_size == visible_size.
gfx::Rect crop = src_visible_rect;
- gfx::Size dest_coded_size = crop.size();
gfx::Rect dest_visible_rect = gfx::Rect(crop.size());
// The array buffer hasn't been transferred, we need to allocate and
// copy pixel data.
auto& frame_pool = CachedVideoFramePool::From(*execution_context);
- frame = frame_pool.CreateFrame(media_fmt, dest_coded_size,
+ frame = frame_pool.CreateFrame(media_fmt, /*coded_size=*/crop.size(),
dest_visible_rect, display_size, timestamp);
+ // Note: CreateFrame() may expand the coded size for odd sized frames.
+
if (!frame) {
exception_state.ThrowDOMException(
DOMExceptionCode::kOperationError,
String::Format("Failed to create a VideoFrame with format: %s, "
"coded size: %s, visibleRect: %s, display size: %s.",
VideoPixelFormatToString(media_fmt).c_str(),
- dest_coded_size.ToString().c_str(),
+ crop.size().ToString().c_str(),
dest_visible_rect.ToString().c_str(),
display_size.ToString().c_str()));
return nullptr;
@@ -1059,7 +1060,8 @@
for (wtf_size_t i = 0; i < media::VideoFrame::NumPlanes(media_fmt); i++) {
libyuv::CopyPlane(src_frame->visible_data(i), src_frame->stride(i),
frame->GetWritableVisibleData(i), frame->stride(i),
- frame->row_bytes(i), frame->rows(i));
+ /*width=*/src_frame->GetVisibleRowBytes(i),
+ /*height=*/src_frame->GetVisibleRows(i));
}
}
diff --git a/media/base/video_frame_unittest.cc b/media/base/video_frame_unittest.cc
index 2b906fa..01280fe0 100644
--- a/media/base/video_frame_unittest.cc
+++ b/media/base/video_frame_unittest.cc
@@ -240,7 +240,9 @@
frame->stride(plane) * frame->rows(plane));
}
- EXPECT_EQ(VideoFrame::HexHashOfFrameForTesting(*frame.get()), expected_hash);
+ EXPECT_EQ(VideoFrame::HexHashOfFrameForTesting(*frame,
+ /*visible_data_only=*/false),
+ expected_hash);
}
TEST(VideoFrame, CreateFrame) {
@@ -785,6 +787,75 @@
EXPECT_EQ(677, frame->coded_size().width());
}
+TEST(VideoFrame, CreateFrame_OddSize) {
+ constexpr gfx::Size odd_size(677, 287);
+ constexpr auto kTimestamp = base::TimeDelta();
+
+ // I420A frames should have the Y, A planes aligned upward to an even size,
+ // since it's required by the subsampled U,V planes.
+ auto frame = VideoFrame::CreateFrame(
+ PIXEL_FORMAT_I420A, odd_size, gfx::Rect(odd_size), odd_size, kTimestamp);
+ ASSERT_TRUE(frame);
+ EXPECT_EQ(gfx::Size(678, 288), frame->coded_size());
+ for (int plane = 0; plane < 4; plane++) {
+ // CreateFrame() will always allocate to an even coded size for non-sampled
+ // planes, so the visible row data should be less than the coded row data.
+ if (plane == VideoFrame::Plane::kY || plane == VideoFrame::Plane::kA) {
+ EXPECT_LT(frame->GetVisibleRowBytes(plane), frame->row_bytes(plane));
+ EXPECT_LT(frame->GetVisibleRows(plane), frame->rows(plane));
+ } else {
+ EXPECT_EQ(frame->GetVisibleRowBytes(plane), frame->row_bytes(plane));
+ EXPECT_EQ(frame->GetVisibleRows(plane), frame->rows(plane));
+ }
+ }
+
+ // I444A frames don't have subsampling, so no adjustment is needed.
+ frame = VideoFrame::CreateFrame(PIXEL_FORMAT_I444A, odd_size,
+ gfx::Rect(odd_size), odd_size, kTimestamp);
+ ASSERT_TRUE(frame);
+ EXPECT_EQ(odd_size, frame->coded_size());
+ for (int plane = 0; plane < 4; plane++) {
+ EXPECT_EQ(frame->GetVisibleRowBytes(plane), frame->row_bytes(plane));
+ EXPECT_EQ(frame->GetVisibleRows(plane), frame->rows(plane));
+ }
+}
+
+TEST(VideoFrame, RowBytes) {
+ constexpr gfx::Size kCodedSize(16, 14);
+ constexpr gfx::Rect kVisibleRect(4, 4, 8, 8);
+
+ // Just spot test I420A for simplicity.
+ scoped_refptr<VideoFrame> frame =
+ VideoFrame::CreateFrame(PIXEL_FORMAT_I420A, kCodedSize, kVisibleRect,
+ kVisibleRect.size(), base::TimeDelta());
+ ASSERT_TRUE(frame);
+ ASSERT_EQ(frame->row_bytes(VideoFrame::Plane::kY), kCodedSize.width());
+ ASSERT_EQ(frame->row_bytes(VideoFrame::Plane::kU), kCodedSize.width() / 2);
+ ASSERT_EQ(frame->row_bytes(VideoFrame::Plane::kV), kCodedSize.width() / 2);
+ ASSERT_EQ(frame->row_bytes(VideoFrame::Plane::kA), kCodedSize.width());
+ ASSERT_EQ(frame->rows(VideoFrame::Plane::kY), kCodedSize.height());
+ ASSERT_EQ(frame->rows(VideoFrame::Plane::kU), kCodedSize.height() / 2);
+ ASSERT_EQ(frame->rows(VideoFrame::Plane::kV), kCodedSize.height() / 2);
+ ASSERT_EQ(frame->rows(VideoFrame::Plane::kA), kCodedSize.height());
+
+ ASSERT_EQ(frame->GetVisibleRowBytes(VideoFrame::Plane::kY),
+ kVisibleRect.width());
+ ASSERT_EQ(frame->GetVisibleRowBytes(VideoFrame::Plane::kU),
+ kVisibleRect.width() / 2);
+ ASSERT_EQ(frame->GetVisibleRowBytes(VideoFrame::Plane::kV),
+ kVisibleRect.width() / 2);
+ ASSERT_EQ(frame->GetVisibleRowBytes(VideoFrame::Plane::kA),
+ kVisibleRect.width());
+ ASSERT_EQ(frame->GetVisibleRows(VideoFrame::Plane::kY),
+ kVisibleRect.height());
+ ASSERT_EQ(frame->GetVisibleRows(VideoFrame::Plane::kU),
+ kVisibleRect.height() / 2);
+ ASSERT_EQ(frame->GetVisibleRows(VideoFrame::Plane::kV),
+ kVisibleRect.height() / 2);
+ ASSERT_EQ(frame->GetVisibleRows(VideoFrame::Plane::kA),
+ kVisibleRect.height());
+}
+
TEST(VideoFrame, AllocationSize_OddSize) {
const gfx::Size size(3, 5);
diff --git a/media/test/pipeline_integration_test.cc b/media/test/pipeline_integration_test.cc
index fb0c86b..a69a08b 100644
--- a/media/test/pipeline_integration_test.cc
+++ b/media/test/pipeline_integration_test.cc
@@ -3035,7 +3035,8 @@
ASSERT_EQ(PIPELINE_OK, StartPipelineWithHlsManifest("hls/mp_ts_avc1.m3u8"));
Play();
ASSERT_TRUE(WaitUntilOnEnded());
- EXPECT_EQ("00df0aa6796123f535402c39c20baa3fe1c2ad02fe1f015f84878977b30931d2",
+ // 320x192 video of which only 320x180 is visible.
+ EXPECT_EQ("9537d9d2592aa801cff8fceb2af9f6e3c5226df089e16f8f789d43e1fdec7ba2",
GetVideoHash());
}
#endif
diff --git a/third_party/blink/renderer/modules/webcodecs/video_frame_test.cc b/third_party/blink/renderer/modules/webcodecs/video_frame_test.cc
index 7fa9293d..15030a2f 100644
--- a/third_party/blink/renderer/modules/webcodecs/video_frame_test.cc
+++ b/third_party/blink/renderer/modules/webcodecs/video_frame_test.cc
@@ -18,6 +18,7 @@
#include "third_party/blink/renderer/bindings/modules/v8/v8_union_blob_htmlcanvaselement_htmlimageelement_htmlvideoelement_imagebitmap_imagedata_offscreencanvas_svgimageelement_videoframe.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_union_cssimagevalue_htmlcanvaselement_htmlimageelement_htmlvideoelement_imagebitmap_offscreencanvas_svgimageelement_videoframe.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_video_decoder_config.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_video_frame_buffer_init.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_video_frame_copy_to_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_video_frame_init.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_video_frame_metadata.h"
@@ -114,6 +115,78 @@
EXPECT_EQ(nullptr, blink_frame->frame());
}
+TEST_F(VideoFrameTest, ConstructorOddSize) {
+ V8TestingScope scope;
+
+ constexpr auto kOddSize = gfx::Size(61, 21);
+ const auto kOddUVSize = gfx::Size(std::ceil(kOddSize.width() / 2.0),
+ std::ceil(kOddSize.height() / 2.0));
+ const size_t allocation_size =
+ kOddSize.Area64() * 2 + kOddUVSize.Area64() * 2;
+
+ auto* array_buffer = DOMArrayBuffer::Create(allocation_size, 1);
+
+ // Fill buffer with random data for hash and extents testing.
+ base::RandBytes(array_buffer->ByteSpan());
+
+ std::string media_frame_hash;
+ {
+ const size_t kYAPlaneByteSize = kOddSize.Area64();
+ const size_t kUVPlaneByteSize = kOddUVSize.Area64();
+ auto src_media_frame = media::VideoFrame::WrapExternalYuvaData(
+ media::PIXEL_FORMAT_I420A, kOddSize, gfx::Rect(kOddSize), kOddSize,
+ kOddSize.width(), kOddUVSize.width(), kOddUVSize.width(),
+ kOddSize.width(), array_buffer->ByteSpan().first(kYAPlaneByteSize),
+ array_buffer->ByteSpan().subspan(kYAPlaneByteSize, kUVPlaneByteSize),
+ array_buffer->ByteSpan().subspan(kYAPlaneByteSize + kUVPlaneByteSize,
+ kUVPlaneByteSize),
+ array_buffer->ByteSpan().subspan(
+ kYAPlaneByteSize + kUVPlaneByteSize * 2, kYAPlaneByteSize),
+ base::TimeDelta());
+ ASSERT_TRUE(src_media_frame);
+ media_frame_hash =
+ media::VideoFrame::HexHashOfFrameForTesting(*src_media_frame);
+ }
+
+ auto* init = VideoFrameBufferInit::Create();
+ init->setTimestamp(0);
+ init->setCodedWidth(kOddSize.width());
+ init->setCodedHeight(kOddSize.height());
+ init->setFormat("I420A");
+ init->setDisplayWidth(kOddSize.width());
+ init->setDisplayHeight(kOddSize.height());
+
+ // Test non-transfer constructor first then the transfer constructor.
+ for (bool test_transfer : {false, true}) {
+ SCOPED_TRACE(test_transfer);
+ if (test_transfer) {
+ HeapVector<Member<DOMArrayBuffer>> transfer;
+ transfer.push_back(Member<DOMArrayBuffer>(array_buffer));
+ init->setTransfer(std::move(transfer));
+ }
+
+ VideoFrame* blink_frame = VideoFrame::Create(
+ scope.GetScriptState(),
+ MakeGarbageCollected<V8AllowSharedBufferSource>(array_buffer), init,
+ scope.GetExceptionState());
+ ASSERT_TRUE(blink_frame);
+
+ EXPECT_LE(static_cast<unsigned>(kOddSize.width()),
+ blink_frame->codedWidth());
+ EXPECT_LE(static_cast<unsigned>(kOddSize.height()),
+ blink_frame->codedHeight());
+ EXPECT_EQ(static_cast<unsigned>(kOddSize.width()),
+ blink_frame->displayWidth());
+ EXPECT_EQ(static_cast<unsigned>(kOddSize.height()),
+ blink_frame->displayHeight());
+
+ auto blink_media_frame = blink_frame->frame();
+ EXPECT_EQ(media_frame_hash,
+ media::VideoFrame::HexHashOfFrameForTesting(*blink_media_frame));
+ blink_frame->close();
+ }
+}
+
TEST_F(VideoFrameTest, CopyToRGB) {
V8TestingScope scope;
Regression Test / PoC
diff --git a/media/base/video_frame_unittest.cc b/media/base/video_frame_unittest.cc
index 2b906fa..01280fe0 100644
--- a/media/base/video_frame_unittest.cc
+++ b/media/base/video_frame_unittest.cc
@@ -240,7 +240,9 @@
frame->stride(plane) * frame->rows(plane));
}
- EXPECT_EQ(VideoFrame::HexHashOfFrameForTesting(*frame.get()), expected_hash);
+ EXPECT_EQ(VideoFrame::HexHashOfFrameForTesting(*frame,
+ /*visible_data_only=*/false),
+ expected_hash);
}
TEST(VideoFrame, CreateFrame) {
@@ -785,6 +787,75 @@
EXPECT_EQ(677, frame->coded_size().width());
}
+TEST(VideoFrame, CreateFrame_OddSize) {
+ constexpr gfx::Size odd_size(677, 287);
+ constexpr auto kTimestamp = base::TimeDelta();
+
+ // I420A frames should have the Y, A planes aligned upward to an even size,
+ // since it's required by the subsampled U,V planes.
+ auto frame = VideoFrame::CreateFrame(
+ PIXEL_FORMAT_I420A, odd_size, gfx::Rect(odd_size), odd_size, kTimestamp);
+ ASSERT_TRUE(frame);
+ EXPECT_EQ(gfx::Size(678, 288), frame->coded_size());
+ for (int plane = 0; plane < 4; plane++) {
+ // CreateFrame() will always allocate to an even coded size for non-sampled
+ // planes, so the visible row data should be less than the coded row data.
+ if (plane == VideoFrame::Plane::kY || plane == VideoFrame::Plane::kA) {
+ EXPECT_LT(frame->GetVisibleRowBytes(plane), frame->row_bytes(plane));
+ EXPECT_LT(frame->GetVisibleRows(plane), frame->rows(plane));
+ } else {
+ EXPECT_EQ(frame->GetVisibleRowBytes(plane), frame->row_bytes(plane));
+ EXPECT_EQ(frame->GetVisibleRows(plane), frame->rows(plane));
+ }
+ }
+
+ // I444A frames don't have subsampling, so no adjustment is needed.
+ frame = VideoFrame::CreateFrame(PIXEL_FORMAT_I444A, odd_size,
+ gfx::Rect(odd_size), odd_size, kTimestamp);
+ ASSERT_TRUE(frame);
+ EXPECT_EQ(odd_size, frame->coded_size());
+ for (int plane = 0; plane < 4; plane++) {
+ EXPECT_EQ(frame->GetVisibleRowBytes(plane), frame->row_bytes(plane));
+ EXPECT_EQ(frame->GetVisibleRows(plane), frame->rows(plane));
+ }
+}
+
+TEST(VideoFrame, RowBytes) {
+ constexpr gfx::Size kCodedSize(16, 14);
+ constexpr gfx::Rect kVisibleRect(4, 4, 8, 8);
+
+ // Just spot test I420A for simplicity.
+ scoped_refptr<VideoFrame> frame =
+ VideoFrame::CreateFrame(PIXEL_FORMAT_I420A, kCodedSize, kVisibleRect,
+ kVisibleRect.size(), base::TimeDelta());
+ ASSERT_TRUE(frame);
+ ASSERT_EQ(frame->row_bytes(VideoFrame::Plane::kY), kCodedSize.width());
+ ASSERT_EQ(frame->row_bytes(VideoFrame::Plane::kU), kCodedSize.width() / 2);
+ ASSERT_EQ(frame->row_bytes(VideoFrame::Plane::kV), kCodedSize.width() / 2);
+ ASSERT_EQ(frame->row_bytes(VideoFrame::Plane::kA), kCodedSize.width());
+ ASSERT_EQ(frame->rows(VideoFrame::Plane::kY), kCodedSize.height());
+ ASSERT_EQ(frame->rows(VideoFrame::Plane::kU), kCodedSize.height() / 2);
+ ASSERT_EQ(frame->rows(VideoFrame::Plane::kV), kCodedSize.height() / 2);
+ ASSERT_EQ(frame->rows(VideoFrame::Plane::kA), kCodedSize.height());
+
+ ASSERT_EQ(frame->GetVisibleRowBytes(VideoFrame::Plane::kY),
+ kVisibleRect.width());
+ ASSERT_EQ(frame->GetVisibleRowBytes(VideoFrame::Plane::kU),
+ kVisibleRect.width() / 2);
+ ASSERT_EQ(frame->GetVisibleRowBytes(VideoFrame::Plane::kV),
+ kVisibleRect.width() / 2);
+ ASSERT_EQ(frame->GetVisibleRowBytes(VideoFrame::Plane::kA),
+ kVisibleRect.width());
+ ASSERT_EQ(frame->GetVisibleRows(VideoFrame::Plane::kY),
+ kVisibleRect.height());
+ ASSERT_EQ(frame->GetVisibleRows(VideoFrame::Plane::kU),
+ kVisibleRect.height() / 2);
+ ASSERT_EQ(frame->GetVisibleRows(VideoFrame::Plane::kV),
+ kVisibleRect.height() / 2);
+ ASSERT_EQ(frame->GetVisibleRows(VideoFrame::Plane::kA),
+ kVisibleRect.height());
+}
+
TEST(VideoFrame, AllocationSize_OddSize) {
const gfx::Size size(3, 5);
diff --git a/media/test/pipeline_integration_test.cc b/media/test/pipeline_integration_test.cc
index fb0c86b..a69a08b 100644
--- a/media/test/pipeline_integration_test.cc
+++ b/media/test/pipeline_integration_test.cc
@@ -3035,7 +3035,8 @@
ASSERT_EQ(PIPELINE_OK, StartPipelineWithHlsManifest("hls/mp_ts_avc1.m3u8"));
Play();
ASSERT_TRUE(WaitUntilOnEnded());
- EXPECT_EQ("00df0aa6796123f535402c39c20baa3fe1c2ad02fe1f015f84878977b30931d2",
+ // 320x192 video of which only 320x180 is visible.
+ EXPECT_EQ("9537d9d2592aa801cff8fceb2af9f6e3c5226df089e16f8f789d43e1fdec7ba2",
GetVideoHash());
}
#endif
diff --git a/third_party/blink/renderer/modules/webcodecs/video_frame_test.cc b/third_party/blink/renderer/modules/webcodecs/video_frame_test.cc
index 7fa9293d..15030a2f 100644
--- a/third_party/blink/renderer/modules/webcodecs/video_frame_test.cc
+++ b/third_party/blink/renderer/modules/webcodecs/video_frame_test.cc
@@ -18,6 +18,7 @@
#include "third_party/blink/renderer/bindings/modules/v8/v8_union_blob_htmlcanvaselement_htmlimageelement_htmlvideoelement_imagebitmap_imagedata_offscreencanvas_svgimageelement_videoframe.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_union_cssimagevalue_htmlcanvaselement_htmlimageelement_htmlvideoelement_imagebitmap_offscreencanvas_svgimageelement_videoframe.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_video_decoder_config.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_video_frame_buffer_init.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_video_frame_copy_to_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_video_frame_init.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_video_frame_metadata.h"
@@ -114,6 +115,78 @@
EXPECT_EQ(nullptr, blink_frame->frame());
}
+TEST_F(VideoFrameTest, ConstructorOddSize) {
+ V8TestingScope scope;
+
+ constexpr auto kOddSize = gfx::Size(61, 21);
+ const auto kOddUVSize = gfx::Size(std::ceil(kOddSize.width() / 2.0),
+ std::ceil(kOddSize.height() / 2.0));
+ const size_t allocation_size =
+ kOddSize.Area64() * 2 + kOddUVSize.Area64() * 2;
+
+ auto* array_buffer = DOMArrayBuffer::Create(allocation_size, 1);
+
+ // Fill buffer with random data for hash and extents testing.
+ base::RandBytes(array_buffer->ByteSpan());
+
+ std::string media_frame_hash;
+ {
+ const size_t kYAPlaneByteSize = kOddSize.Area64();
+ const size_t kUVPlaneByteSize = kOddUVSize.Area64();
+ auto src_media_frame = media::VideoFrame::WrapExternalYuvaData(
+ media::PIXEL_FORMAT_I420A, kOddSize, gfx::Rect(kOddSize), kOddSize,
+ kOddSize.width(), kOddUVSize.width(), kOddUVSize.width(),
+ kOddSize.width(), array_buffer->ByteSpan().first(kYAPlaneByteSize),
+ array_buffer->ByteSpan().subspan(kYAPlaneByteSize, kUVPlaneByteSize),
+ array_buffer->ByteSpan().subspan(kYAPlaneByteSize + kUVPlaneByteSize,
+ kUVPlaneByteSize),
+ array_buffer->ByteSpan().subspan(
+ kYAPlaneByteSize + kUVPlaneByteSize * 2, kYAPlaneByteSize),
+ base::TimeDelta());
+ ASSERT_TRUE(src_media_frame);
+ media_frame_hash =
+ media::VideoFrame::HexHashOfFrameForTesting(*src_media_frame);
+ }
+
+ auto* init = VideoFrameBufferInit::Create();
+ init->setTimestamp(0);
+ init->setCodedWidth(kOddSize.width());
+ init->setCodedHeight(kOddSize.height());
+ init->setFormat("I420A");
+ init->setDisplayWidth(kOddSize.width());
+ init->setDisplayHeight(kOddSize.height());
+
+ // Test non-transfer constructor first then the transfer constructor.
+ for (bool test_transfer : {false, true}) {
+ SCOPED_TRACE(test_transfer);
+ if (test_transfer) {
+ HeapVector<Member<DOMArrayBuffer>> transfer;
+ transfer.push_back(Member<DOMArrayBuffer>(array_buffer));
+ init->setTransfer(std::move(transfer));
+ }
+
+ VideoFrame* blink_frame = VideoFrame::Create(
+ scope.GetScriptState(),
+ MakeGarbageCollected<V8AllowSharedBufferSource>(array_buffer), init,
+ scope.GetExceptionState());
+ ASSERT_TRUE(blink_frame);
+
+ EXPECT_LE(static_cast<unsigned>(kOddSize.width()),
+ blink_frame->codedWidth());
+ EXPECT_LE(static_cast<unsigned>(kOddSize.height()),
+ blink_frame->codedHeight());
+ EXPECT_EQ(static_cast<unsigned>(kOddSize.width()),
+ blink_frame->displayWidth());
+ EXPECT_EQ(static_cast<unsigned>(kOddSize.height()),
+ blink_frame->displayHeight());
+
+ auto blink_media_frame = blink_frame->frame();
+ EXPECT_EQ(media_frame_hash,
+ media::VideoFrame::HexHashOfFrameForTesting(*blink_media_frame));
+ blink_frame->close();
+ }
+}
+
TEST_F(VideoFrameTest, CopyToRGB) {
V8TestingScope scope;
Original Bug Report
WebCodecs VideoFrame constructor crashes browser when non-even width and height are used
Steps to reproduce the problem
Constructing a VideoFrame from array buffer source with non-even width and height crashes the browser. If only one of width and height is non-even it works. Happens only on non-gpu accelerated ARM linux, e.g. AWS Graviton.
This code:
const width = 1139; const height = 641; const buffer = new Uint8Array(2widthheight + 2 * (Math.ceil(width / 2) * Math.ceil(height / 2))); const i420aFrame = new VideoFrame(buffer, { format: ‘I420A’, codedWidth: width, codedHeight: height, displayWidth: width, displayHeight: height, timestamp: 0, });
Potentially reproducible when running Chrome without GPU accelerated video decode on arm linux. Easiest to reproduce with puppeteer on arm linux, try opening this page: https://cache-ssl.celtra.io/api/blobs/3cf5aa9adedb3b14054869474264e73c004c1bbd472aa8ce052abd76fddbaf8a/reproduction.html. It’s the JS code from above, hosted on https so that WebCodecs API is available.
Puppeteer script for reproduction: https://gist.github.com/jkosir/3134cbe9c95df268ebbcda3c9475f03b . Must be run on arm linux, also note that puppeteer install does not support arm linux and chrome binary must be installed with system package manager or obtained in other way.
Problem Description
Constructing a VideoFrame from array buffer source with non-even width and height crashes the browser. If only one of width and height is non-even it works. Happens only on non-gpu accelerated ARM linux, e.g. AWS Graviton. Works as expected in 134.0.6998.165 (and some alter versions, was not able to bisect), reproducible in 138.0.7204.100 and above stable versions.
Stacktrace on crash: Received signal 11 SEGV_ACCERR fbec001c2000 #0 0xbc3885f311e8 base::debug::CollectStackTrace() #1 0xbc3885f20bc0 base::debug::StackTrace::StackTrace() #2 0xbc3885f31094 base::debug::(anonymous namespace)::StackDumpSignalHandler() #3 0xfd2981cfc8f8 ([vdso]+0x8f7) #4 0xbc38868bf3bc CopyRow_NEON #5 0xbc38868a41b0 CopyRow_Any_NEON #6 0xbc388689e8a0 CopyPlane #7 0xbc388987d778 blink::VideoFrame::Create() #8 0xbc38892bab8c blink::(anonymous namespace)::v8_video_frame::ConstructorOverload2() #9 0xbc38839d01ac v8::internal::FunctionCallbackArguments::CallOrConstruct() #10 0xbc38839cfcb0 v8::internal::(anonymous namespace)::HandleApiCallHelper<>() #11 0xbc38839cf5fc v8::internal::Builtin_HandleApiConstruct() #12 0xbc38f7eb30e4 <unknown> [end of stack trace]
I suspect it has something to do with this change in video_frame.cc: https://github.com/chromium/chromium/commit/a4a5b472dbe783f4a668916c28d0aa6c56680ce9. Tried building chromium with older libyuv (that implements the NEON copy) version, as used in 134.0.6998.165, same issue persists.
Summary
WebCodecs VideoFrame constructor crashes browser when non-even width and height are used
Additional Data
Category: JavaScript
Chrome Channel: Stable
Regression: Yes \