diff --git a/media/base/platform_features.cc b/media/base/platform_features.cc index 936063e26f5087..66131437184057 100644 --- a/media/base/platform_features.cc +++ b/media/base/platform_features.cc @@ -11,18 +11,11 @@ namespace media { bool IsVp9kSVCHWDecodingEnabled() { -#if BUILDFLAG(IS_CHROMEOS) - // arm: Support is API and driver dependent: - // - V4L2 state*less* API decoder is not capable of decoding VP9 kSVC stream. - // - V4L2 state*ful* API decoder is capable of decoding, but is driver - // dependent. x86: Always supported. + // POC: Force-enable to simulate ChromeOS x86 production behaviour, where + // this returns true unconditionally. This is a *platform availability* gate, + // not a security gate; on the affected platform the SVC path is reachable by + // default from a compromised renderer. return true; -#elif BUILDFLAG(IS_WIN) - // TODO(crbug.com/40286220): Experiment to enable on Windows. - return base::FeatureList::IsEnabled(media::kD3D11Vp9kSVCHWDecoding); -#else - return false; -#endif } } // namespace media diff --git a/media/gpu/BUILD.gn b/media/gpu/BUILD.gn index a6a1e975ea4fd9..34af3f26a2465a 100644 --- a/media/gpu/BUILD.gn +++ b/media/gpu/BUILD.gn @@ -641,6 +641,7 @@ source_set("unit_tests") { "exponential_moving_average_unittest.cc", "frame_size_estimator_unittest.cc", "h264_decoder_unittest.cc", + "vp9_decoder_svc_unittest.cc", "h264_rate_control_util_unittest.cc", "h264_rate_controller_unittest.cc", "h264_ratectrl_rtc_unittest.cc", diff --git a/media/gpu/vp9_decoder_svc_unittest.cc b/media/gpu/vp9_decoder_svc_unittest.cc new file mode 100644 index 00000000000000..fa5e2b088caeb4 --- /dev/null +++ b/media/gpu/vp9_decoder_svc_unittest.cc @@ -0,0 +1,145 @@ +// Copyright 2025 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Regression / security test for VP9 k-SVC render_rect clamping. +// +// When a DecoderBuffer carries side_data->spatial_layers (k-SVC), +// Vp9Parser::ParseSVCFrame() stamps the *maximum* layer frame size onto every +// layer's allocate_size. VP9Decoder::Decode() then uses that allocate_size as +// the bound for the render-size safety clamp instead of the per-frame +// {frame_width, frame_height}. A small layer (64x64) whose bitstream +// render_width/height is 256x256 (<= max layer size) therefore passes the +// clamp unmodified, and OutputPicture() is called with visible_rect = 256x256 +// for a frame whose decoded region is only 64x64. +// +// On ChromeOS x86 VAAPI this exposes uninitialized / stale GPU surface pixels +// to the renderer (CWE-908). + +#include + +#include +#include + +#include "base/check_op.h" +#include "base/logging.h" +#include "base/memory/scoped_refptr.h" +#include "media/base/decoder_buffer.h" +#include "media/base/video_codecs.h" +#include "media/gpu/vp9_decoder.h" +#include "media/gpu/vp9_picture.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace media { +namespace { + +// Layer A: VP9 profile-0 keyframe, frame_size=256x256, render_size=frame_size. +constexpr uint8_t kLayerA[] = { + 0x83, 0x49, 0x83, 0x42, 0x00, 0x0f, 0xf0, 0x0f, + 0xf0, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, +}; + +// Layer B: VP9 profile-0 keyframe, frame_size=64x64, render_size=256x256. +constexpr uint8_t kLayerB[] = { + 0x83, 0x49, 0x83, 0x42, 0x00, 0x03, 0xf0, 0x03, 0xf8, 0x07, + 0xf8, 0x07, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, +}; + +// Mock accelerator that records every OutputPicture() call so the test can +// verify the security invariant: visible_rect must lie within the per-frame +// decoded region (frame_width x frame_height). +class RecordingVP9Accelerator : public VP9Decoder::VP9Accelerator { + public: + struct Output { + gfx::Rect visible_rect; + uint32_t frame_width = 0; + uint32_t frame_height = 0; + }; + + RecordingVP9Accelerator() = default; + ~RecordingVP9Accelerator() override = default; + + scoped_refptr CreateVP9Picture() override { + return base::MakeRefCounted(); + } + + Status SubmitDecode(scoped_refptr pic, + const Vp9SegmentationParams& segm_params, + const Vp9LoopFilterParams& lf_params, + const Vp9ReferenceFrameVector& reference_frames) override { + return Status::kOk; + } + + bool OutputPicture(scoped_refptr pic) override { + Output out; + out.visible_rect = pic->visible_rect(); + out.frame_width = pic->frame_hdr->frame_width; + out.frame_height = pic->frame_hdr->frame_height; + outputs_.push_back(out); + LOG(ERROR) << "[POC] OutputPicture: visible_rect=" + << out.visible_rect.ToString() + << " frame=" << out.frame_width << "x" << out.frame_height; + return true; + } + + const std::vector& outputs() const { return outputs_; } + + private: + std::vector outputs_; +}; + +TEST(VP9DecoderSVCTest, RenderRectClampedToFrameSizeNotMaxLayerSize) { + // Concatenate the two SVC sub-frames into one buffer, exactly as a + // compromised renderer would send via media.mojom.VideoDecoder::Decode. + std::vector data; + data.insert(data.end(), std::begin(kLayerA), std::end(kLayerA)); + data.insert(data.end(), std::begin(kLayerB), std::end(kLayerB)); + + scoped_refptr buffer = DecoderBuffer::CopyFrom(data); + // Renderer-controlled side data: spatial_layers = [size_A, size_B]. + // (Only validation in production is size() <= 3.) + buffer->WritableSideData().spatial_layers = { + static_cast(sizeof(kLayerA)), + static_cast(sizeof(kLayerB))}; + + auto accel = std::make_unique(); + RecordingVP9Accelerator* accel_raw = accel.get(); + VP9Decoder decoder(std::move(accel), VP9PROFILE_PROFILE0); + + decoder.SetStream(/*id=*/1, buffer); + + // First Decode() returns kConfigChange for the 256x256 allocation. + AcceleratedVideoDecoder::DecodeResult result = decoder.Decode(); + ASSERT_EQ(result, AcceleratedVideoDecoder::kConfigChange) + << "Expected kConfigChange for first SVC layer; got " << result + << ". On non-ChromeOS builds this fails because " + "IsVp9kSVCHWDecodingEnabled() returns false (platform gate, not a " + "security gate). The accompanying patch enables the SVC path on Linux " + "to demonstrate the bug."; + EXPECT_EQ(decoder.GetPicSize(), gfx::Size(256, 256)); + + // Resume: layer A decoded (show_frame=false), layer B decoded+output. + result = decoder.Decode(); + ASSERT_EQ(result, AcceleratedVideoDecoder::kRanOutOfStreamData); + + // Exactly one output (layer B is last -> show_frame forced true). + ASSERT_EQ(accel_raw->outputs().size(), 1u); + const auto& out = accel_raw->outputs()[0]; + + EXPECT_EQ(out.frame_width, 64u); + EXPECT_EQ(out.frame_height, 64u); + + // SECURITY INVARIANT: visible_rect must not exceed the per-frame decoded + // region. On ChromeOS VAAPI, the driver fills only frame_width x + // frame_height pixels into a pic_size_-sized surface; anything outside that + // region is uninitialized / stale GPU memory. If this EXPECT fails, a + // compromised renderer can read those pixels back via WebCodecs / WebGL. + const gfx::Rect decoded_region(out.frame_width, out.frame_height); + EXPECT_TRUE(decoded_region.Contains(out.visible_rect)) + << "BUG: visible_rect=" << out.visible_rect.ToString() + << " exceeds decoded region " << decoded_region.ToString() + << " -- undecoded GPU surface pixels exposed to renderer"; +} + +} // namespace +} // namespace media