diff --git a/components/cast_streaming/browser/control/remoting/rpc_demuxer_stream_handler_unittest.cc b/components/cast_streaming/browser/control/remoting/rpc_demuxer_stream_handler_unittest.cc index faabb5212ec86..c68e5c8370067 100644 --- a/components/cast_streaming/browser/control/remoting/rpc_demuxer_stream_handler_unittest.cc +++ b/components/cast_streaming/browser/control/remoting/rpc_demuxer_stream_handler_unittest.cc @@ -465,4 +465,121 @@ TEST_F(RpcDemuxerStreamHandlerTest, OnEnableVideoBitstreamConverter) { stream_handler_.OnRpcBitstreamConverterEnabled(video_local_handle_, false); } +// ----------------------------------------------------------------------------- +// Regression / security POC test: +// +// Models PlaybackCommandDispatcher's ownership of RpcDemuxerStreamHandler. +// In production (playback_command_dispatcher.cc:134-140) the handler is +// constructed with: +// process_message_cb_ = base::BindRepeating( +// &PlaybackCommandDispatcher::SendRemotingRpcMessageToRemote, +// base::Unretained(this)); +// and ~PlaybackCommandDispatcher() only resets the handler; it does NOT cancel +// tasks already posted to the (longer-lived) sequence task runner. +// +// MessageProcessor::OnNoBuffersAvailable() (rpc_demuxer_stream_handler.cc:308) +// posts that Unretained callback via PostDelayedTask with up to 100ms delay. +// If the dispatcher is destroyed in that window, the delayed task invokes a +// member function on freed memory. +class FakeDispatcher : public RpcDemuxerStreamHandler::Client { + public: + explicit FakeDispatcher(scoped_refptr task_runner) + : handler_(std::make_unique( + std::move(task_runner), + this, + base::BindRepeating(&FakeDispatcher::AcquireHandle, + base::Unretained(this)), + // Mirrors playback_command_dispatcher.cc:138-140. + base::BindRepeating(&FakeDispatcher::SendMessage, + base::Unretained(this)))) {} + + ~FakeDispatcher() override { + // Mirrors ~PlaybackCommandDispatcher -> OnRemotingSessionEnded(): + // resets the handler but does NOT cancel posted tasks. + handler_.reset(); + } + + media::cast::RpcDemuxerStreamCBMessageHandler* handler() { + return handler_.get(); + } + RpcDemuxerStreamHandler* raw_handler() { return handler_.get(); } + + openscreen::cast::RpcMessenger::Handle video_local_handle() const { + return video_local_handle_; + } + + // RpcDemuxerStreamHandler::Client: + void OnNewAudioConfig(media::AudioDecoderConfig) override {} + void OnNewVideoConfig(media::VideoDecoderConfig) override {} + + private: + openscreen::cast::RpcMessenger::Handle AcquireHandle() { + auto h = next_handle_++; + if (h == 102) { + video_local_handle_ = h; + } + return h; + } + + // Mirrors PlaybackCommandDispatcher::SendRemotingRpcMessageToRemote: touches + // |this| (in production it reads messenger_). When invoked on freed memory + // ASAN reports heap-use-after-free. + void SendMessage(openscreen::cast::RpcMessenger::Handle handle, + std::unique_ptr msg) { + last_handle_ = handle; + } + + std::unique_ptr handler_; + openscreen::cast::RpcMessenger::Handle next_handle_ = 101; + openscreen::cast::RpcMessenger::Handle video_local_handle_ = -1; + openscreen::cast::RpcMessenger::Handle last_handle_ = -1; +}; + +TEST(RpcDemuxerStreamHandlerUAFTest, + DelayedReadUntilTaskRunsAfterOwnerDestroyed) { + base::test::SingleThreadTaskEnvironment task_environment{ + base::test::TaskEnvironment::TimeSource::MOCK_TIME}; + + auto dispatcher = std::make_unique( + task_environment.GetMainThreadTaskRunner()); + + // Step 1 (RPC_ACQUIRE_DEMUXER): create audio (local handle 101) and video + // (local handle 102) MessageProcessors. + dispatcher->raw_handler()->OnRpcAcquireDemuxer( + /*audio_stream_handle=*/50, + /*video_stream_handle=*/51); + + const media::AudioDecoderConfig audio_config( + media::AudioCodec::kAAC, media::SampleFormat::kSampleFormatF32, + media::ChannelLayoutConfig::Mono(), 48000, media::EmptyExtraData(), + media::EncryptionScheme::kUnencrypted); + + // Step 2: First RPC_DS_READUNTIL_CALLBACK addressed to the *video* processor + // carrying an *audio* config (status=kConfigChanged equivalent). + // OnRpcReadUntilCallback clears is_read_until_call_ongoing_, then + // OnRpcInitializeCallback returns false (audio_config && type_!=kAudio) -> + // OnNoBuffersAvailable() -> PostDelayedTask(Unretained cb, delay=0), + // last_request_time_ = now. + dispatcher->handler()->OnRpcReadUntilCallback( + dispatcher->video_local_handle(), audio_config, std::nullopt, 0u); + + // Step 3: Second identical message, back-to-back (no time advance). + // is_read_until_call_ongoing_ was cleared again at line 227, so + // OnNoBuffersAvailable() runs again with remaining_time ~= 100ms -> + // PostDelayedTask(Unretained cb, ~100ms). This is the dangerous task. + dispatcher->handler()->OnRpcReadUntilCallback( + dispatcher->video_local_handle(), audio_config, std::nullopt, 0u); + + // Step 4: Attacker closes the Cast control channel -> + // ~PlaybackCommandDispatcher(). The handler is destroyed, but both posted + // tasks (delay=0 and delay~=100ms) remain queued on the task runner with an + // Unretained pointer to the now-freed dispatcher. + dispatcher.reset(); + + // Step 5: Advance time. The queued tasks fire and call + // FakeDispatcher::SendMessage on freed |this|. + // EXPECTED: ASAN heap-use-after-free in FakeDispatcher::SendMessage. + task_environment.FastForwardBy(base::Milliseconds(200)); +} + } // namespace cast_streaming::remoting