diff --git a/media/gpu/vaapi/vp9_vaapi_video_encoder_delegate_unittest.cc b/media/gpu/vaapi/vp9_vaapi_video_encoder_delegate_unittest.cc index 989c1b2f2155d..92fcd7032279e 100644 --- a/media/gpu/vaapi/vp9_vaapi_video_encoder_delegate_unittest.cc +++ b/media/gpu/vaapi/vp9_vaapi_video_encoder_delegate_unittest.cc @@ -298,6 +298,7 @@ class VP9VaapiVideoEncoderDelegateTest void UpdateRatesTest(SVCInterLayerPredMode inter_layer_pred, size_t num_spatial_layers, size_t num_temporal_layers); + void RunSpatialLayerSizeMismatchPOC(); void UpdateRatesAndEncode( SVCInterLayerPredMode inter_layer_pred, const VideoBitrateAllocation& bitrate_allocation, @@ -890,6 +891,155 @@ TEST_P(VP9VaapiVideoEncoderDelegateTest, FailsWithInvalidSpatialLayers) { } } +// Regression / security demonstration test for the VP9 k-SVC spatial-layer +// size validation gap. A renderer-supplied VideoEncodeAccelerator::Config with +// input_visible_size << spatial_layers.back() must be rejected somewhere on +// the GPU side; otherwise vaCreateContext() and the VAEncCodedBuffer are sized +// from |input_visible_size| while VAEncPictureParameterBufferVP9 and the +// input/reconstructed surfaces are sized from the attacker-chosen +// spatial-layer dimensions, and the VA-API driver writes a 16384x16384 encode +// into context state and a coded buffer sized for 320x256 (driver-heap OOB +// write inside the GPU process). +// +// This test feeds the malicious config through Initialize() and one +// PrepareEncodeJob() per spatial layer, captures the VA buffers that would be +// handed to vaRenderPicture(), and asserts the security invariant +// pic_param.frame_{width,height}_src <= vaCreateContext picture_{width,height}. +// On a vulnerable build the assertion FAILS, proving the VA-API contract +// violation that leads to the driver-heap OOB write on real hardware. +void VP9VaapiVideoEncoderDelegateTest::RunSpatialLayerSizeMismatchPOC() { + // Build the malicious config a compromised renderer can send over + // media.mojom.VideoEncodeAccelerator.Initialize. Both input_visible_size and + // every spatial layer pass the kMaxDimension/kMaxCanvas bounds in + // MojoVideoEncodeAcceleratorService::Initialize, and the layer list passes + // the is_sorted check in VaapiVideoEncodeAccelerator::Initialize. + constexpr gfx::Size kInputVisibleSize(320, 240); + constexpr int kHuge = 16384; // 16384*16384 = 268,435,456 < kMaxCanvas + static_assert(uint64_t{kHuge} * kHuge < (uint64_t{1} << 29) - 1, + "must pass MojoVEAService kMaxCanvas check"); + + VideoEncodeAccelerator::Config config( + PIXEL_FORMAT_NV12, kInputVisibleSize, VP9PROFILE_PROFILE0, + Bitrate::ConstantBitrate(40000000u), + VideoEncodeAccelerator::kDefaultFramerate, + VideoEncodeAccelerator::Config::StorageType::kGpuMemoryBuffer, + VideoEncodeAccelerator::Config::ContentType::kCamera); + config.inter_layer_pred = SVCInterLayerPredMode::kOnKeyPic; + struct { + int w; + int h; + uint32_t bps; + } const kLayers[] = { + {160, 120, 100000u}, {320, 240, 200000u}, {kHuge, kHuge, 40000000u}}; + for (const auto& l : kLayers) { + VideoEncodeAccelerator::Config::SpatialLayer sl; + sl.width = l.w; + sl.height = l.h; + sl.bitrate_bps = l.bps; + sl.framerate = 30; + sl.max_qp = 56; + sl.num_of_temporal_layers = 1; + config.spatial_layers.push_back(sl); + } + + // Inject a mock rate controller so Initialize() does not depend on libvpx + // accepting the absurd resolutions. + auto rate_ctrl = std::make_unique(); + mock_rate_ctrl_ = rate_ctrl.get(); + encoder_->set_rate_ctrl_for_testing(std::move(rate_ctrl)); + EXPECT_CALL(*mock_rate_ctrl_, UpdateRateControl(_)) + .Times(::testing::AnyNumber()); + EXPECT_CALL(*mock_rate_ctrl_, ComputeQP(_)) + .WillRepeatedly(Return(libvpx::FrameDropDecision::kOk)); + EXPECT_CALL(*mock_rate_ctrl_, GetQP()).WillRepeatedly(Return(34)); + EXPECT_CALL(*mock_rate_ctrl_, GetLoopfilterLevel()).WillRepeatedly(Return(8)); + + // Secure behaviour: Initialize() must reject this config. + // Vulnerable behaviour: it accepts it. + const bool accepted = + encoder_->Initialize(config, kDefaultVaapiVideoEncoderDelegateConfig); + if (!accepted) { + SUCCEED() << "Config rejected at Initialize() (fixed)."; + return; + } + + // The VA encode context and VAEncCodedBuffer are sized from coded_size_, + // which is derived purely from input_visible_size (320x240, 16-aligned). + const gfx::Size coded_size = encoder_->GetCodedSize(); + const size_t coded_buffer_size = encoder_->GetBitstreamBufferSize(); + const std::vector svc = encoder_->GetSVCLayerResolutions(); + ASSERT_EQ(coded_size, gfx::Size(320, 240)); + ASSERT_EQ(coded_buffer_size, size_t{2u * 1024 * 1024}); // 2 MB + ASSERT_EQ(svc.back(), gfx::Size(kHuge, kHuge)); + + // Capture the VA buffers that would be handed to vaRenderPicture(). + VAEncSequenceParameterBufferVP9 seq_param = {}; + VAEncPictureParameterBufferVP9 pic_param = {}; + EXPECT_CALL(*mock_vaapi_wrapper_, SubmitBuffer_Locked(_)) + .WillRepeatedly([&](const VaapiWrapper::VABufferDescriptor& desc) { + if (desc.type == VAEncSequenceParameterBufferType) { + seq_param = *static_cast( + desc.data.get()); + } else if (desc.type == VAEncPictureParameterBufferType) { + pic_param = *static_cast( + desc.data.get()); + } + return true; + }); + + // Encode one keyframe through all three spatial layers; the captured + // pic_param after the loop reflects the top (attacker-sized) layer. + constexpr VASurfaceID kVASurface = 99; + const base::TimeDelta ts = base::Milliseconds(1); + for (uint8_t sid = 0; sid < 3; ++sid) { + auto pic = base::MakeRefCounted( + std::make_unique(kVASurface, base::DoNothing())); + auto job = CreateEncodeJob(/*keyframe=*/sid == 0, sid, + /*end_of_picture=*/sid == 2, ts, kVASurface, + pic); + ASSERT_EQ(encoder_->PrepareEncodeJob(*job), + VaapiVideoEncoderDelegate::PrepareEncodeJobResult::kSuccess); + } + + LOG(ERROR) << "vaCreateContext picture_size = " << coded_size.ToString() + << ", VAEncCodedBuffer size = " << coded_buffer_size << " bytes" + << ", seq_param.max_frame = " << seq_param.max_frame_width << "x" + << seq_param.max_frame_height + << ", pic_param.frame_src = " << pic_param.frame_width_src << "x" + << pic_param.frame_height_src; + + // SECURITY INVARIANT. These assertions FAIL on the vulnerable build, + // demonstrating that the VA-API driver is asked to encode a 16384x16384 + // picture against a 320x256 context with a 2 MB coded buffer. + EXPECT_LE(static_cast(pic_param.frame_width_src), coded_size.width()) + << "VAEncPictureParameterBufferVP9.frame_width_src (" + << pic_param.frame_width_src + << ") exceeds vaCreateContext picture_width (" << coded_size.width() + << "): VA-API contract violation -> driver-heap OOB write in GPU " + "process"; + EXPECT_LE(static_cast(pic_param.frame_height_src), coded_size.height()) + << "VAEncPictureParameterBufferVP9.frame_height_src (" + << pic_param.frame_height_src + << ") exceeds vaCreateContext picture_height (" << coded_size.height() + << "): VA-API contract violation -> driver-heap OOB write in GPU " + "process"; + EXPECT_LE(pic_param.frame_width_src, seq_param.max_frame_width) + << "pic_param.frame_width_src exceeds seq_param.max_frame_width"; + EXPECT_LE(pic_param.frame_height_src, seq_param.max_frame_height) + << "pic_param.frame_height_src exceeds seq_param.max_frame_height"; +} + +TEST_P(VP9VaapiVideoEncoderDelegateTest, + SpatialLayerLargerThanInputVisibleSizeIsRejectedOrBounded) { + // The bug is independent of TEST_P params; run once for L3T1/kOnKeyPic. + if (GetParam().num_spatial_layers != 3 || + GetParam().num_temporal_layers != 1 || + GetParam().inter_layer_pred != SVCInterLayerPredMode::kOnKeyPic) { + GTEST_SKIP(); + } + RunSpatialLayerSizeMismatchPOC(); +} + INSTANTIATE_TEST_SUITE_P( , VP9VaapiVideoEncoderDelegateTest,