CVE-2026-95278
Overview
Background
- Wake lock
- a platform primitive that keeps a device awake; a screen wake lock (
kPreventDisplaySleep) keeps the display on, while a system wake lock (kPreventAppSuspension) keeps the whole machine from suspending. - `WakeLockServiceImpl`
- a browser-process
DocumentServicethat implements theblink::mojom::WakeLockServiceMojo interface a renderer talks to, brokering wake-lock requests to the device service viaWakeLockContext. - Permissions Policy
- a per-document allow/deny mechanism;
kScreenWakeLock(thescreen-wake-lockfeature) governs whether a frame may hold a screen wake lock, checked withRenderFrameHost::IsFeatureEnabled. - Compromised renderer
- a threat model in which the sandboxed renderer process is attacker-controlled, so Blink-side (renderer) validation can be bypassed and the browser process must independently enforce every security invariant on Mojo IPC.
Root Cause Analysis
WakeLockServiceImpl::GetWakeLock received a renderer-supplied device::mojom::WakeLockType and forwarded it directly to WebContents::GetWakeLockContext() without any browser-side validation. Because the only checks on the requested type and on the screen-wake-lock Permissions Policy feature lived in Blink (the renderer), a compromised renderer could send any WakeLockType — including the system-level kPreventAppSuspension — or request a screen wake lock from a document where kScreenWakeLock was disabled. This violated the invariant that a web document may only acquire a screen wake lock, and only when its Permissions Policy permits it.
The fix adds two guards at the top of GetWakeLock: it returns early if type != WakeLockType::kPreventDisplaySleep, and returns early if render_frame_host().IsFeatureEnabled(PermissionsPolicyFeature::kScreenWakeLock) is false. On either early return the incoming mojo::PendingReceiver<device::mojom::WakeLock> is dropped (never bound), so the renderer’s wake-lock endpoint is closed rather than serviced, re-establishing the authorization check in the trusted browser process.
kPreventDisplaySleep type and any frame where kScreenWakeLock is not allowed.Attack Path
- Compromise the renderer
An attacker gains code execution in a sandboxed renderer process (e.g. via a separate renderer bug), giving them the ability to send arbitrary messages over the
blink::mojom::WakeLockServiceMojo pipe. (inferred) - Skip the Blink checks
Because type and Permissions Policy validation existed only in Blink, the attacker bypasses them and directly invokes
GetWakeLockon the browser-hostedWakeLockServiceImpl. - Request an unauthorized type
The attacker passes
WakeLockType::kPreventAppSuspension(a system-level lock) instead ofkPreventDisplaySleep, which the pre-patch code forwarded verbatim toWakeLockContext. - Or bypass Permissions Policy
Alternatively the attacker requests
kPreventDisplaySleepfrom a frame whose Permissions Policy disablesscreen-wake-lock, which the pre-patch code also honored. - Acquire the lock The browser process binds the receiver and grants the wake lock, keeping the display on or the system from suspending without the required authorization.
Impact Assessment
kPreventAppSuspension lock or a screen lock in a policy-disabled frame — corresponding to the advisory’s “Missing authorization in WakeLock” (a LogicError). The precondition is a compromised or malicious renderer able to reach the WakeLockService Mojo interface; the observable effect is preventing display sleep or system suspension (battery drain / power denial-of-service). The vendor advisory rates this low severity and does not report in-the-wild exploitation.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/wake_lock/wake_lock_service_impl.cc |
modified | |
WakeLockServiceImplcontent/browser/wake_lock/wake_lock_service_impl.h |
modified | |
CONTENT_EXPORTcontent/browser/wake_lock/wake_lock_service_impl.h |
modified | |
WakeLockServiceImplTestcontent/browser/wake_lock/wake_lock_service_impl_unittest.cc |
modified | |
ifcontent/browser/wake_lock/wake_lock_service_impl_unittest.cc |
modified | |
TEST_Fcontent/browser/wake_lock/wake_lock_service_impl_unittest.cc |
modified |
Files Changed
content/browser/wake_lock/wake_lock_service_impl.hcontent/browser/wake_lock/wake_lock_service_impl_unittest.cccontent/test/BUILD.gncontent/browser/wake_lock/wake_lock_service_impl.cc
Audit Directions
- Renderer-supplied enum forwardingAudit browser-process Mojo handlers that forward a renderer-provided enum (like
WakeLockType) or type parameter straight into a privileged capability without validating it against the set permitted for web content. - Renderer-only Permissions Policy checksLook for features whose Permissions Policy / permission gating is enforced only in Blink; the browser-side
DocumentServiceshould independently callRenderFrameHost::IsFeatureEnabledbefore granting the capability. - Device-service brokersReview other
DocumentService-based brokers toservices/device(sensors, geolocation, USB, HID) for the same pattern of binding aPendingReceiverbefore running all authorization checks.
Patch
From e65d6a695c3ea789781529252cfc6266d1cf87bf Mon Sep 17 00:00:00 2001
From: Rob Pitkin <robpitkin@google.com>
Date: Wed, 26 Aug 2026 08:58:03 -0700
Subject: [PATCH] wake-lock: Validate type and permissions in WakeLockServiceImpl
WakeLockServiceImpl::GetWakeLock directly forwarded renderer-requested
WakeLockType parameters to WakeLockContext without verifying that the
requested type is allowed for web documents (kPreventDisplaySleep) or
checking that the frame's Permissions Policy allows kScreenWakeLock.
A compromised renderer could bypass Blink-side checks to request
unauthorized system-level wake locks (kPreventAppSuspension) or acquire
wake locks in contexts where the screen-wake-lock feature is disabled.
This CL updates WakeLockServiceImpl::GetWakeLock to return early and
drop the incoming receiver when the requested type is not
kPreventDisplaySleep or when the frame's Permissions Policy does not
allow kScreenWakeLock.
Bug: 497344014
Test: content_unittests --gtest_filter="WakeLockServiceImplTest.*"
Change-Id: I1df126fd9a17b0b9fd5805b975e3098edaa5d069
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8289909
Reviewed-by: Bo Liu <boliu@chromium.org>
Commit-Queue: Rob Pitkin <robpitkin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1686434}
---
diff --git a/content/browser/wake_lock/wake_lock_service_impl.cc b/content/browser/wake_lock/wake_lock_service_impl.cc
index bcf1c1b5..06fe1ec3 100644
--- a/content/browser/wake_lock/wake_lock_service_impl.cc
+++ b/content/browser/wake_lock/wake_lock_service_impl.cc
@@ -4,8 +4,10 @@
#include "content/browser/wake_lock/wake_lock_service_impl.h"
+#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "services/device/public/mojom/wake_lock_context.mojom.h"
+#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom.h"
namespace content {
@@ -22,6 +24,16 @@
device::mojom::WakeLockReason reason,
const std::string& description,
mojo::PendingReceiver<device::mojom::WakeLock> receiver) {
+ // Web content is only permitted to request screen wake locks.
+ if (type != device::mojom::WakeLockType::kPreventDisplaySleep) {
+ return;
+ }
+
+ if (!render_frame_host().IsFeatureEnabled(
+ network::mojom::PermissionsPolicyFeature::kScreenWakeLock)) {
+ return;
+ }
+
device::mojom::WakeLockContext* wake_lock_context =
WebContents::FromRenderFrameHost(&render_frame_host())
->GetWakeLockContext();
diff --git a/content/browser/wake_lock/wake_lock_service_impl.h b/content/browser/wake_lock/wake_lock_service_impl.h
index 137e107..5477b04 100644
--- a/content/browser/wake_lock/wake_lock_service_impl.h
+++ b/content/browser/wake_lock/wake_lock_service_impl.h
@@ -5,13 +5,14 @@
#ifndef CONTENT_BROWSER_WAKE_LOCK_WAKE_LOCK_SERVICE_IMPL_H_
#define CONTENT_BROWSER_WAKE_LOCK_WAKE_LOCK_SERVICE_IMPL_H_
+#include "content/common/content_export.h"
#include "content/public/browser/document_service.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "third_party/blink/public/mojom/wake_lock/wake_lock.mojom.h"
namespace content {
-class WakeLockServiceImpl final
+class CONTENT_EXPORT WakeLockServiceImpl final
: public DocumentService<blink::mojom::WakeLockService> {
public:
static void Create(RenderFrameHost*,
diff --git a/content/browser/wake_lock/wake_lock_service_impl_unittest.cc b/content/browser/wake_lock/wake_lock_service_impl_unittest.cc
new file mode 100644
index 0000000..d74fc2e
--- /dev/null
+++ b/content/browser/wake_lock/wake_lock_service_impl_unittest.cc
@@ -0,0 +1,117 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/wake_lock/wake_lock_service_impl.h"
+
+#include "content/public/test/navigation_simulator.h"
+#include "content/test/test_render_frame_host.h"
+#include "content/test/test_render_view_host.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "services/device/public/mojom/wake_lock.mojom.h"
+#include "services/network/public/cpp/permissions_policy/permissions_policy_declaration.h"
+#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/mojom/wake_lock/wake_lock.mojom.h"
+#include "url/gurl.h"
+
+namespace content {
+
+namespace {
+
+constexpr char kTestUrl[] = "https://example.com";
+
+} // namespace
+
+class WakeLockServiceImplTest : public RenderViewHostImplTestHarness {
+ public:
+ WakeLockServiceImplTest() = default;
+ ~WakeLockServiceImplTest() override = default;
+
+ WakeLockServiceImplTest(const WakeLockServiceImplTest&) = delete;
+ WakeLockServiceImplTest& operator=(const WakeLockServiceImplTest&) = delete;
+
+ void SetUp() override {
+ RenderViewHostImplTestHarness::SetUp();
+ NavigateAndCommit(GURL(kTestUrl));
+ }
+
+ mojo::Remote<blink::mojom::WakeLockService> BindWakeLockService(
+ RenderFrameHost* rfh = nullptr) {
+ if (!rfh) {
+ rfh = main_rfh();
+ }
+ mojo::Remote<blink::mojom::WakeLockService> service;
+ WakeLockServiceImpl::Create(rfh, service.BindNewPipeAndPassReceiver());
+ return service;
+ }
+};
+
+TEST_F(WakeLockServiceImplTest, RequestScreenWakeLockSuccess) {
+ mojo::Remote<blink::mojom::WakeLockService> service = BindWakeLockService();
+
+ mojo::Remote<device::mojom::WakeLock> wake_lock;
+ service->GetWakeLock(device::mojom::WakeLockType::kPreventDisplaySleep,
+ device::mojom::WakeLockReason::kOther, "TestDescription",
+ wake_lock.BindNewPipeAndPassReceiver());
+
+ service.FlushForTesting();
+ EXPECT_TRUE(service.is_connected());
+ wake_lock.FlushForTesting();
+ EXPECT_TRUE(wake_lock.is_connected());
+}
+
+TEST_F(WakeLockServiceImplTest, RejectAppSuspensionWakeLock) {
+ mojo::Remote<blink::mojom::WakeLockService> service = BindWakeLockService();
+
+ mojo::Remote<device::mojom::WakeLock> wake_lock;
+ service->GetWakeLock(device::mojom::WakeLockType::kPreventAppSuspension,
+ device::mojom::WakeLockReason::kOther, "TestDescription",
+ wake_lock.BindNewPipeAndPassReceiver());
+
+ wake_lock.FlushForTesting();
+ EXPECT_FALSE(wake_lock.is_connected());
+ service.FlushForTesting();
+ EXPECT_TRUE(service.is_connected());
+}
+
+TEST_F(WakeLockServiceImplTest, RejectDisplaySleepAllowDimmingWakeLock) {
+ mojo::Remote<blink::mojom::WakeLockService> service = BindWakeLockService();
+
+ mojo::Remote<device::mojom::WakeLock> wake_lock;
+ service->GetWakeLock(
+ device::mojom::WakeLockType::kPreventDisplaySleepAllowDimming,
+ device::mojom::WakeLockReason::kOther, "TestDescription",
+ wake_lock.BindNewPipeAndPassReceiver());
+
+ wake_lock.FlushForTesting();
+ EXPECT_FALSE(wake_lock.is_connected());
+ service.FlushForTesting();
+ EXPECT_TRUE(service.is_connected());
+}
+
+TEST_F(WakeLockServiceImplTest, DisallowedByPermissionsPolicy) {
+ // Navigate with permissions policy disallowing screen-wake-lock.
+ network::ParsedPermissionsPolicy permissions_policy(1);
+ permissions_policy[0].feature =
+ network::mojom::PermissionsPolicyFeature::kScreenWakeLock;
+
+ auto navigation_simulator =
+ NavigationSimulator::CreateRendererInitiated(GURL(kTestUrl), main_rfh());
+ navigation_simulator->SetPermissionsPolicyHeader(permissions_policy);
+ navigation_simulator->Commit();
+
+ mojo::Remote<blink::mojom::WakeLockService> service = BindWakeLockService();
+
+ mojo::Remote<device::mojom::WakeLock> wake_lock;
+ service->GetWakeLock(device::mojom::WakeLockType::kPreventDisplaySleep,
+ device::mojom::WakeLockReason::kOther, "TestDescription",
+ wake_lock.BindNewPipeAndPassReceiver());
+
+ wake_lock.FlushForTesting();
+ EXPECT_FALSE(wake_lock.is_connected());
+ service.FlushForTesting();
+ EXPECT_TRUE(service.is_connected());
+}
+
+} // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 3680d0c..2f5ac88 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -2883,6 +2883,7 @@
"../browser/tracing/traces_internals/traces_internals_handler_unittest.cc",
"../browser/url_loader_factory_params_helper_unittest.cc",
"../browser/usb/web_usb_service_impl_unittest.cc",
+ "../browser/wake_lock/wake_lock_service_impl_unittest.cc",
"../browser/web_contents/aura/gesture_nav_simple_unittest.cc",
"../browser/web_contents/file_chooser_impl_unittest.cc",
"../browser/web_contents/web_contents_delegate_unittest.cc",
Regression Test / PoC
diff --git a/content/browser/wake_lock/wake_lock_service_impl_unittest.cc b/content/browser/wake_lock/wake_lock_service_impl_unittest.cc
new file mode 100644
index 0000000..d74fc2e
--- /dev/null
+++ b/content/browser/wake_lock/wake_lock_service_impl_unittest.cc
@@ -0,0 +1,117 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/wake_lock/wake_lock_service_impl.h"
+
+#include "content/public/test/navigation_simulator.h"
+#include "content/test/test_render_frame_host.h"
+#include "content/test/test_render_view_host.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "services/device/public/mojom/wake_lock.mojom.h"
+#include "services/network/public/cpp/permissions_policy/permissions_policy_declaration.h"
+#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/mojom/wake_lock/wake_lock.mojom.h"
+#include "url/gurl.h"
+
+namespace content {
+
+namespace {
+
+constexpr char kTestUrl[] = "https://example.com";
+
+} // namespace
+
+class WakeLockServiceImplTest : public RenderViewHostImplTestHarness {
+ public:
+ WakeLockServiceImplTest() = default;
+ ~WakeLockServiceImplTest() override = default;
+
+ WakeLockServiceImplTest(const WakeLockServiceImplTest&) = delete;
+ WakeLockServiceImplTest& operator=(const WakeLockServiceImplTest&) = delete;
+
+ void SetUp() override {
+ RenderViewHostImplTestHarness::SetUp();
+ NavigateAndCommit(GURL(kTestUrl));
+ }
+
+ mojo::Remote<blink::mojom::WakeLockService> BindWakeLockService(
+ RenderFrameHost* rfh = nullptr) {
+ if (!rfh) {
+ rfh = main_rfh();
+ }
+ mojo::Remote<blink::mojom::WakeLockService> service;
+ WakeLockServiceImpl::Create(rfh, service.BindNewPipeAndPassReceiver());
+ return service;
+ }
+};
+
+TEST_F(WakeLockServiceImplTest, RequestScreenWakeLockSuccess) {
+ mojo::Remote<blink::mojom::WakeLockService> service = BindWakeLockService();
+
+ mojo::Remote<device::mojom::WakeLock> wake_lock;
+ service->GetWakeLock(device::mojom::WakeLockType::kPreventDisplaySleep,
+ device::mojom::WakeLockReason::kOther, "TestDescription",
+ wake_lock.BindNewPipeAndPassReceiver());
+
+ service.FlushForTesting();
+ EXPECT_TRUE(service.is_connected());
+ wake_lock.FlushForTesting();
+ EXPECT_TRUE(wake_lock.is_connected());
+}
+
+TEST_F(WakeLockServiceImplTest, RejectAppSuspensionWakeLock) {
+ mojo::Remote<blink::mojom::WakeLockService> service = BindWakeLockService();
+
+ mojo::Remote<device::mojom::WakeLock> wake_lock;
+ service->GetWakeLock(device::mojom::WakeLockType::kPreventAppSuspension,
+ device::mojom::WakeLockReason::kOther, "TestDescription",
+ wake_lock.BindNewPipeAndPassReceiver());
+
+ wake_lock.FlushForTesting();
+ EXPECT_FALSE(wake_lock.is_connected());
+ service.FlushForTesting();
+ EXPECT_TRUE(service.is_connected());
+}
+
+TEST_F(WakeLockServiceImplTest, RejectDisplaySleepAllowDimmingWakeLock) {
+ mojo::Remote<blink::mojom::WakeLockService> service = BindWakeLockService();
+
+ mojo::Remote<device::mojom::WakeLock> wake_lock;
+ service->GetWakeLock(
+ device::mojom::WakeLockType::kPreventDisplaySleepAllowDimming,
+ device::mojom::WakeLockReason::kOther, "TestDescription",
+ wake_lock.BindNewPipeAndPassReceiver());
+
+ wake_lock.FlushForTesting();
+ EXPECT_FALSE(wake_lock.is_connected());
+ service.FlushForTesting();
+ EXPECT_TRUE(service.is_connected());
+}
+
+TEST_F(WakeLockServiceImplTest, DisallowedByPermissionsPolicy) {
+ // Navigate with permissions policy disallowing screen-wake-lock.
+ network::ParsedPermissionsPolicy permissions_policy(1);
+ permissions_policy[0].feature =
+ network::mojom::PermissionsPolicyFeature::kScreenWakeLock;
+
+ auto navigation_simulator =
+ NavigationSimulator::CreateRendererInitiated(GURL(kTestUrl), main_rfh());
+ navigation_simulator->SetPermissionsPolicyHeader(permissions_policy);
+ navigation_simulator->Commit();
+
+ mojo::Remote<blink::mojom::WakeLockService> service = BindWakeLockService();
+
+ mojo::Remote<device::mojom::WakeLock> wake_lock;
+ service->GetWakeLock(device::mojom::WakeLockType::kPreventDisplaySleep,
+ device::mojom::WakeLockReason::kOther, "TestDescription",
+ wake_lock.BindNewPipeAndPassReceiver());
+
+ wake_lock.FlushForTesting();
+ EXPECT_FALSE(wake_lock.is_connected());
+ service.FlushForTesting();
+ EXPECT_TRUE(service.is_connected());
+}
+
+} // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 3680d0c..2f5ac88 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -2883,6 +2883,7 @@
"../browser/tracing/traces_internals/traces_internals_handler_unittest.cc",
"../browser/url_loader_factory_params_helper_unittest.cc",
"../browser/usb/web_usb_service_impl_unittest.cc",
+ "../browser/wake_lock/wake_lock_service_impl_unittest.cc",
"../browser/web_contents/aura/gesture_nav_simple_unittest.cc",
"../browser/web_contents/file_chooser_impl_unittest.cc",
"../browser/web_contents/web_contents_delegate_unittest.cc",