diff --git a/media/gpu/vaapi/BUILD.gn b/media/gpu/vaapi/BUILD.gn index 98b03e5864b6b..f71737491e799 100644 --- a/media/gpu/vaapi/BUILD.gn +++ b/media/gpu/vaapi/BUILD.gn @@ -184,6 +184,7 @@ source_set("vaapi_test_utils") { source_set("unit_test") { testonly = true sources = [ + "av1_vaapi_video_encoder_delegate_unittest.cc", "h264_vaapi_video_encoder_delegate_unittest.cc", "vaapi_video_encode_accelerator_unittest.cc", "vaapi_wrapper_unittest.cc", diff --git a/media/gpu/vaapi/av1_vaapi_video_encoder_delegate.h b/media/gpu/vaapi/av1_vaapi_video_encoder_delegate.h index c1eaf2cbe9e3d..947b4701399cd 100644 --- a/media/gpu/vaapi/av1_vaapi_video_encoder_delegate.h +++ b/media/gpu/vaapi/av1_vaapi_video_encoder_delegate.h @@ -67,6 +67,8 @@ class AV1VaapiVideoEncoderDelegate : public VaapiVideoEncoderDelegate { std::vector GetSVCLayerResolutions() override; private: + friend class AV1VaapiVideoEncoderDelegateTest; + BitstreamBufferMetadata GetMetadata(const EncodeJob& encode_job, size_t payload_size) override; PrepareEncodeJobResult PrepareEncodeJob(EncodeJob& encode_job) override; diff --git a/media/gpu/vaapi/av1_vaapi_video_encoder_delegate_unittest.cc b/media/gpu/vaapi/av1_vaapi_video_encoder_delegate_unittest.cc new file mode 100644 index 0000000000000..32c56a5b7dd9b --- /dev/null +++ b/media/gpu/vaapi/av1_vaapi_video_encoder_delegate_unittest.cc @@ -0,0 +1,186 @@ +// Copyright 2024 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// POC unit test demonstrating a heap-buffer-overflow WRITE in +// AV1RateControlRTC::ComputeQP / av1_save_layer_context, reachable from a +// renderer-controlled VideoBitrateAllocation that desynchronizes +// |num_temporal_layers_| (passed to libaom) from |svc_layers_| (which produces +// per-frame temporal_layer_id). +// +// In production this is reachable on ChromeOS / Linux+VAAPI with AV1 HW encode +// via media.mojom.VideoEncodeAccelerator. This unit test exercises the exact +// AV1VaapiVideoEncoderDelegate code path with a mocked VaapiWrapper so it +// runs on machines without AV1 HW encode support. + +#include "media/gpu/vaapi/av1_vaapi_video_encoder_delegate.h" + +#include + +#include + +#include "base/functional/callback_helpers.h" +#include "base/memory/scoped_refptr.h" +#include "media/gpu/svc_layers.h" +#include "media/gpu/vaapi/vaapi_common.h" +#include "media/gpu/vaapi/vaapi_utils.h" +#include "media/gpu/vaapi/vaapi_wrapper.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::_; +using ::testing::Return; + +namespace media { +namespace { + +class MockVaapiWrapper : public VaapiWrapper { + public: + MockVaapiWrapper() + : VaapiWrapper(VADisplayStateHandle(), + kEncodeConstantQuantizationParameter) {} + + MOCK_METHOD1(SubmitBuffer_Locked, bool(const VABufferDescriptor&)); + // Avoid calling into libva (no driver available in this environment). + bool GetMinAV1SegmentSize(VideoCodecProfile, uint32_t& s) override { + s = 8; + return true; + } + + protected: + ~MockVaapiWrapper() override = default; +}; + +VaapiVideoEncoderDelegate::Config kAveConfig{.max_num_ref_frames = 8}; + +} // namespace + +class AV1VaapiVideoEncoderDelegateTest : public ::testing::Test { + public: + void SetUp() override { + mock_vaapi_wrapper_ = base::MakeRefCounted(); + ON_CALL(*mock_vaapi_wrapper_, SubmitBuffer_Locked(_)) + .WillByDefault(Return(true)); + encoder_ = std::make_unique( + mock_vaapi_wrapper_, base::DoNothing()); + } + + protected: + std::unique_ptr CreateEncodeJob( + bool keyframe) { + const VASurfaceID recon_id = next_surface_id_++; + const VASurfaceID display_id = next_surface_id_++; + const VASurfaceID input_id = next_surface_id_++; + auto picture = base::MakeRefCounted( + std::make_unique(recon_id, base::DoNothing()), + std::make_unique(display_id, base::DoNothing())); + constexpr VABufferID kDummyVABufferID = 12; + auto coded_buffer = ScopedVABuffer::CreateForTesting( + kDummyVABufferID, VAEncCodedBufferType, 320 * 240); + return std::make_unique( + keyframe, base::Milliseconds(input_id), + /*spatial_index=*/0u, + /*end_of_picture=*/true, input_id, picture, std::move(coded_buffer)); + } + + // Friend-access wrappers (TEST_F body is a derived class, not this class). + bool PrepareEncodeJob(VaapiVideoEncoderDelegate::EncodeJob& job) { + return encoder_->PrepareEncodeJob(job) == + VaapiVideoEncoderDelegate::PrepareEncodeJobResult::kSuccess; + } + void BitrateControlUpdate(const BitstreamBufferMetadata& md) { + encoder_->BitrateControlUpdate(md); + } + + std::unique_ptr encoder_; + scoped_refptr mock_vaapi_wrapper_; + unsigned int next_surface_id_ = 1; +}; + +// Reproduces the renderer-reachable temporal-layer desync: +// +// 1. Initialize() with one spatial layer / two temporal layers. +// -> num_temporal_layers_ = 2 +// -> svc_layers_ created with T=2 +// -> rate_ctrl_ = AV1RateControlRTC::Create({ts_number_layers = 2}) +// -> aom_calloc(2, sizeof(LAYER_CONTEXT)) +// +// 2. UpdateRates() with a VideoBitrateAllocation that has THREE non-zero +// temporal layers (this is exactly what the renderer can supply via +// RequestEncodingParametersChangeWithLayers; no validation rejects it). +// -> rc_config.ts_number_layers = num_temporal_layers_ = 2 (UNCHANGED) +// -> rate_ctrl_->UpdateRateControl() (no realloc) +// -> svc_layers_->RecreateSVCLayersIfNeeded() -> NEW svc_layers_, T=3 +// +// 3. PrepareEncodeJob() x2. +// Frame 0 -> keyframe (svc_layers_->frame_num_ == 0), temporal_idx = 0. +// Frame 1 -> GetFrameConfig(3, 1).layer_index() == 2, temporal_idx = 2. +// ComputeQP({temporal_layer_id = 2}) with +// number_temporal_layers == 2: +// layer = LAYER_IDS_TO_IDX(0, 2, 2) = 2 +// cpi_->svc.layer_context[2].is_key_frame = 0; <-- OOB +// +// Under ASAN this fires heap-buffer-overflow WRITE in +// aom::AV1RateControlRTC::ComputeQP. Under PostEncodeUpdate(), the +// av1_save_layer_context() path performs an additional ~13 KB OOB struct-copy +// write (lc->rc / lc->p_rc) into the same out-of-bounds slot. +TEST_F(AV1VaapiVideoEncoderDelegateTest, TemporalLayerDesyncOobWrite) { + // Step 1: Initialize with 2 temporal layers (renderer-supplied config). + VideoEncodeAccelerator::Config config( + PIXEL_FORMAT_I420, gfx::Size(320, 240), AV1PROFILE_PROFILE_MAIN, + Bitrate::ConstantBitrate(1000000u), + VideoEncodeAccelerator::kDefaultFramerate, + VideoEncodeAccelerator::Config::StorageType::kShmem, + VideoEncodeAccelerator::Config::ContentType::kCamera); + config.inter_layer_pred = SVCInterLayerPredMode::kOff; + config.spatial_layers.resize(1); + config.spatial_layers[0].width = 320; + config.spatial_layers[0].height = 240; + config.spatial_layers[0].bitrate_bps = 1000000u; + config.spatial_layers[0].framerate = 30; + config.spatial_layers[0].max_qp = 56; + config.spatial_layers[0].num_of_temporal_layers = 2; + ASSERT_TRUE(encoder_->Initialize(config, kAveConfig)); + + // libaom now has layer_context[] sized for exactly 2 entries. + // svc_layers_ is configured for 2 temporal layers. + + // Step 2: Renderer sends RequestEncodingParametersChangeWithLayers with + // three non-zero temporal-layer bitrates. There is no validation in the + // mojom traits, MojoVideoEncodeAcceleratorService, or + // VaapiVideoEncodeAccelerator that rejects this — it reaches UpdateRates() + // unchanged. + VideoBitrateAllocation alloc; + alloc.SetBitrate(/*spatial_index=*/0, /*temporal_index=*/0, 300000u); + alloc.SetBitrate(0, 1, 300000u); + alloc.SetBitrate(0, 2, 300000u); + ASSERT_TRUE(encoder_->UpdateRates(alloc, /*framerate=*/30)); + + // Now: rate_ctrl_ still believes ts_number_layers == 2 (allocation + // unchanged), but svc_layers_ was recreated with num_temporal_layers == 3. + + // Step 3: Encode two frames. + // Frame 0: keyframe, temporal_idx == 0 (in bounds). PostEncode() advances + // svc_layers_->frame_num_ to 1. + { + auto job = CreateEncodeJob(/*keyframe=*/false); + ASSERT_TRUE(PrepareEncodeJob(*job)); + } + + // Frame 1: TL3 pattern frame_num_=1 -> temporal_idx == 2. + // ComputeQP indexes layer_context[2] in a 2-element calloc(). + // ASAN: heap-buffer-overflow WRITE. + { + auto job = CreateEncodeJob(/*keyframe=*/false); + PrepareEncodeJob(*job); // OOB write inside ComputeQP(). + + // If the first OOB int write does not trip ASAN (e.g., redzone layout), + // PostEncodeUpdate() -> av1_save_layer_context() performs the ~13 KB + // struct-copy OOB write, which definitely will. + BitstreamBufferMetadata md; + md.payload_size_bytes = 1000; + BitrateControlUpdate(md); + } +} + +} // namespace media diff --git a/media/gpu/vaapi/vaapi_video_encoder_delegate.h b/media/gpu/vaapi/vaapi_video_encoder_delegate.h index 7c85b738c88cc..9b9dc40149603 100644 --- a/media/gpu/vaapi/vaapi_video_encoder_delegate.h +++ b/media/gpu/vaapi/vaapi_video_encoder_delegate.h @@ -187,6 +187,7 @@ class VaapiVideoEncoderDelegate { // Friend in order o access PrepareEncodeJobResult declaration. friend class H264VaapiVideoEncoderDelegateTest; friend class VP9VaapiVideoEncoderDelegateTest; + friend class AV1VaapiVideoEncoderDelegateTest; friend class VaapiVideoEncodeAcceleratorTest; enum class PrepareEncodeJobResult { diff --git a/media/gpu/vaapi/vaapi_wrapper.h b/media/gpu/vaapi/vaapi_wrapper.h index cd1be5d824baa..c2e8642b6f271 100644 --- a/media/gpu/vaapi/vaapi_wrapper.h +++ b/media/gpu/vaapi/vaapi_wrapper.h @@ -573,8 +573,8 @@ class MEDIA_GPU_EXPORT VaapiWrapper bool& packed_raw); // Gets the minimum segment block size supported for AV1 encoding. - [[nodiscard]] bool GetMinAV1SegmentSize(VideoCodecProfile profile, - uint32_t& min_seg_size); + [[nodiscard]] virtual bool GetMinAV1SegmentSize(VideoCodecProfile profile, + uint32_t& min_seg_size); // Blits |va_surface_src_id| into |va_surface_dst_id| applying pixel format // conversion, cropping and scaling if needed. |src_rect| and |dest_rect| are