diff --git a/chromecast/media/cma/BUILD.gn b/chromecast/media/cma/BUILD.gn index 7c19326f38be4..29137ebdda730 100644 --- a/chromecast/media/cma/BUILD.gn +++ b/chromecast/media/cma/BUILD.gn @@ -5,6 +5,27 @@ import("//chromecast/chromecast.gni") import("//testing/test.gni") +# Minimal test target for the FlushBeforeStartBypassesBackendInitialize POC. +# Avoids the heavyweight //content/test:test_support pulled in by +# //chromecast/media:cast_media_unittests so the POC can be built quickly. +test("media_pipeline_impl_poc_unittest") { + sources = [ "pipeline/media_pipeline_impl_unittest.cc" ] + deps = [ + ":test_support", + "//base", + "//base/test:run_all_unittests", + "//base/test:test_support", + "//chromecast/media:libcast_media", + "//chromecast/media/api", + "//chromecast/media/api:test_support", + "//chromecast/media/cma/pipeline", + "//media", + "//testing/gmock", + "//testing/gtest", + "//ui/gfx/geometry", + ] +} + # Targets may depend on this to bring in every CMA component. group("cma") { public_deps = [ diff --git a/chromecast/media/cma/pipeline/media_pipeline_impl_unittest.cc b/chromecast/media/cma/pipeline/media_pipeline_impl_unittest.cc index 88acfe8692b81..d7112c3dae84e 100644 --- a/chromecast/media/cma/pipeline/media_pipeline_impl_unittest.cc +++ b/chromecast/media/cma/pipeline/media_pipeline_impl_unittest.cc @@ -4,20 +4,28 @@ #include "chromecast/media/cma/pipeline/media_pipeline_impl.h" +#include #include +#include #include +#include +#include "base/functional/bind.h" #include "base/functional/callback_helpers.h" #include "base/test/task_environment.h" #include "chromecast/media/api/test/mock_cma_backend.h" +#include "chromecast/media/cma/pipeline/av_pipeline_client.h" #include "chromecast/media/cma/pipeline/load_type.h" #include "chromecast/media/cma/pipeline/video_pipeline_client.h" #include "chromecast/media/cma/test/mock_frame_provider.h" +#include "media/base/audio_decoder_config.h" +#include "media/base/channel_layout.h" #include "media/base/encryption_scheme.h" #include "media/base/video_codecs.h" #include "media/base/video_color_space.h" #include "media/base/video_decoder_config.h" #include "media/base/video_transformation.h" +#include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/gfx/geometry/rect.h" #include "ui/gfx/geometry/size.h" @@ -26,9 +34,137 @@ namespace chromecast { namespace media { namespace { +using ::testing::_; using ::testing::AtLeast; using ::testing::Return; +// ----------------------------------------------------------------------------- +// POC: Flush-before-StartPlayingFrom bypasses lazy backend Initialize(). +// +// A renderer that calls mojom::Renderer::Flush() immediately after a successful +// Initialize() (i.e. before StartPlayingFrom() ever runs) drives +// MediaPipelineImpl from BACKEND_STATE_UNINITIALIZED into Flush(). +// +// Because no demuxer Read is pending yet, the underlying frame-provider Flush +// completes synchronously, so MediaPipelineImpl::OnFlushDone() runs and: +// 1. calls media_pipeline_backend_->Stop() on a backend that was never +// Initialize()d, and +// 2. sets backend_state_ = BACKEND_STATE_INITIALIZED. +// +// A subsequent StartPlayingFrom() then sees BACKEND_STATE_INITIALIZED, *skips* +// the lazy media_pipeline_backend_->Initialize() at line 215, and directly +// calls media_pipeline_backend_->Start(). +// +// On production Chromecast devices the CmaBackend is an out-of-tree OEM shlib +// (CastMediaShlib::CreateMediaPipelineBackend); the public ABI contract in +// chromecast/public/media/media_pipeline_backend.h:358-371 requires +// Initialize() before Start()/Stop(). Violating that contract is undefined +// behaviour in unsandboxed vendor C++ code. +// +// This test PASSES today, which proves the contract is violated: +// EXPECT_CALL(backend, Initialize()).Times(0) <-- never invoked +// EXPECT_CALL(backend, Stop()) <-- invoked +// EXPECT_CALL(backend, Start(_)) <-- invoked +// +// After a fix (e.g. OnFlushDone() preserving BACKEND_STATE_UNINITIALIZED, or +// Flush() being rejected pre-Start), this test should FAIL because +// Initialize() will be called (or Stop()/Start() won't be). +// ----------------------------------------------------------------------------- +TEST(MediaPipelineImplTest, FlushBeforeStartBypassesBackendInitialize_POC) { + base::test::TaskEnvironment task_environment; + + // Tracking backend that records the call sequence so we can print it. + std::vector call_log; + + testing::NiceMock audio_decoder; + ON_CALL(audio_decoder, SetConfig).WillByDefault(Return(true)); + ON_CALL(audio_decoder, PushBuffer) + .WillByDefault(Return(CmaBackend::BufferStatus::kBufferSuccess)); + + auto backend = std::make_unique>(); + MockCmaBackend* backend_ptr = backend.get(); + + EXPECT_CALL(*backend_ptr, CreateAudioDecoder()) + .WillOnce(Return(&audio_decoder)); + + // *** THE BUG ***: Initialize() must never be called on the backend across + // the entire Flush()->StartPlayingFrom() sequence below. If MediaPipelineImpl + // were correct, this expectation would be violated. + EXPECT_CALL(*backend_ptr, Initialize()).Times(0); + + // Stop() is invoked from OnFlushDone() on a never-Initialize()d backend. + EXPECT_CALL(*backend_ptr, Stop()).Times(1).WillOnce([&] { + call_log.push_back("Stop"); + }); + + // Start() is invoked from StartPlayingFrom() with the lazy-init skipped. + EXPECT_CALL(*backend_ptr, Start(_)).Times(1).WillOnce([&](int64_t) { + call_log.push_back("Start"); + return true; + }); + + EXPECT_CALL(*backend_ptr, GetCurrentPts()) + .WillRepeatedly(Return(0)); + + // Use a synchronous-flush frame provider (delay_flush_ defaults to false). + // This mirrors DemuxerStreamAdapter::Flush() when no read is pending: the + // flush callback runs inline, so OnFlushDone() executes during Flush(). + auto frame_provider = std::make_unique(); + + MediaPipelineImpl media_pipeline; + // is_buffering_enabled=false avoids BufferingController, matching the + // "Flush before any data is buffered" attacker scenario and keeping the + // test deterministic. + media_pipeline.Initialize(LoadType::kLoadTypeMediaStream, std::move(backend), + /*is_buffering_enabled=*/false); + + ::media::AudioDecoderConfig audio_config( + ::media::AudioCodec::kMP3, ::media::kSampleFormatS16, + ::media::ChannelLayoutConfig::Stereo(), 44100, + /*extra_data=*/{}, ::media::EncryptionScheme::kUnencrypted); + ::media::PipelineStatus st = media_pipeline.InitializeAudio( + audio_config, AvPipelineClient(), std::move(frame_provider)); + ASSERT_TRUE(st.is_ok()); + // backend_state_ is still BACKEND_STATE_UNINITIALIZED here. + + // ---- Compromised-renderer step 1: Flush() before StartPlayingFrom() ------ + // In a release build, the DCHECK on backend_state_ in Flush() is stripped, so + // this proceeds and OnFlushDone() forces backend_state_ to INITIALIZED. + bool flush_done = false; + media_pipeline.Flush( + base::BindOnce([](bool* d) { *d = true; }, &flush_done)); + task_environment.RunUntilIdle(); + ASSERT_TRUE(flush_done) << "Flush did not complete"; + + // ---- Compromised-renderer step 2: StartPlayingFrom() -------------------- + // Lazy-init is skipped because backend_state_ == BACKEND_STATE_INITIALIZED; + // backend->Start() is called directly on the never-Initialize()d backend. + media_pipeline.StartPlayingFrom(base::Seconds(0)); + + // Verify the contract-violating sequence: Stop, Start — and no Initialize. + ASSERT_EQ(call_log.size(), 2u) + << "Expected exactly Stop+Start; got " << call_log.size() << " calls"; + EXPECT_EQ(call_log[0], "Stop") + << "Backend Stop() should have been called by OnFlushDone() on a backend " + "that was never Initialize()d"; + EXPECT_EQ(call_log[1], "Start") + << "Backend Start() should have been called by StartPlayingFrom() with " + "the lazy Initialize() bypassed"; + + fprintf(stderr, + "\n[POC] CmaBackend call sequence (Initialize() never called):\n"); + for (size_t i = 0; i < call_log.size(); ++i) { + fprintf(stderr, "[POC] %zu. %s()\n", i + 1, call_log[i].c_str()); + } + fprintf(stderr, + "[POC] => MediaPipelineBackend ABI contract violated: " + "Stop()/Start() invoked without prior Initialize().\n\n"); + + // Do NOT RunUntilIdle() here: StartPlayingFrom() posted a FetchBuffer task + // that would call into the unconfigured MockFrameProvider. The proof is + // already complete; the TaskEnvironment dtor drops pending tasks. +} + TEST(MediaPipelineImplTest, DoesNotCrashOnFlushWhenBufferingIsDisabled) { base::test::TaskEnvironment task_environment;