diff --git a/third_party/blink/renderer/platform/peerconnection/rtc_video_encoder_test.cc b/third_party/blink/renderer/platform/peerconnection/rtc_video_encoder_test.cc index 7a400043724ed..baeabdbc5a3f8 100644 --- a/third_party/blink/renderer/platform/peerconnection/rtc_video_encoder_test.cc +++ b/third_party/blink/renderer/platform/peerconnection/rtc_video_encoder_test.cc @@ -44,6 +44,7 @@ #include "third_party/blink/renderer/platform/webrtc/testing/mock_webrtc_video_frame_adapter_shared_resources.h" #include "third_party/blink/renderer/platform/webrtc/webrtc_video_frame_adapter.h" #include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h" +#include "third_party/libyuv/include/libyuv/cpu_id.h" #include "third_party/libyuv/include/libyuv/planar_functions.h" #include "third_party/webrtc/api/video/i420_buffer.h" #include "third_party/webrtc/api/video/video_frame_buffer.h" @@ -1372,6 +1373,89 @@ TEST_F(RTCVideoEncoderEncodeTest, SoftwareFallbackOnBadEncodeInput) { &frame_types)); } +// POC: heap-buffer-overflow READ in CreateNV12SharedImageFrame. +// +// On ChromeOS (where kVideoCaptureUseGpuMemoryBuffer is set by default and the +// IS_WIN/IS_ANDROID storage-type toggle in Enqueue() does not apply), a CPU +// I420 frame whose visible_rect is larger than its natural_size — trivially +// constructible from JS via `new VideoFrame(buf, {... displayWidth/Height < +// visibleRect})` and pushed through a MediaStreamTrackGenerator into an +// RTCPeerConnection — reaches CreateNV12SharedImageFrame(). There, +// `frame_buffer.ToI420()` materialises an I420 buffer at natural_size while +// `libyuv::I420ToNV12` is called with width/height = visible_rect.size(), +// reading far past the end of the natural_size-sized heap allocation. +// +// This test reproduces that exact code path under a mock VEA + test SII (since +// no HW encoder is available in this environment) and is expected to *crash* +// with an ASAN heap-buffer-overflow READ inside libyuv::CopyPlane / +// MergeUVPlane under RTCVideoEncoder::Impl::CreateNV12SharedImageFrame. +TEST_F(RTCVideoEncoderEncodeTest, + CreateNV12SharedImageFrame_VisibleRectLargerThanNaturalSize_OOBRead) { + // 0. Force libyuv onto its C row-functions so ASAN can observe the loads. + // The OOB read happens regardless of the row impl; the optimised paths + // use inline asm that ASAN cannot instrument. + libyuv::MaskCpuFlags(libyuv::kCpuInitialized); + + // 1. Force the native-input (NV12 + GpuMemoryBuffer) path. This switch is on + // by default on ChromeOS via overlays. + base::CommandLine::ForCurrentProcess()->AppendSwitch( + switches::kVideoCaptureUseGpuMemoryBuffer); + + // 2. Re-create mock_gpu_factories_ with a real TestSharedImageInterface so + // that CreateClientSharedImage() in CreateNV12SharedImageFrame succeeds + // and we reach the vulnerable libyuv::I420ToNV12 call. + scoped_refptr test_sii = + base::MakeRefCounted(); + mock_gpu_factories_ = + std::make_unique(test_sii.get()); + EXPECT_CALL(*mock_gpu_factories_, GetTaskRunner()) + .WillRepeatedly(Return(encoder_thread_.task_runner())); + + const webrtc::VideoCodec codec = GetDefaultCodec(webrtc::kVideoCodecVP8); + CreateEncoder(codec.codecType); + ExpectCreateInitAndDestroyVEA( + media::PIXEL_FORMAT_NV12, + media::VideoEncodeAccelerator::Config::StorageType::kGpuMemoryBuffer); + ASSERT_EQ(WEBRTC_VIDEO_CODEC_OK, + rtc_encoder_->InitEncode(&codec, kVideoEncoderSettings)); + + // 3. Build the attacker frame: visible_rect 640x480 > natural_size 480x360. + // This is exactly what JS produces for + // new VideoFrame(buf, {format:'I420', codedWidth:640, codedHeight:480, + // visibleRect:{x:0,y:0,width:640,height:480}, + // displayWidth:480, displayHeight:360, ...}); + constexpr gfx::Size kCoded(640, 480); + constexpr gfx::Rect kVisible(0, 0, 640, 480); + constexpr gfx::Size kNatural(480, 360); + scoped_refptr media_frame = + media::VideoFrame::CreateZeroInitializedFrame( + media::PIXEL_FORMAT_I420, kCoded, kVisible, kNatural, + base::Milliseconds(1)); + ASSERT_TRUE(media_frame); + ASSERT_EQ(media_frame->visible_rect().size(), kVisible.size()); + ASSERT_EQ(media_frame->natural_size(), kNatural); + + webrtc::scoped_refptr frame_adapter( + new webrtc::RefCountedObject( + media_frame, + WebRtcVideoFrameAdapter::SharedResources::Create(nullptr))); + + // 4. Encode. On the encoder thread this enters EncodeOneFrameWithNativeInput + // → (no SharedImage) → MaybeConvertRGBAToNV12AndEncode returns false → + // CreateNV12SharedImageFrame → ToI420() allocates a 480x360 buffer → + // libyuv::I420ToNV12 reads 640x480 from it. ASAN catches the OOB read. + EXPECT_CALL(*mock_vea_, Encode).Times(::testing::AnyNumber()); + std::vector frame_types; + rtc_encoder_->Encode(webrtc::VideoFrame::Builder() + .set_video_frame_buffer(frame_adapter) + .set_rtp_timestamp(1000) + .set_timestamp_us(2000) + .set_rotation(webrtc::kVideoRotation_0) + .build(), + &frame_types); + RunUntilIdle(); +} + TEST_F(RTCVideoEncoderEncodeTest, EncodeScaledFrame) { const webrtc::VideoCodec codec = GetDefaultCodec(webrtc::kVideoCodecVP8); CreateEncoder(codec.codecType);