CVE-2026-106262
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcontent/browser/renderer_host/media/media_stream_manager_unittest.cc |
modified | |
BindLambdaForTestingcontent/browser/renderer_host/media/media_stream_manager_unittest.cc |
modified | |
EXPECT_TRUEcontent/browser/renderer_host/media/media_stream_manager_unittest.cc |
modified |
Files Changed
content/browser/renderer_host/media/media_stream_manager.cccontent/browser/renderer_host/media/media_stream_manager_unittest.cccontent/browser/renderer_host/media/media_stream_manager.h
Patch
From 5afc76c15fb08e5721e7121bb277b4e3c0ebbed3 Mon Sep 17 00:00:00 2001 From: Tove Petersson <[email protected]> Date: Fri, 04 Sep 2026 02:40:19 -0700 Subject: [PATCH] media: Close capture session on teardown during source change When changing a tab-capture source via the browser UI (such as clicking "Share this tab instead" on a tab-sharing infobar), SetUpDesktopCaptureChangeSourceRequest() reuses the live DeviceRequest and PostRequestToUI() transitions its per-type state to PENDING_APPROVAL while the source-change request is dispatched to and processed on the UI thread (where it may also be queued behind pending access requests for the WebContents). If the stream is cancelled or stopped (e.g. via navigation or renderer- initiated track stop) before the source change completes, CancelRequest() and StopDevice() skip CloseDevice() because they only close for OPENING/DONE, leaving the VideoCaptureManager session for the currently shared surface open even though the DeviceRequest and its MediaStreamUIProxy are torn down (removing all sharing indicators). Treat PENDING_APPROVAL on a MEDIA_DEVICE_UPDATE request as an already-open device in both teardown paths so CloseDevice() runs as usual. MEDIA_DEVICE_UPDATE is only ever set on a request whose devices were previously granted and opened, so this cannot close a device that was never opened. TAG=agy CONV=d2ae4709-47f0-4231-a7d9-7b71f7f39ed7 Bug: 533066295 Change-Id: I3aa55afca5f69bcdd67a95d553cdfd6bafc2a035 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8351467 Commit-Queue: Tove Petersson <[email protected]> Reviewed-by: Guido Urdaneta <[email protected]> Cr-Commit-Position: refs/heads/main@{#1692302} --- diff --git a/content/browser/renderer_host/media/media_stream_manager.cc b/content/browser/renderer_host/media/media_stream_manager.cc index 70a74fe..6a041974 100644 --- a/content/browser/renderer_host/media/media_stream_manager.cc +++ b/content/browser/renderer_host/media/media_stream_manager.cc @@ -718,6 +718,14 @@ return state_[static_cast<int>(stream_type)]; } + bool ShouldCloseDeviceOnTeardown(MediaStreamType stream_type) const { + const MediaRequestState request_state = state(stream_type); + return request_state == MEDIA_REQUEST_STATE_OPENING || + request_state == MEDIA_REQUEST_STATE_DONE || + (request_type_ == blink::MEDIA_DEVICE_UPDATE && + request_state == MEDIA_REQUEST_STATE_PENDING_APPROVAL); + } + void ResetDevicesOpened(MediaStreamType stream_type) { devices_opened_count_[static_cast<int>(stream_type)] = 0; } @@ -2039,7 +2047,7 @@ if (devices.audio_device.has_value() && devices.audio_device->type == type && devices.audio_device->session_id() == session_id) { - if (request->state(type) == MEDIA_REQUEST_STATE_DONE) { + if (request->ShouldCloseDeviceOnTeardown(type)) { CloseDevice(type, session_id); } devices.audio_device = std::nullopt; @@ -2047,7 +2055,7 @@ if (devices.video_device.has_value() && devices.video_device->type == type && devices.video_device->session_id() == session_id) { - if (request->state(type) == MEDIA_REQUEST_STATE_DONE) { + if (request->ShouldCloseDeviceOnTeardown(type)) { CloseDevice(type, session_id); } devices.video_device = std::nullopt; @@ -2574,10 +2582,8 @@ continue; } const blink::MediaStreamDevice& device = device_ptr->value(); - const MediaRequestState state = request->state(device.type); // If we have not yet requested the device to be opened - just ignore it. - if (state != MEDIA_REQUEST_STATE_OPENING && - state != MEDIA_REQUEST_STATE_DONE) { + if (!request->ShouldCloseDeviceOnTeardown(device.type)) { continue; } // Stop the opening/opened devices of the requests. diff --git a/content/browser/renderer_host/media/media_stream_manager.h b/content/browser/renderer_host/media/media_stream_manager.h index 7d016be..6c2cc4dd 100644 --- a/content/browser/renderer_host/media/media_stream_manager.h +++ b/content/browser/renderer_host/media/media_stream_manager.h @@ -519,6 +519,10 @@ FRIEND_TEST_ALL_PREFIXES(MediaStreamManagerTest, DesktopCaptureDeviceChangeDeniedThenCancel); FRIEND_TEST_ALL_PREFIXES(MediaStreamManagerTest, + DesktopCaptureCancelDuringPendingChangeSource); + FRIEND_TEST_ALL_PREFIXES(MediaStreamManagerTest, + DesktopCaptureStopDeviceDuringPendingChangeSource); + FRIEND_TEST_ALL_PREFIXES(MediaStreamManagerTest, MultiCaptureOnMediaStreamUIWindowId); FRIEND_TEST_ALL_PREFIXES(MediaStreamManagerTest, MultiCaptureAllDevicesOpened); diff --git a/content/browser/renderer_host/media/media_stream_manager_unittest.cc b/content/browser/renderer_host/media/media_stream_manager_unittest.cc index aa17c9e..46b3293 100644 --- a/content/browser/renderer_host/media/media_stream_manager_unittest.cc +++ b/content/browser/renderer_host/media/media_stream_manager_unittest.cc @@ -23,6 +23,7 @@ #include "base/task/single_thread_task_runner.h" #include "base/test/bind.h" #include "base/test/mock_callback.h" +#include "base/test/run_until.h" #include "base/test/scoped_command_line.h" #include "base/test/scoped_feature_list.h" #include "base/test/test_future.h" @@ -1635,6 +1636,153 @@ session_id, &formats)); } +TEST_F(MediaStreamManagerTest, DesktopCaptureCancelDuringPendingChangeSource) { + const std::string tab_id = + DesktopMediaID(DesktopMediaID::TYPE_WEB_CONTENTS, /*id=*/0, + WebContentsMediaCaptureId(5, 5)) + .ToString(); + media_stream_manager_->UseFakeUIFactoryForTests( + base::BindLambdaForTesting([&]() { + auto fake_ui = std::make_unique<FakeMediaStreamUIProxy>( + /*tests_use_fake_render_frame_hosts=*/true); + fake_ui->AddAvailableDevices({blink::MediaStreamDevice( + blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE, tab_id, + "Tab")}); + return std::unique_ptr<FakeMediaStreamUIProxy>(std::move(fake_ui)); + })); + + blink::StreamControls controls(false /* request_audio */, + true /* request_video */); + controls.video.stream_type = + blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE; + const int requester_id = 1; + const int page_request_id = 1; + + blink::MediaStreamDevice video_device; + MediaStreamManager::GenerateStreamsCallback generate_stream_callback = + base::BindOnce(GenerateStreamsCallback, &run_loop_, + /*request_audio=*/false, + /*request_video=*/true, /*audio_device=*/nullptr, + &video_device, + /*audio_share=*/true); + EXPECT_CALL(*media_observer_, OnMediaRequestStateChanged(_, _, _, _, _, _)) + .Times(testing::AtLeast(1)); + + media_stream_manager_->GenerateStreams( + kRenderFrameHostId, requester_id, page_request_id, controls, + MediaDeviceSaltAndOrigin::Empty(), false /* user_gesture */, + StreamSelectionInfo::NewSearchOnlyByDeviceId({}), + std::move(generate_stream_callback), + MediaStreamManager::DeviceStoppedCallback(), + MediaStreamManager::DeviceChangedCallback(), + MediaStreamManager::DeviceRequestStateChangeCallback(), + MediaStreamManager::DeviceCaptureConfigurationChangeCallback(), + MediaStreamManager::DeviceCaptureHandleChangeCallback(), + MediaStreamManager::ZoomLevelChangeCallback()); + run_loop_.Run(); + EXPECT_EQ(controls.video.stream_type, video_device.type); + + const std::string request_label = GetLatestLabel(); + const base::UnguessableToken session_id = video_device.session_id(); + media::VideoCaptureFormats formats; + ASSERT_TRUE( + media_stream_manager_->video_capture_manager()->GetDeviceSupportedFormats( + session_id, &formats)); + + // Request a source change. The request moves to MEDIA_DEVICE_UPDATE and its + // state becomes PENDING_APPROVAL while the source-change request is + // outstanding on the UI thread. + media_stream_manager_->ChangeMediaStreamSourceFromBrowser( + request_label, DesktopMediaID(), + /*captured_surface_control_active=*/false); + + // Cancelling the request while change-source is pending must close the + // underlying capture session. + media_stream_manager_->CancelRequest(request_label); + EXPECT_TRUE( + media_stream_manager_->GetDevicesOpenedByRequest(request_label).empty()); + EXPECT_TRUE(base::test::RunUntil([&]() { + formats.clear(); + return !media_stream_manager_->video_capture_manager() + ->GetDeviceSupportedFormats(session_id, &formats); + })); +} + +TEST_F(MediaStreamManagerTest, + DesktopCaptureStopDeviceDuringPendingChangeSource) { + const std::string tab_id = + DesktopMediaID(DesktopMediaID::TYPE_WEB_CONTENTS, /*id=*/0, + WebContentsMediaCaptureId(5, 5)) + .ToString(); + media_stream_manager_->UseFakeUIFactoryForTests( + base::BindLambdaForTesting([&]() { + auto fake_ui = std::make_unique<FakeMediaStreamUIProxy>( + /*tests_use_fake_render_frame_hosts=*/true); + fake_ui->AddAvailableDevices({blink::MediaStreamDevice( + blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE, tab_id, + "Tab")}); + return std::unique_ptr<FakeMediaStreamUIProxy>(std::move(fake_ui)); + })); + + blink::StreamControls controls(false /* request_audio */, + true /* request_video */); + controls.video.stream_type = + blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE; + const int requester_id = 1; + const int page_request_id = 1; + + blink::MediaStreamDevice video_device; + MediaStreamManager::GenerateStreamsCallback generate_stream_callback = + base::BindOnce(GenerateStreamsCallback, &run_loop_, + /*request_audio=*/false, + /*request_video=*/true, /*audio_device=*/nullptr, + &video_device, + /*audio_share=*/true); + EXPECT_CALL(*media_observer_, OnMediaRequestStateChanged(_, _, _, _, _, _)) + .Times(testing::AtLeast(1)); + + media_stream_manager_->GenerateStreams( + kRenderFrameHostId, requester_id, page_request_id, controls, + MediaDeviceSaltAndOrigin::Empty(), false /* user_gesture */, + StreamSelectionInfo::NewSearchOnlyByDeviceId({}), + std::move(generate_stream_callback), + MediaStreamManager::DeviceStoppedCallback(), + MediaStreamManager::DeviceChangedCallback(), + MediaStreamManager::DeviceRequestStateChangeCallback(), + MediaStreamManager::DeviceCaptureConfigurationChangeCallback(), + MediaStreamManager::DeviceCaptureHandleChangeCallback(), + MediaStreamManager::ZoomLevelChangeCallback()); + run_loop_.Run(); + EXPECT_EQ(controls.video.stream_type, video_device.type); + + const std::string request_label = GetLatestLabel(); + const base::UnguessableToken session_id = video_device.session_id(); + media::VideoCaptureFormats formats; + ASSERT_TRUE( + media_stream_manager_->video_capture_manager()->GetDeviceSupportedFormats( + session_id, &formats)); + + // Request a source change. The request moves to MEDIA_DEVICE_UPDATE and its + // state becomes PENDING_APPROVAL while the source-change request is + // outstanding on the UI thread. + media_stream_manager_->ChangeMediaStreamSourceFromBrowser( + request_label, DesktopMediaID(), + /*captured_surface_control_active=*/false); + + // Stopping the device while change-source is pending must close the + // underlying capture session. + media_stream_manager_->StopStreamDevice(kRenderFrameHostId, requester_id, + video_device.id, + video_device.session_id()); + EXPECT_TRUE( + media_stream_manager_->GetDevicesOpenedByRequest(request_label).empty()); + EXPECT_TRUE(base::test::RunUntil([&]() { + formats.clear(); + return !media_stream_manager_->video_capture_manager() + ->GetDeviceSupportedFormats(session_id, &formats); + })); +} + TEST_F(MediaStreamManagerTest, MultiCaptureOnMediaStreamUIWindowId) { std::vector<media::VideoCaptureSessionId> session_ids; VideoCaptureManager::SetDesktopCaptureWindowIdCallback callback =
Regression Test / PoC
diff --git a/content/browser/renderer_host/media/media_stream_manager_unittest.cc b/content/browser/renderer_host/media/media_stream_manager_unittest.cc
index aa17c9e..46b3293 100644
--- a/content/browser/renderer_host/media/media_stream_manager_unittest.cc
+++ b/content/browser/renderer_host/media/media_stream_manager_unittest.cc
@@ -23,6 +23,7 @@
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/mock_callback.h"
+#include "base/test/run_until.h"
#include "base/test/scoped_command_line.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
@@ -1635,6 +1636,153 @@
session_id, &formats));
}
+TEST_F(MediaStreamManagerTest, DesktopCaptureCancelDuringPendingChangeSource) {
+ const std::string tab_id =
+ DesktopMediaID(DesktopMediaID::TYPE_WEB_CONTENTS, /*id=*/0,
+ WebContentsMediaCaptureId(5, 5))
+ .ToString();
+ media_stream_manager_->UseFakeUIFactoryForTests(
+ base::BindLambdaForTesting([&]() {
+ auto fake_ui = std::make_unique<FakeMediaStreamUIProxy>(
+ /*tests_use_fake_render_frame_hosts=*/true);
+ fake_ui->AddAvailableDevices({blink::MediaStreamDevice(
+ blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE, tab_id,
+ "Tab")});
+ return std::unique_ptr<FakeMediaStreamUIProxy>(std::move(fake_ui));
+ }));
+
+ blink::StreamControls controls(false /* request_audio */,
+ true /* request_video */);
+ controls.video.stream_type =
+ blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE;
+ const int requester_id = 1;
+ const int page_request_id = 1;
+
+ blink::MediaStreamDevice video_device;
+ MediaStreamManager::GenerateStreamsCallback generate_stream_callback =
+ base::BindOnce(GenerateStreamsCallback, &run_loop_,
+ /*request_audio=*/false,
+ /*request_video=*/true, /*audio_device=*/nullptr,
+ &video_device,
+ /*audio_share=*/true);
+ EXPECT_CALL(*media_observer_, OnMediaRequestStateChanged(_, _, _, _, _, _))
+ .Times(testing::AtLeast(1));
+
+ media_stream_manager_->GenerateStreams(
+ kRenderFrameHostId, requester_id, page_request_id, controls,
+ MediaDeviceSaltAndOrigin::Empty(), false /* user_gesture */,
+ StreamSelectionInfo::NewSearchOnlyByDeviceId({}),
+ std::move(generate_stream_callback),
+ MediaStreamManager::DeviceStoppedCallback(),
+ MediaStreamManager::DeviceChangedCallback(),
+ MediaStreamManager::DeviceRequestStateChangeCallback(),
+ MediaStreamManager::DeviceCaptureConfigurationChangeCallback(),
+ MediaStreamManager::DeviceCaptureHandleChangeCallback(),
+ MediaStreamManager::ZoomLevelChangeCallback());
+ run_loop_.Run();
+ EXPECT_EQ(controls.video.stream_type, video_device.type);
+
+ const std::string request_label = GetLatestLabel();
+ const base::UnguessableToken session_id = video_device.session_id();
+ media::VideoCaptureFormats formats;
+ ASSERT_TRUE(
+ media_stream_manager_->video_capture_manager()->GetDeviceSupportedFormats(
+ session_id, &formats));
+
+ // Request a source change. The request moves to MEDIA_DEVICE_UPDATE and its
+ // state becomes PENDING_APPROVAL while the source-change request is
+ // outstanding on the UI thread.
+ media_stream_manager_->ChangeMediaStreamSourceFromBrowser(
+ request_label, DesktopMediaID(),
+ /*captured_surface_control_active=*/false);
+
+ // Cancelling the request while change-source is pending must close the
+ // underlying capture session.
+ media_stream_manager_->CancelRequest(request_label);
+ EXPECT_TRUE(
+ media_stream_manager_->GetDevicesOpenedByRequest(request_label).empty());
+ EXPECT_TRUE(base::test::RunUntil([&]() {
+ formats.clear();
+ return !media_stream_manager_->video_capture_manager()
+ ->GetDeviceSupportedFormats(session_id, &formats);
+ }));
+}
+
+TEST_F(MediaStreamManagerTest,
+ DesktopCaptureStopDeviceDuringPendingChangeSource) {
+ const std::string tab_id =
+ DesktopMediaID(DesktopMediaID::TYPE_WEB_CONTENTS, /*id=*/0,
+ WebContentsMediaCaptureId(5, 5))
+ .ToString();
+ media_stream_manager_->UseFakeUIFactoryForTests(
+ base::BindLambdaForTesting([&]() {
+ auto fake_ui = std::make_unique<FakeMediaStreamUIProxy>(
+ /*tests_use_fake_render_frame_hosts=*/true);
+ fake_ui->AddAvailableDevices({blink::MediaStreamDevice(
+ blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE, tab_id,
+ "Tab")});
+ return std::unique_ptr<FakeMediaStreamUIProxy>(std::move(fake_ui));
+ }));
+
+ blink::StreamControls controls(false /* request_audio */,
+ true /* request_video */);
+ controls.video.stream_type =
+ blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE;
+ const int requester_id = 1;
+ const int page_request_id = 1;
+
+ blink::MediaStreamDevice video_device;
+ MediaStreamManager::GenerateStreamsCallback generate_stream_callback =
+ base::BindOnce(GenerateStreamsCallback, &run_loop_,
+ /*request_audio=*/false,
+ /*request_video=*/true, /*audio_device=*/nullptr,
+ &video_device,
+ /*audio_share=*/true);
+ EXPECT_CALL(*media_observer_, OnMediaRequestStateChanged(_, _, _, _, _, _))
+ .Times(testing::AtLeast(1));
+
+ media_stream_manager_->GenerateStreams(
+ kRenderFrameHostId, requester_id, page_request_id, controls,
+ MediaDeviceSaltAndOrigin::Empty(), false /* user_gesture */,
+ StreamSelectionInfo::NewSearchOnlyByDeviceId({}),
+ std::move(generate_stream_callback),
+ MediaStreamManager::DeviceStoppedCallback(),
+ MediaStreamManager::DeviceChangedCallback(),
+ MediaStreamManager::DeviceRequestStateChangeCallback(),
+ MediaStreamManager::DeviceCaptureConfigurationChangeCallback(),
+ MediaStreamManager::DeviceCaptureHandleChangeCallback(),
+ MediaStreamManager::ZoomLevelChangeCallback());
+ run_loop_.Run();
+ EXPECT_EQ(controls.video.stream_type, video_device.type);
+
+ const std::string request_label = GetLatestLabel();
+ const base::UnguessableToken session_id = video_device.session_id();
+ media::VideoCaptureFormats formats;
+ ASSERT_TRUE(
+ media_stream_manager_->video_capture_manager()->GetDeviceSupportedFormats(
+ session_id, &formats));
+
+ // Request a source change. The request moves to MEDIA_DEVICE_UPDATE and its
+ // state becomes PENDING_APPROVAL while the source-change request is
+ // outstanding on the UI thread.
+ media_stream_manager_->ChangeMediaStreamSourceFromBrowser(
+ request_label, DesktopMediaID(),
+ /*captured_surface_control_active=*/false);
+
+ // Stopping the device while change-source is pending must close the
+ // underlying capture session.
+ media_stream_manager_->StopStreamDevice(kRenderFrameHostId, requester_id,
+ video_device.id,
+ video_device.session_id());
+ EXPECT_TRUE(
+ media_stream_manager_->GetDevicesOpenedByRequest(request_label).empty());
+ EXPECT_TRUE(base::test::RunUntil([&]() {
+ formats.clear();
+ return !media_stream_manager_->video_capture_manager()
+ ->GetDeviceSupportedFormats(session_id, &formats);
+ }));
+}
+
TEST_F(MediaStreamManagerTest, MultiCaptureOnMediaStreamUIWindowId) {
std::vector<media::VideoCaptureSessionId> session_ids;
VideoCaptureManager::SetDesktopCaptureWindowIdCallback callback =
Original Bug Report
Tab-capture teardown bypass during pending change-source requests orphans capture session
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential state-machine logic flaw in MediaStreamManager allows a compromised renderer to bypass sharing indicators. When a renderer-initiated teardown occurs while a browser-initiated change-source request is pending, active device cleanup is skipped. This removes all user-facing sharing indicators while leaving the underlying capture session active and accessible.
Affected files:
content/browser/renderer_host/media/media_stream_manager.ccchrome/browser/media/webrtc/display_media_access_handler.cccontent/browser/renderer_host/media/media_stream_ui_proxy.ccchrome/browser/ui/views/tab_sharing/tab_sharing_ui_views.cc
Estimated timestamp from git blame: Unknown (Google3 checkout)
Description
A potential state-machine logic flaw in MediaStreamManager allows a compromised renderer to bypass sharing indicators and continuously exfiltrate tab-capture pixels.
When a user initiates a tab-source change (e.g., clicking the “Share this tab instead” button on a browser infobar), MediaStreamManager::PostRequestToUI overwrites the active device’s state to MEDIA_REQUEST_STATE_PENDING_APPROVAL on the active DeviceRequest (content/browser/renderer_host/media/media_stream_manager.cc):
if (blink::IsVideoInputMediaType(video_type)) {
request->SetState(video_type, MEDIA_REQUEST_STATE_PENDING_APPROVAL);
}
If the renderer triggers a teardown (via CloseDevice or StopStreamDevice Mojo IPCs) while this change-source request is pending, CancelRequest (media_stream_manager.cc:2538) or StopDevice (media_stream_manager.cc:2014) skips calling CloseDevice because the state is neither MEDIA_REQUEST_STATE_OPENING nor MEDIA_REQUEST_STATE_DONE:
const MediaRequestState state = request->state(device.type);
if (state != MEDIA_REQUEST_STATE_OPENING &&
state != MEDIA_REQUEST_STATE_DONE) {
continue; // <-- CloseDevice is skipped
}
CloseDevice(device.type, device.session_id());
However, the teardown path still proceeds to execute DeleteRequest(request_it), which destroys the request and its associated MediaStreamUIProxy. This completely clears all user-facing sharing indicators (such as the infobars and the tab-strip capture glyph). Because CloseDevice was skipped, the underlying capture session in VideoCaptureManager remains active, and the compromised renderer continues to receive video frames silently.
Note: The PENDING_APPROVAL window on the UI thread can be held open for an arbitrary duration by queuing a second getDisplayMedia picker prompt, making the race window highly reliable to hit.
Potential Steps to Trigger / Exploit
Note: The following are suggested, potential steps. Our static analysis tooling is descriptive and does not currently have the capability to run code or execute a live proof of concept.
- A compromised renderer initiates tab capture via
navigator.mediaDevices.getDisplayMedia({video: true, surfaceSwitching: 'include'})and begins receiving video frames fromVideoCaptureHost::Start(session_id). - The renderer calls
getDisplayMediaa second time to prompt another picker. This queues aPendingAccessRequestat the UI side inDisplayMediaAccessHandler::pending_requests_(chrome/browser/media/webrtc/display_media_access_handler.cc:442). - The user clicks the browser-provided “Share this tab instead” button on another tab’s infobar. This invokes
TabSharingUIViews::StartSharing(chrome/browser/ui/views/tab_sharing/tab_sharing_ui_views.cc:251) and posts a source-change request back to the IO thread. - The IO thread receives the update request, changes the request type to
MEDIA_DEVICE_UPDATE, and overwrites the active device state toMEDIA_REQUEST_STATE_PENDING_APPROVAL(media_stream_manager.cc:2633). This update is forwarded to the UI thread, where it is queued behind the still-open second media picker. - While the second media picker remains open (blocking the queue and holding the state as
PENDING_APPROVAL), the compromised renderer sends a Mojo IPCStopStreamDeviceorCloseDevicemessage. - On the IO thread,
CancelRequestorStopDevicesees that the request state isPENDING_APPROVAL. It skips callingCloseDevice, meaning the video capture session remains active inVideoCaptureManager. However, it proceeds to callDeleteRequest, destroying theMediaStreamUIProxyand instantly wiping out all visible sharing indicators. - The user eventually dismisses the second picker. The early-return check at
HandleAccessRequestResponse(media_stream_manager.cc:3615) prevents the state-restoring fix from running because the request has already been deleted fromrequests_. - The compromised renderer continues to receive capture frames invisibly.
Potential Impact
A compromised renderer can capture cross-origin tab pixels persistently and invisibly after the user believes sharing has completely stopped, bypassing standard security indicators and browser controls.
Suggested Fix
Ensure that if a DeviceRequest is canceled or stopped while in the MEDIA_REQUEST_STATE_PENDING_APPROVAL state (or any other intermediate state during a dynamic update), any already active/opened devices are still properly closed. Rather than relying solely on the request’s pending UI state, MediaStreamManager should track active device sessions independently or check if a session is actively running before skipping CloseDevice in the teardown loop.
Evaluated with Chrome root at commit: 84065d9121f6e48f67755f0ae963cc09617e5c85
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.