CVE-2026-79104
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/generic_sensor/frame_sensor_provider_proxy.cc |
modified |
Files Changed
content/browser/generic_sensor/frame_sensor_provider_proxy.cc
Patch
From 7d7f3150edf76707accdc5ce07f0aa1c330cbb23 Mon Sep 17 00:00:00 2001
From: Rob Pitkin <robpitkin@google.com>
Date: Mon, 06 Jul 2026 10:42:42 -0700
Subject: [PATCH] sensors: Supervisory control for generic sensors
Prior to this change, the browser process brokered a direct Mojo data
connection for Generic Sensors between the renderer and the Device
Service without retaining supervisory control. This could allow a
compromised renderer to bypass BFCache suspension, page visibility loss
gating, and permission revocation.
This change introduces the SensorClientController supervisory control
interface, brokered and tracked by FrameSensorProviderProxy in the
browser process. When a page is hidden, enters BFCache, or has its
sensor permissions revoked, Content broadcasts Suspend() or clears the
brokered sessions over this control pipe, ensuring the Device Service
pauses reading updates or tears down the sensor connection entirely.
services_unittests
gtest_filter="FrameSensorProviderProxyTest.*"
gtest_filter="GenericSensorServiceTest.*"
Bug: 497017869
Test: autoninja -C out/Default content_unittests device_unittests
Test: out/Default/content_unittests --
Test: out/Default/services_unittests --
Change-Id: Id4d53926f4434ffbb52083a71b6a8c981b92d689
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7919973
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Commit-Queue: Rob Pitkin <robpitkin@chromium.org>
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1657294}
---
diff --git a/content/browser/generic_sensor/frame_sensor_provider_proxy.cc b/content/browser/generic_sensor/frame_sensor_provider_proxy.cc
index d992e1f..c06f9f0 100644
--- a/content/browser/generic_sensor/frame_sensor_provider_proxy.cc
+++ b/content/browser/generic_sensor/frame_sensor_provider_proxy.cc
@@ -67,8 +67,9 @@
FrameSensorProviderProxy::FrameSensorProviderProxy(
RenderFrameHost* render_frame_host)
- : DocumentUserData<FrameSensorProviderProxy>(render_frame_host) {
- watcher_receivers_.set_disconnect_handler(
+ : DocumentUserData<FrameSensorProviderProxy>(render_frame_host),
+ WebContentsObserver(WebContents::FromRenderFrameHost(render_frame_host)) {
+ client_controllers_.set_disconnect_handler(
base::BindRepeating(&FrameSensorProviderProxy::OnSensorDisconnect,
weak_factory_.GetWeakPtr()));
}
@@ -84,7 +85,7 @@
// Notify the delegate for each active connection that is being cleared.
auto* delegate = GetContentClient()->browser()->GetSensorDelegate();
if (delegate) {
- for (size_t i = 0; i < watcher_receivers_.size(); ++i) {
+ for (size_t i = 0; i < client_controllers_.size(); ++i) {
delegate->OnSensorStopped(&render_frame_host());
}
}
@@ -97,6 +98,7 @@
void FrameSensorProviderProxy::OnMojoConnectionError() {
receiver_set_.Clear();
+ client_controllers_.Clear();
}
void FrameSensorProviderProxy::GetSensor(device::mojom::SensorType type,
@@ -144,7 +146,7 @@
WebContents::FromRenderFrameHost(&render_frame_host()));
web_contents_sensor_provider->GetSensor(
- type, mojo::NullRemote(),
+ type, mojo::NullReceiver(), /*initially_suspended=*/false,
base::BindOnce(
&FrameSensorProviderProxy::OnHardwareCheckForBlockedSensor,
weak_factory_.GetWeakPtr(), type));
@@ -158,33 +160,37 @@
scoped_observation_.Observe(web_contents_sensor_provider);
}
- if (base::FeatureList::IsEnabled(
- features::kSeverSensorConnectionsOnPermissionRevocation) &&
- !permission_subscription_id_) {
- permission_subscription_id_ =
- permission_controller->SubscribeToPermissionResultChange(
- content::PermissionDescriptorUtil::
- CreatePermissionDescriptorForPermissionType(
- blink::PermissionType::SENSORS),
- nullptr, &render_frame_host(),
- render_frame_host().GetLastCommittedOrigin().GetURL(),
- /*should_include_device_status=*/false,
- base::BindRepeating(&FrameSensorProviderProxy::OnPermissionChanged,
- weak_factory_.GetWeakPtr()));
- }
+ mojo::PendingReceiver<device::mojom::SensorClientController>
+ controller_receiver;
+ mojo::PendingRemote<device::mojom::SensorClientController> controller;
+ bool initially_suspended = false;
- // Create watcher but defer binding until success
- mojo::PendingRemote<device::mojom::SensorConnectionWatcher> watcher;
- mojo::PendingReceiver<device::mojom::SensorConnectionWatcher> receiver;
if (ShouldTrackSensorConnection()) {
- receiver = watcher.InitWithNewPipeAndPassReceiver();
+ if (!permission_subscription_id_) {
+ permission_subscription_id_ =
+ permission_controller->SubscribeToPermissionResultChange(
+ content::PermissionDescriptorUtil::
+ CreatePermissionDescriptorForPermissionType(
+ blink::PermissionType::SENSORS),
+ nullptr, &render_frame_host(),
+ render_frame_host().GetLastCommittedOrigin().GetURL(),
+ /*should_include_device_status=*/false,
+ base::BindRepeating(
+ &FrameSensorProviderProxy::OnPermissionChanged,
+ weak_factory_.GetWeakPtr()));
+ }
+
+ controller_receiver = controller.InitWithNewPipeAndPassReceiver();
+ if (base::FeatureList::IsEnabled(features::kSensorPrivacyMitigations)) {
+ initially_suspended = ShouldSuspendSensors();
+ }
}
web_contents_sensor_provider->GetSensor(
- type, std::move(watcher),
+ type, std::move(controller_receiver), initially_suspended,
base::BindOnce(&FrameSensorProviderProxy::OnHardwareCheckCompleted,
weak_factory_.GetWeakPtr(), type, permission_status,
- has_valid_gesture, std::move(receiver),
+ has_valid_gesture, std::move(controller),
std::move(callback)));
}
@@ -192,7 +198,7 @@
device::mojom::SensorType type,
blink::mojom::PermissionStatus permission_status,
bool user_gesture,
- mojo::PendingReceiver<device::mojom::SensorConnectionWatcher> receiver,
+ mojo::PendingRemote<device::mojom::SensorClientController> controller,
GetSensorCallback callback,
device::mojom::SensorCreationResult result,
device::mojom::SensorInitParamsPtr params) {
@@ -207,7 +213,7 @@
}
if (permission_status == blink::mojom::PermissionStatus::GRANTED) {
- FinalizeSensorConnection(std::move(receiver));
+ FinalizeSensorConnection(std::move(controller));
std::move(callback).Run(result, std::move(params));
return;
}
@@ -227,7 +233,7 @@
user_gesture),
base::BindOnce(&FrameSensorProviderProxy::OnPermissionRequestCompleted,
weak_factory_.GetWeakPtr(), std::move(params),
- std::move(callback), std::move(receiver)));
+ std::move(callback), std::move(controller)));
}
void FrameSensorProviderProxy::OnHardwareCheckForBlockedSensor(
@@ -256,7 +262,7 @@
void FrameSensorProviderProxy::OnPermissionRequestCompleted(
device::mojom::SensorInitParamsPtr params,
GetSensorCallback callback,
- mojo::PendingReceiver<device::mojom::SensorConnectionWatcher> receiver,
+ mojo::PendingRemote<device::mojom::SensorClientController> controller,
PermissionResult permission_result) {
if (permission_result.status != blink::mojom::PermissionStatus::GRANTED) {
std::move(callback).Run(
@@ -264,7 +270,7 @@
return;
}
- FinalizeSensorConnection(std::move(receiver));
+ FinalizeSensorConnection(std::move(controller));
std::move(callback).Run(device::mojom::SensorCreationResult::SUCCESS,
std::move(params));
@@ -277,15 +283,49 @@
GetContentClient()->browser()->GetSensorDelegate();
if (delegate) {
// Notify the delegate for each active connection that is being cleared.
- for (size_t i = 0; i < watcher_receivers_.size(); ++i) {
+ for (size_t i = 0; i < client_controllers_.size(); ++i) {
delegate->OnSensorStopped(&render_frame_host());
}
}
- watcher_receivers_.Clear();
+ client_controllers_.Clear();
}
}
-void FrameSensorProviderProxy::OnSensorDisconnect() {
+void FrameSensorProviderProxy::OnVisibilityChanged(
+ content::Visibility visibility) {
+ UpdateSensorSessionControllers();
+}
+
+void FrameSensorProviderProxy::RenderFrameHostStateChanged(
Regression Test / PoC
diff --git a/content/browser/generic_sensor/frame_sensor_provider_proxy_unittest.cc b/content/browser/generic_sensor/frame_sensor_provider_proxy_unittest.cc
index dfb3c42..fc8d0a5 100644
--- a/content/browser/generic_sensor/frame_sensor_provider_proxy_unittest.cc
+++ b/content/browser/generic_sensor/frame_sensor_provider_proxy_unittest.cc
@@ -259,5 +259,61 @@
EXPECT_EQ(result, device::mojom::SensorCreationResult::ERROR_NOT_ALLOWED);
}
+TEST_F(FrameSensorProviderProxyTest,
+ GetSensor_VisibilityChanged_SuspendsAndResumes) {
+ EXPECT_CALL(*permission_manager(),
+ GetPermissionResultForCurrentDocument(_, _, _))
+ .WillOnce(Return(PermissionResult(blink::mojom::PermissionStatus::GRANTED,
+ PermissionStatusSource::UNSPECIFIED)));
+
+ auto provider = GetWebSensorProvider();
+ static_cast<TestRenderFrameHost*>(main_test_rfh())->SimulateUserActivation();
+
+ mojo::Remote<device::mojom::Sensor> sensor_remote;
+ base::test::TestFuture<device::mojom::SensorCreationResult,
+ device::mojom::SensorInitParamsPtr>
+ future;
+ provider->GetSensor(device::mojom::SensorType::ACCELEROMETER,
+ /*user_gesture=*/true, future.GetCallback());
+ auto [result, params] = future.Take();
+ EXPECT_EQ(result, device::mojom::SensorCreationResult::SUCCESS);
+ sensor_remote.Bind(std::move(params->sensor));
+
+ device::FakeSensor* fake_sensor = fake_sensor_provider()->accelerometer();
+ ASSERT_TRUE(fake_sensor);
+
+ web_contents()->WasHidden();
+ EXPECT_TRUE(fake_sensor->WaitForBrowserSuspend(true));
+
+ web_contents()->WasShown();
+ EXPECT_TRUE(fake_sensor->WaitForBrowserSuspend(false));
+}
+
+TEST_F(FrameSensorProviderProxyTest,
+ GetSensor_InitiallyHidden_StartsSuspended) {
+ EXPECT_CALL(*permission_manager(),
+ GetPermissionResultForCurrentDocument(_, _, _))
+ .WillOnce(Return(PermissionResult(blink::mojom::PermissionStatus::GRANTED,
+ PermissionStatusSource::UNSPECIFIED)));
+
+ web_contents()->WasHidden();
+ auto provider = GetWebSensorProvider();
+ static_cast<TestRenderFrameHost*>(main_test_rfh())->SimulateUserActivation();
+
+ mojo::Remote<device::mojom::Sensor> sensor_remote;
+ base::test::TestFuture<device::mojom::SensorCreationResult,
+ device::mojom::SensorInitParamsPtr>
+ future;
+ provider->GetSensor(device::mojom::SensorType::ACCELEROMETER,
+ /*user_gesture=*/true, future.GetCallback());
+ auto [result, params] = future.Take();
+ EXPECT_EQ(result, device::mojom::SensorCreationResult::SUCCESS);
+ sensor_remote.Bind(std::move(params->sensor));
+
+ device::FakeSensor* fake_sensor = fake_sensor_provider()->accelerometer();
+ ASSERT_TRUE(fake_sensor);
+ EXPECT_TRUE(fake_sensor->is_browser_suspended());
+}
+
} // namespace
} // namespace content
diff --git a/content/browser/generic_sensor/web_contents_sensor_provider_proxy_unittest.cc b/content/browser/generic_sensor/web_contents_sensor_provider_proxy_unittest.cc
index 3ce1944..5daedf4b 100644
--- a/content/browser/generic_sensor/web_contents_sensor_provider_proxy_unittest.cc
+++ b/content/browser/generic_sensor/web_contents_sensor_provider_proxy_unittest.cc
@@ -137,11 +137,12 @@
void GetSensor(
device::mojom::SensorType type,
- mojo::PendingRemote<device::mojom::SensorConnectionWatcher> watcher,
+ mojo::PendingReceiver<device::mojom::SensorClientController> controller,
+ bool initially_suspended,
GetSensorCallback callback) override {
std::move(interception_callback_).Run();
- device::FakeSensorProvider::GetSensor(type, std::move(watcher),
- std::move(callback));
+ device::FakeSensorProvider::GetSensor(
+ type, std::move(controller), initially_suspended, std::move(callback));
}
private:
@@ -202,7 +203,7 @@
ASSERT_TRUE(fake_sensor);
base::RunLoop run_loop;
- fake_sensor->SetWatcherDisconnectCallback(run_loop.QuitClosure());
+ fake_sensor->SetControllerDisconnectCallback(run_loop.QuitClosure());
// Trigger permission revocation.
TestPermissionManager* permission_manager =
diff --git a/device/vr/test/fake_sensor_provider.cc b/device/vr/test/fake_sensor_provider.cc
index 09c2c7de..ac573989 100644
--- a/device/vr/test/fake_sensor_provider.cc
+++ b/device/vr/test/fake_sensor_provider.cc
@@ -29,7 +29,8 @@
void FakeXRSensorProvider::GetSensor(
mojom::SensorType type,
- mojo::PendingRemote<mojom::SensorConnectionWatcher> watcher,
+ mojo::PendingReceiver<mojom::SensorClientController> controller,
+ bool initially_suspended,
GetSensorCallback callback) {
callback_ = std::move(callback);
}
diff --git a/device/vr/test/fake_sensor_provider.h b/device/vr/test/fake_sensor_provider.h
index d82eff2..8736e35 100644
--- a/device/vr/test/fake_sensor_provider.h
+++ b/device/vr/test/fake_sensor_provider.h
@@ -8,7 +8,6 @@
#include "device/vr/vr_export.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
-#include "services/device/public/mojom/sensor.mojom.h"
#include "services/device/public/mojom/sensor_provider.mojom.h"
namespace device {
@@ -24,9 +23,11 @@
void CallCallback(mojom::SensorInitParamsPtr param);
// device::mojom::SensorProvider overrides.
- void GetSensor(mojom::SensorType type,
- mojo::PendingRemote<mojom::SensorConnectionWatcher> watcher,
- GetSensorCallback callback) override;
+ void GetSensor(
+ mojom::SensorType type,
+ mojo::PendingReceiver<mojom::SensorClientController> controller,
+ bool initially_suspended,
+ GetSensorCallback callback) override;
void CreateVirtualSensor(
mojom::SensorType type,
mojom::VirtualSensorMetadataPtr metadata,
diff --git a/services/device/generic_sensor/generic_sensor_service_unittest.cc b/services/device/generic_sensor/generic_sensor_service_unittest.cc
index cc5d0cf2..83759ec 100644
--- a/services/device/generic_sensor/generic_sensor_service_unittest.cc
+++ b/services/device/generic_sensor/generic_sensor_service_unittest.cc
@@ -14,6 +14,7 @@
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/device/device_service.h"
@@ -245,7 +246,8 @@
auto client = std::make_unique<TestSensorClient>(SensorType::ACCELEROMETER);
base::RunLoop run_loop;
sensor_provider_->GetSensor(
- SensorType::ACCELEROMETER, mojo::NullRemote(),
+ SensorType::ACCELEROMETER, mojo::NullReceiver(),
+ /*initially_suspended=*/false,
base::BindOnce(&TestSensorClient::OnSensorCreated,
base::Unretained(client.get()), run_loop.QuitClosure()));
run_loop.Run();
@@ -260,7 +262,8 @@
{
base::RunLoop run_loop;
sensor_provider_->GetSensor(
- SensorType::ACCELEROMETER, mojo::NullRemote(),
+ SensorType::ACCELEROMETER, mojo::NullReceiver(),
+ /*initially_suspended=*/false,
base::BindOnce(&TestSensorClient::OnSensorCreated,
base::Unretained(client.get()), run_loop.QuitClosure()));
run_loop.Run();
@@ -285,7 +288,8 @@
{
base::RunLoop run_loop;
sensor_provider_->GetSensor(
- SensorType::AMBIENT_LIGHT, mojo::NullRemote(),
+ SensorType::AMBIENT_LIGHT, mojo::NullReceiver(),
+ /*initially_suspended=*/false,
base::BindOnce(&TestSensorClient::OnSensorCreated,
base::Unretained(client.get()), run_loop.QuitClosure()));
run_loop.Run();
@@ -307,7 +311,8 @@
{
base::RunLoop run_loop;
sensor_provider_->GetSensor(
- SensorType::LINEAR_ACCELERATION, mojo::NullRemote(),
+ SensorType::LINEAR_ACCELERATION, mojo::NullReceiver(),
+ /*initially_suspended=*/false,
base::BindOnce(&TestSensorClient::OnSensorCreated,
base::Unretained(client.get()), run_loop.QuitClosure()));
run_loop.Run();
@@ -329,11 +334,13 @@
base::RunLoop run_loop;
auto barrier_closure = base::BarrierClosure(2, run_loop.QuitClosure());
sensor_provider_->GetSensor(
- SensorType::ACCELEROMETER, mojo::NullRemote(),
+ SensorType::ACCELEROMETER, mojo::NullReceiver(),
+ /*initially_suspended=*/false,
base::BindOnce(&TestSensorClient::OnSensorCreated,
base::Unretained(client_1.get()), barrier_closure));
sensor_provider_->GetSensor(
- SensorType::ACCELEROMETER, mojo::NullRemote(),
+ SensorType::ACCELEROMETER, mojo::NullReceiver(),
+ /*initially_suspended=*/false,
base::BindOnce(&TestSensorClient::OnSensorCreated,
base::Unretained(client_2.get()), barrier_closure));
run_loop.Run();
@@ -360,11 +367,13 @@
base::RunLoop run_loop;
auto barrier_closure = base::BarrierClosure(2, run_loop.QuitClosure());
sensor_provider_->GetSensor(
- SensorType::ACCELEROMETER, mojo::NullRemote(),
+ SensorType::ACCELEROMETER, mojo::NullReceiver(),
+ /*initially_suspended=*/false,
base::BindOnce(&TestSensorClient::OnSensorCreated,
base::Unretained(client_1.get()), barrier_closure));
sensor_provider_->GetSensor(
- SensorType::ACCELEROMETER, mojo::NullRemote(),
+ SensorType::ACCELEROMETER, mojo::NullReceiver(),
+ /*initially_suspended=*/false,
base::BindOnce(&TestSensorClient::OnSensorCreated,
base::Unretained(client_2.get()), barrier_closure));
run_loop.Run();
@@ -389,7 +398,8 @@
{
base::RunLoop run_loop;
sensor_provider_->GetSensor(
- SensorType::ACCELEROMETER, mojo::NullRemote(),
+ SensorType::ACCELEROMETER, mojo::NullReceiver(),
+ /*initially_suspended=*/false,
base::BindOnce(&TestSensorClient::OnSensorCreated,
base::Unretained(client.get()), run_loop.QuitClosure()));
run_loop.Run();
@@ -430,7 +440,8 @@
{
base::RunLoop run_loop;
sensor_provider_->GetSensor(
- SensorType::AMBIENT_LIGHT, mojo::NullRemote(),
+ SensorType::AMBIENT_LIGHT, mojo::NullReceiver(),
+ /*initially_suspended=*/false,
base::BindOnce(&TestSensorClient::OnSensorCreated,
base::Unretained(client.get()), run_loop.QuitClosure()));
run_loop.Run();
@@ -462,7 +473,8 @@
{
base::RunLoop run_loop;
sensor_provider_->GetSensor(
- SensorType::AMBIENT_LIGHT, mojo::NullRemote(),
+ SensorType::AMBIENT_LIGHT, mojo::NullReceiver(),
+ /*initially_suspended=*/false,
base::BindOnce(&TestSensorClient::OnSensorCreated,
base::Unretained(client.get()), run_loop.QuitClosure()));
run_loop.Run();
@@ -495,7 +507,8 @@
{
base::RunLoop run_loop;
sensor_provider_->GetSensor(
- SensorType::ACCELEROMETER, mojo::NullRemote(),
+ SensorType::ACCELEROMETER, mojo::NullReceiver(),
+ /*initially_suspended=*/false,
base::BindOnce(&TestSensorClient::OnSensorCreated,
base::Unretained(client.get()), run_loop.QuitClosure()));
run_loop.Run();
@@ -541,11 +554,13 @@
base::RunLoop run_loop;
auto barrier_closure = base::BarrierClosure(2, run_loop.QuitClosure());
sensor_provider_->GetSensor(
- SensorType::ACCELEROMETER, mojo::NullRemote(),
+ SensorType::ACCELEROMETER, mojo::NullReceiver(),
+ /*initially_suspended=*/false,
base::BindOnce(&TestSensorClient::OnSensorCreated,
base::Unretained(client_1.get()), barrier_closure));
sensor_provider_->GetSensor(
- SensorType::ACCELEROMETER, mojo::NullRemote(),
+ SensorType::ACCELEROMETER, mojo::NullReceiver(),
+ /*initially_suspended=*/false,
base::BindOnce(&TestSensorClient::OnSensorCreated,
base::Unretained(client_2.get()), barrier_closure));
run_loop.Run();
@@ -571,7 +586,8 @@
{
base::RunLoop run_loop;
sensor_provider_->GetSensor(
- SensorType::ACCELEROMETER, mojo::NullRemote(),
+ SensorType::ACCELEROMETER, mojo::NullReceiver(),
+ /*initially_suspended=*/false,
base::BindOnce(&TestSensorClient::OnSensorCreated,
base::Unretained(client.get()), run_loop.QuitClosure()));
run_loop.Run();
@@ -608,7 +624,8 @@
{
base::RunLoop run_loop;
sensor_provider_->GetSensor(
- SensorType::ACCELEROMETER, mojo::NullRemote(),
+ SensorType::ACCELEROMETER, mojo::NullReceiver(),
+ /*initially_suspended=*/false,
base::BindOnce(&TestSensorClient::OnSensorCreated,
base::Unretained(new_client.get()),
run_loop.QuitClosure()));
@@ -664,7 +681,8 @@
{
base::RunLoop run_loop;
sensor_provider_->GetSensor(
... (truncated)
Original Bug Report
Missing browser-side enforcement for Generic Sensor privacy mitigations
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: The browser process brokers a direct Mojo connection for Generic Sensors between a renderer and the Device Service without retaining control of the pipe. This potentially allows a compromised renderer to bypass visibility/focus gating, BFCache suspension, and permission revocation. An attacker could thereby continuously monitor sensitive motion data in the background or across cross-origin navigations.
Affected files:
content/browser/generic_sensor/frame_sensor_provider_proxy.cccontent/browser/generic_sensor/web_contents_sensor_provider_proxy.ccservices/device/generic_sensor/sensor_provider_impl.ccservices/device/generic_sensor/sensor_impl.ccthird_party/blink/renderer/modules/sensor/sensor_proxy.cc
Estimated timestamp from git blame: 2023-10-18
Vulnerability Details
The Generic Sensor API implementation currently suffers from a structural flaw where the browser process fails to retain control over sensor pipes after they are brokered to a renderer.
When a renderer requests a sensor, content::FrameSensorProviderProxy::GetSensor() performs a one-time permission and policy check. Upon success, it simply forwards the renderer’s Mojo callback to the Device Service (services/device/generic_sensor/sensor_provider_impl.cc). The Device Service creates a device::SensorImpl instance, bounds it to a mojo::UniqueReceiverSet<mojom::Sensor>, and returns a direct mojom::Sensor pipe and a shared-memory region handle directly to the renderer.
Crucially, neither FrameSensorProviderProxy nor WebContentsSensorProviderProxy retains a reference to this mojom::Sensor pipe. This architectural hand-off leads to several severe privacy mitigation bypasses:
- Visibility and Focus Gating Bypass: The Generic Sensor specification requires that sensors only provide data to visible, focused frames. This is currently enforced entirely client-side by the renderer (
blink::SensorProxy::ShouldSuspendUpdates()). The Device Service’sSensorImplblindly trustsResume()andSuspend()calls from the renderer. A compromised renderer can simply callResume()and ignore visibility state. - Permission Revocation Bypass: Because the browser proxy does not retain the brokered sensor pipes, it cannot close them if the user later revokes the site’s “Sensors” permission. The direct pipe to the Device Service remains active.
- Navigation and BFCache Bypass: When a document is destroyed or enters the BFCache,
FrameSensorProviderProxyis destroyed (as it is aDocumentUserData). However, destroying the proxy only severs the brokeringWebSensorProviderpipe. The actualSensorImplin the Device Service remains fully alive because its lifetime is tied to the renderer holding themojom::Sensorremote.
Suggested Attacker Steps
(Note: These are potential steps based on code analysis; our tooling agent does not yet have the ability to run code to produce a working PoC.)
- Achieve code execution within a sandboxed renderer process (e.g., via a separate V8 or Blink vulnerability).
- Construct a Mojo request to
blink::mojom::WebSensorProvider::GetSensor()for a sensitive sensor likeACCELEROMETER. - Because the page is active and motion sensors often default to allowed, the browser process’s initial permission check passes.
- Receive the
device::mojom::SensorInitParamsresponse, which contains a directmojom::Sensorremote to the Device Service and a read-only handle to the sensor’s shared memory buffer. - Call the
Resume()method on themojom::Sensorremote to instruct the Device Service to start polling the hardware. - Intentionally ignore any backgrounding, visibility loss, or focus loss events. Do not call
Suspend()on the remote. - Continue to read high-resolution motion data from the shared memory buffer indefinitely, even after the user navigates away or revokes permissions, enabling persistent side-channel attacks (e.g., keystroke inference).
Suggested Fix
To resolve this, the browser process must act as the authoritative gatekeeper for the lifetime and state of active sensors:
- Active Proxying or Tracking:
FrameSensorProviderProxyshould either actively proxy thedevice::mojom::Sensorinterface (interceptingSuspend/Resumeand sensor data notifications) or track the returned pipes so they can be forcefully disconnected. - Browser-side State Enforcement: The browser process should independently monitor the
WebContentsvisibility, focus state, andPermissionControllerstatus. When a frame loses visibility/focus, or if permissions are revoked, the browser should forcefully suspend or disconnect the tracked sensor pipes, rather than relying on the untrusted renderer to do so voluntarily.
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.