// 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 <stdint.h>

#include <memory>
#include <vector>

#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<VP9Picture> CreateVP9Picture() override {
    return base::MakeRefCounted<VP9Picture>();
  }

  Status SubmitDecode(scoped_refptr<VP9Picture> pic,
                      const Vp9SegmentationParams& segm_params,
                      const Vp9LoopFilterParams& lf_params,
                      const Vp9ReferenceFrameVector& reference_frames) override {
    return Status::kOk;
  }

  bool OutputPicture(scoped_refptr<VP9Picture> 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<Output>& outputs() const { return outputs_; }

 private:
  std::vector<Output> 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<uint8_t> data;
  data.insert(data.end(), std::begin(kLayerA), std::end(kLayerA));
  data.insert(data.end(), std::begin(kLayerB), std::end(kLayerB));

  scoped_refptr<DecoderBuffer> 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<uint32_t>(sizeof(kLayerA)),
      static_cast<uint32_t>(sizeof(kLayerB))};

  auto accel = std::make_unique<RecordingVP9Accelerator>();
  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
