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

#include "components/cast_streaming/renderer/frame/demuxer_connector.h"

#include <memory>

#include "base/functional/callback_helpers.h"
#include "base/test/task_environment.h"
#include "base/test/test_simple_task_runner.h"
#include "components/cast_streaming/renderer/frame/frame_injecting_demuxer.h"
#include "media/base/mock_demuxer_host.h"
#include "mojo/public/cpp/bindings/associated_remote.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace cast_streaming {

// Regression / proof-of-concept test demonstrating a use-after-free in
// DemuxerConnector::OnReceiverDisconnected().
//
// Scenario (matches what a Cast Receiver page can trigger):
//   1. Browser binds the renderer-side mojom::DemuxerConnector receiver.
//   2. Page creates <video src="data:cast_streaming_receiver">; the media
//      pipeline creates a FrameInjectingDemuxer and calls Initialize() on the
//      media thread, which posts SetDemuxer(this) back to the main thread.
//   3. SetDemuxer() stores a raw pointer to the demuxer in |demuxer_|.
//   4. Page tears down the <video> element before stream initialization
//      finishes. ~FrameInjectingDemuxer() runs with
//      |was_initialization_successful_| == false, so it skips the
//      OnDemuxerDestroyed() notification, leaving |demuxer_| dangling.
//   5. The Cast session's mojo pipe disconnects. OnReceiverDisconnected()
//      sees a non-null |demuxer_| with |is_demuxer_initialized_| == false and
//      invokes demuxer_->OnStreamsInitialized() on freed memory.
//
// Under ASAN this test produces a heap-use-after-free report inside
// FrameInjectingDemuxer::OnStreamsInitialized().
TEST(DemuxerConnectorUafTest, DanglingDemuxerOnDisconnect) {
  base::test::TaskEnvironment task_environment;

  // Separate sequence for the media thread, as in production.
  auto media_task_runner = base::MakeRefCounted<base::TestSimpleTaskRunner>();

  DemuxerConnector connector;

  // Step 1: bind the receiver (simulates the browser-process AssociatedRemote
  // created by ReceiverSessionImpl/streaming controller).
  mojo::AssociatedRemote<mojom::DemuxerConnector> remote;
  connector.BindReceiver(remote.BindNewEndpointAndPassDedicatedReceiver());
  ASSERT_TRUE(connector.IsBound());

  // Step 2: create the demuxer on the "main" thread and run Initialize() as
  // the media pipeline does. Initialize() posts SetDemuxer(this) back to the
  // main thread via |original_task_runner_|.
  auto demuxer = std::make_unique<FrameInjectingDemuxer>(&connector,
                                                         media_task_runner);
  testing::NiceMock<media::MockDemuxerHost> host;
  static_cast<media::Demuxer*>(demuxer.get())
      ->Initialize(&host, base::DoNothing());

  // Step 3: drain the main thread so DemuxerConnector::SetDemuxer() runs and
  // stores the raw |demuxer_| pointer.
  task_environment.RunUntilIdle();

  // Step 4: destroy the demuxer before initialization completes. The dtor's
  // |was_initialization_successful_| guard skips OnDemuxerDestroyed(), so
  // |connector.demuxer_| now dangles.
  demuxer.reset();
  task_environment.RunUntilIdle();

  // Step 5: simulate Cast session teardown by closing the remote. The receiver
  // disconnect handler runs OnReceiverDisconnected(), which dereferences the
  // dangling |demuxer_| via OnStreamsInitialized().
  remote.reset();
  task_environment.RunUntilIdle();  // ASAN: heap-use-after-free here.
}

}  // namespace cast_streaming
