Low watch CVSS 6.5 chrome Info Disclosure 🔧 Commit mapped

Overview

Low
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in FoldableAPIs
DescriptionInformation leak in FoldableAPIs
ComponentFoldableAPIs
Bug ClassInfo Disclosure
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker522803735
Fix commita743780616a3 (chromium/src) +258/-17
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-06-11 (per vendor advisory)
Fix landed2026-07-07 (public git)
Disclosed2026-08-25
Patch gap49 days — fix public in source before the advisory

Background

Device Posture API
a web API (navigator.devicePosture) that tells a page whether a foldable device is continuous or folded, backed in the browser by DevicePostureProviderImpl, which pushes OnPostureChanged to each blink::mojom::DevicePostureClient registered for a WebContents.
`DevicePosturePlatformProvider`
the platform backend that reports the physical posture and calls DevicePostureProviderImpl::OnDevicePostureChanged when the hinge state changes.
`WebContentsObserver::OnVisibilityChanged`
a browser-side callback fired when a tab becomes VISIBLE, HIDDEN, or OCCLUDED. Before this fix the posture provider did not observe it.
DevTools posture emulation
OverrideDevicePostureForEmulation / DisableDevicePostureOverrideForEmulation replace the platform posture with an emulated value for a given WebContents.

Root Cause Analysis

DevicePostureProviderImpl sent every platform posture change to every registered client, whatever the visibility of the owning WebContents. The removed code in OnDevicePostureChanged only skipped dispatch when is_posture_emulated_ was set, and the emulation override and restore paths also looped over posture_clients_ with no visibility check.

As a result, pages in background (hidden) or occluded tabs got real-time posture-change events. Every tab, from any site, that subscribed would receive the same event at the same moment, which gives a cross-tab side channel about the user’s physical handling of the device (the commit title says “Privacy Side-Channel … via Background Tabs”).

The fix makes the class a WebContentsObserver. It returns early from all three dispatch paths when web_contents()->GetVisibility() != Visibility::VISIBLE, records last_dispatched_posture_, and in the new OnVisibilityChanged sends the current posture only when the tab becomes visible again and the value has changed. Hidden pages therefore see at most one catch-up value when shown, not a timed stream of events.

Key insight
The provider treated posture as data that could be sent to any subscribed page at any time and ignored whether the page was visible. That lets a background tab watch a shared physical signal in real time. The fix suppresses dispatch while the WebContents is hidden or occluded and sends a single catch-up value when the tab becomes visible again.

Attack Path

  1. Subscribe to posture A web page subscribes to Device Posture changes, which reaches AddListenerAndGetCurrentPosture and adds a client to posture_clients_. This is reachable from ordinary web content on a device and platform that provides a posture backend, and the bug only matters on foldable hardware.
  2. Go to the background The user switches tabs or covers the window, so the WebContents becomes HIDDEN or OCCLUDED, and the page keeps its listener.
  3. Receive events while hidden When the user folds or unfolds the device, OnDevicePostureChanged dispatched OnPostureChanged to the hidden page, because the only pre-patch guard was is_posture_emulated_.
  4. Correlate across tabs (inferred) Several cooperating or tracking pages in different background tabs or origins could match the timing of identical posture events to link one user’s sessions, or to infer user activity while the page was not in view.

Impact Assessment

A web page could observe physical device posture changes in real time while its tab was hidden or occluded. This leaks user-activity information and gives a timing signal that can be used for cross-site correlation (inferred) in the renderer of an unfocused tab. It needs a foldable device or platform that reports posture, and a page that has registered a posture listener. No compromised renderer or extension is required. The vendor advisory rates this low severity and does not report in-the-wild exploitation; NVD scores it CVSS 6.5.

Changed Functions

FunctionChangeNotes
WebContentsObserver
content/browser/device_posture/device_posture_provider_impl.cc
modified
for
content/browser/device_posture/device_posture_provider_impl.cc
modified
if
content/browser/device_posture/device_posture_provider_impl.cc
modified
DevicePostureProviderImpl
content/browser/device_posture/device_posture_provider_impl.h
modified
CONTENT_EXPORT
content/browser/device_posture/device_posture_provider_impl.h
modified
MockDevicePostureClient
content/browser/device_posture/device_posture_provider_impl_unittest.cc
modified
DevicePostureProviderImplTest
content/browser/device_posture/device_posture_provider_impl_unittest.cc
modified
TEST_F
content/browser/device_posture/device_posture_provider_impl_unittest.cc
modified

Files Changed

  • content/browser/device_posture/device_posture_provider_impl_unittest.cc
  • content/browser/device_posture/device_posture_provider_impl.h
  • content/browser/device_posture/device_posture_provider_impl.cc
  • content/test/BUILD.gn

Audit Directions

  • Sensor pushes to hidden pages
    Review other browser-side providers that push device or environment state to renderers (screen and window placement, display or orientation changes, pressure, ambient sensors, gamepad, idle) and check whether they gate dispatch on WebContents visibility or focus.
  • Missing visibility observers
    Look for WebContentsUserData providers that hold a mojo::RemoteSet of clients but do not implement WebContentsObserver::OnVisibilityChanged. Each one may be sending updates to background tabs.
  • Emulation paths bypassing gates
    When a real-signal path gains a visibility or permission gate, check that DevTools emulation and override paths (here OverrideDevicePostureForEmulation) get the same gate and do not dispatch directly.
From a743780616a342356187a6693431edc81a172073 Mon Sep 17 00:00:00 2001
From: Alvin Ji <alvinji@chromium.org>
Date: Tue, 07 Jul 2026 16:09:17 -0700
Subject: [PATCH] Fix Privacy Side-Channel in Device Posture API via Background Tabs

This CL defers device posture change events when the WebContents is
hidden or occluded. DevicePostureProviderImpl now inherits from
WebContentsObserver and observes visibility changes. Updates are only
dispatched when the WebContents is visible.

Bug: 522803735
Change-Id: Ie5c757727cf7787c9968c816e1ff2eb50679a08c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7927408
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
Commit-Queue: Alvin Ji <alvinji@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1658356}
---
diff --git a/content/browser/device_posture/device_posture_provider_impl.cc b/content/browser/device_posture/device_posture_provider_impl.cc
index 6ce8c665..f592bb27 100644
--- a/content/browser/device_posture/device_posture_provider_impl.cc
+++ b/content/browser/device_posture/device_posture_provider_impl.cc
@@ -22,7 +22,8 @@
 }
 
 DevicePostureProviderImpl::DevicePostureProviderImpl(WebContents* web_contents)
-    : WebContentsUserData<DevicePostureProviderImpl>(*web_contents) {
+    : WebContentsUserData<DevicePostureProviderImpl>(*web_contents),
+      WebContentsObserver(web_contents) {
   platform_provider_ = DevicePosturePlatformProvider::Create(web_contents);
   // We need to  listen to disconnections so that if there is nobody interested
   // in posture changes we can shutdown the native backends.
@@ -42,6 +43,13 @@
   return platform_provider_.get();
 }
 
+blink::mojom::DevicePostureType DevicePostureProviderImpl::GetCurrentPosture()
+    const {
+  return (is_posture_emulated_ && emulated_posture_)
+             ? *emulated_posture_
+             : platform_provider_->GetDevicePosture();
+}
+
 void DevicePostureProviderImpl::AddListenerAndGetCurrentPosture(
     mojo::PendingRemote<blink::mojom::DevicePostureClient> client,
     AddListenerAndGetCurrentPostureCallback callback) {
@@ -49,17 +57,27 @@
     platform_provider_->AddObserver(this);
   }
   posture_clients_.Add(std::move(client));
-  blink::mojom::DevicePostureType posture =
-      platform_provider_->GetDevicePosture();
+  blink::mojom::DevicePostureType posture = GetCurrentPosture();
   std::move(callback).Run(posture);
+  last_dispatched_posture_ = posture;
 }
 
 void DevicePostureProviderImpl::OverrideDevicePostureForEmulation(
     blink::mojom::DevicePostureType emulated_posture) {
-  // Notify the related clients about the new posture.
   is_posture_emulated_ = true;
-  for (auto& client : posture_clients_) {
-    client->OnPostureChanged(emulated_posture);
+  emulated_posture_ = emulated_posture;
+  // If the page is hidden, we store the emulated posture but defer dispatching
+  // the event until the page becomes visible again (handled in
+  // OnVisibilityChanged).
+  if (web_contents()->GetVisibility() != Visibility::VISIBLE) {
+    return;
+  }
+  if (!last_dispatched_posture_ ||
+      *last_dispatched_posture_ != emulated_posture) {
+    for (auto& client : posture_clients_) {
+      client->OnPostureChanged(emulated_posture);
+    }
+    last_dispatched_posture_ = emulated_posture;
   }
 }
 
@@ -72,21 +90,52 @@
 
   // Restore the original posture from the platform.
   is_posture_emulated_ = false;
-  for (auto& client : posture_clients_) {
-    client->OnPostureChanged(platform_provider_->GetDevicePosture());
+  emulated_posture_.reset();
+  if (web_contents()->GetVisibility() != Visibility::VISIBLE) {
+    return;
+  }
+  blink::mojom::DevicePostureType posture =
+      platform_provider_->GetDevicePosture();
+  if (!last_dispatched_posture_ || *last_dispatched_posture_ != posture) {
+    for (auto& client : posture_clients_) {
+      client->OnPostureChanged(posture);
+    }
+    last_dispatched_posture_ = posture;
   }
 }
 
 void DevicePostureProviderImpl::OnDevicePostureChanged(
     const blink::mojom::DevicePostureType& posture) {
-  // If we receive a posture change from the platform but we're emulating it we
-  // shouldn't notify the clients.
-  if (is_posture_emulated_) {
+  // If we receive a posture change from the platform but we're emulating it,
+  // or if the page is not visible, we shouldn't notify the clients.
+  if (is_posture_emulated_ ||
+      web_contents()->GetVisibility() != Visibility::VISIBLE) {
     return;
   }
 
-  for (auto& client : posture_clients_) {
-    client->OnPostureChanged(posture);
+  if (!last_dispatched_posture_ || *last_dispatched_posture_ != posture) {
+    for (auto& client : posture_clients_) {
+      client->OnPostureChanged(posture);
+    }
+    last_dispatched_posture_ = posture;
+  }
+}
+
+void DevicePostureProviderImpl::OnVisibilityChanged(Visibility visibility) {
+  if (visibility != Visibility::VISIBLE || posture_clients_.empty()) {
+    return;
+  }
+
+  // When the tab regains visibility, dispatch the latest posture to the
+  // clients if it changed while the tab was hidden.
+  blink::mojom::DevicePostureType current_posture = GetCurrentPosture();
+
+  if (!last_dispatched_posture_ ||
+      *last_dispatched_posture_ != current_posture) {
+    for (auto& client : posture_clients_) {
+      client->OnPostureChanged(current_posture);
+    }
+    last_dispatched_posture_ = current_posture;
   }
 }
 
@@ -95,6 +144,7 @@
   if (posture_clients_.empty()) {
     // We're not interested in receiving posture changes.
     platform_provider_->RemoveObserver(this);
+    last_dispatched_posture_.reset();
   }
 }
 
diff --git a/content/browser/device_posture/device_posture_provider_impl.h b/content/browser/device_posture/device_posture_provider_impl.h
index ce621efe..e91786ae 100644
--- a/content/browser/device_posture/device_posture_provider_impl.h
+++ b/content/browser/device_posture/device_posture_provider_impl.h
@@ -7,6 +7,7 @@
 
 #include "content/browser/device_posture/device_posture_platform_provider.h"
 #include "content/common/content_export.h"
+#include "content/public/browser/web_contents_observer.h"
 #include "content/public/browser/web_contents_user_data.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "mojo/public/cpp/bindings/pending_remote.h"
@@ -16,10 +17,11 @@
 
 namespace content {
 
-class DevicePostureProviderImpl final
+class CONTENT_EXPORT DevicePostureProviderImpl final
     : public blink::mojom::DevicePostureProvider,
       public WebContentsUserData<DevicePostureProviderImpl>,
-      public DevicePosturePlatformProvider::Observer {
+      public DevicePosturePlatformProvider::Observer,
+      public WebContentsObserver {
  public:
   static DevicePostureProviderImpl* GetOrCreate(WebContents*);
   explicit DevicePostureProviderImpl(content::WebContents* web_contents);
@@ -32,15 +34,18 @@
       mojo::PendingReceiver<blink::mojom::DevicePostureProvider> receiver);
   DevicePosturePlatformProvider* platform_provider() const;
 
-  CONTENT_EXPORT void OverrideDevicePostureForEmulation(
+  void OverrideDevicePostureForEmulation(
       blink::mojom::DevicePostureType posture);
-  CONTENT_EXPORT void DisableDevicePostureOverrideForEmulation();
+  void DisableDevicePostureOverrideForEmulation();
 
  private:
   // DevicePostureClient implementation.
   void OnDevicePostureChanged(
       const blink::mojom::DevicePostureType& posture) override;
 
+  // WebContentsObserver implementation.
+  void OnVisibilityChanged(Visibility visibility) override;
+
   // DevicePostureProvider implementation.
   void AddListenerAndGetCurrentPosture(
       mojo::PendingRemote<blink::mojom::DevicePostureClient> client,
@@ -48,8 +53,12 @@
 
   void OnRemoteDisconnect(mojo::RemoteSetElementId);
 
+  blink::mojom::DevicePostureType GetCurrentPosture() const;
+
   std::unique_ptr<DevicePosturePlatformProvider> platform_provider_;
   bool is_posture_emulated_ = false;
+  std::optional<blink::mojom::DevicePostureType> emulated_posture_;
+  std::optional<blink::mojom::DevicePostureType> last_dispatched_posture_;
   mojo::ReceiverSet<blink::mojom::DevicePostureProvider> receivers_;
   mojo::RemoteSet<blink::mojom::DevicePostureClient> posture_clients_;
 
diff --git a/content/browser/device_posture/device_posture_provider_impl_unittest.cc b/content/browser/device_posture/device_posture_provider_impl_unittest.cc
new file mode 100644
index 0000000..ca934abf
--- /dev/null
+++ b/content/browser/device_posture/device_posture_provider_impl_unittest.cc
@@ -0,0 +1,181 @@
+// 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/device_posture/device_posture_provider_impl.h"
+
+#include <optional>
+#include <utility>
+
+#include "base/functional/bind.h"
+#include "base/memory/raw_ptr.h"
+#include "base/test/test_future.h"
+#include "content/public/test/test_renderer_host.h"
+#include "content/test/test_web_contents.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/mojom/device_posture/device_posture_provider.mojom.h"
+
+namespace content {
+
+using ::testing::_;
+
+class MockDevicePostureClient : public blink::mojom::DevicePostureClient {
+ public:
+  MockDevicePostureClient() = default;
+  ~MockDevicePostureClient() override = default;
+
+  mojo::PendingRemote<blink::mojom::DevicePostureClient> BindAndGetRemote() {
+    return receiver_.BindNewPipeAndPassRemote();
+  }
+
+  // blink::mojom::DevicePostureClient:
+  MOCK_METHOD(void,
+              OnPostureChanged,
+              (blink::mojom::DevicePostureType posture),
+              (override));
+
+  void Flush() { receiver_.FlushForTesting(); }
+
+ private:
+  mojo::Receiver<blink::mojom::DevicePostureClient> receiver_{this};
+};
+
+class DevicePostureProviderImplTest : public RenderViewHostTestHarness {
+ public:
+  DevicePostureProviderImplTest() = default;
+
+  void SetUp() override {
+    RenderViewHostTestHarness::SetUp();
+    DevicePostureProviderImpl::GetOrCreate(web_contents());
+  }
+
+  DevicePostureProviderImpl* provider() {
+    return DevicePostureProviderImpl::FromWebContents(web_contents());
+  }
+
+  TestWebContents* test_web_contents() {
+    return static_cast<TestWebContents*>(web_contents());
+  }
+};
+
+TEST_F(DevicePostureProviderImplTest, DeferUpdatesWhileHidden) {
+  MockDevicePostureClient client;
+
+  // Initially visible.
+  test_web_contents()->WasShown();
+
+  // Add listener.
+  base::test::TestFuture<blink::mojom::DevicePostureType> posture_future;
+  blink::mojom::DevicePostureProvider* posture_provider = provider();
+  posture_provider->AddListenerAndGetCurrentPosture(
+      client.BindAndGetRemote(), posture_future.GetCallback());
+  EXPECT_EQ(posture_future.Get(), blink::mojom::DevicePostureType::kContinuous);
+
+  // Trigger posture change while visible.
+  DevicePosturePlatformProvider::Observer* observer = provider();
+  base::test::TestFuture<blink::mojom::DevicePostureType> change_future_1;
+  EXPECT_CALL(client, OnPostureChanged(_))
+      .WillOnce(base::test::InvokeFuture(change_future_1));
+  observer->OnDevicePostureChanged(blink::mojom::DevicePostureType::kFolded);
+  EXPECT_EQ(change_future_1.Take(), blink::mojom::DevicePostureType::kFolded);
+
+  // Hide the web contents.
+  test_web_contents()->WasHidden();
+
+  // Trigger posture change while hidden.
+  EXPECT_CALL(client, OnPostureChanged(_)).Times(0);
+  observer->OnDevicePostureChanged(
+      blink::mojom::DevicePostureType::kContinuous);
+  client.Flush();
+
+  // Show the web contents again.
+  base::test::TestFuture<blink::mojom::DevicePostureType> change_future_2;
+  EXPECT_CALL(client, OnPostureChanged(_))
+      .WillOnce(base::test::InvokeFuture(change_future_2));
+  test_web_contents()->WasShown();
+  EXPECT_EQ(change_future_2.Take(),
+            blink::mojom::DevicePostureType::kContinuous);
+}
+
+TEST_F(DevicePostureProviderImplTest, DeferUpdatesWhileOccluded) {
+  MockDevicePostureClient client;
+
+  // Initially visible.
+  test_web_contents()->WasShown();
+
+  // Add listener.
+  base::test::TestFuture<blink::mojom::DevicePostureType> posture_future;
+  blink::mojom::DevicePostureProvider* posture_provider = provider();
+  posture_provider->AddListenerAndGetCurrentPosture(
+      client.BindAndGetRemote(), posture_future.GetCallback());
+  EXPECT_EQ(posture_future.Get(), blink::mojom::DevicePostureType::kContinuous);
+
+  // Trigger posture change while visible.
+  DevicePosturePlatformProvider::Observer* observer = provider();
+  base::test::TestFuture<blink::mojom::DevicePostureType> change_future_1;
+  EXPECT_CALL(client, OnPostureChanged(_))
+      .WillOnce(base::test::InvokeFuture(change_future_1));
+  observer->OnDevicePostureChanged(blink::mojom::DevicePostureType::kFolded);
+  EXPECT_EQ(change_future_1.Take(), blink::mojom::DevicePostureType::kFolded);
+
+  // Occlude the web contents.
+  test_web_contents()->WasOccluded();
+
+  // Trigger posture change while occluded.
+  EXPECT_CALL(client, OnPostureChanged(_)).Times(0);
+  observer->OnDevicePostureChanged(
+      blink::mojom::DevicePostureType::kContinuous);
+  client.Flush();
+
+  // Show the web contents again.
+  base::test::TestFuture<blink::mojom::DevicePostureType> change_future_2;
+  EXPECT_CALL(client, OnPostureChanged(_))
+      .WillOnce(base::test::InvokeFuture(change_future_2));
+  test_web_contents()->WasShown();
+  EXPECT_EQ(change_future_2.Take(),
+            blink::mojom::DevicePostureType::kContinuous);
+}
+
+TEST_F(DevicePostureProviderImplTest, DeferUpdatesWhileHiddenEmulation) {
+  MockDevicePostureClient client;
+
+  // Initially visible.
+  test_web_contents()->WasShown();
+
+  // Add listener.
+  base::test::TestFuture<blink::mojom::DevicePostureType> posture_future;
+  blink::mojom::DevicePostureProvider* posture_provider = provider();
+  posture_provider->AddListenerAndGetCurrentPosture(
+      client.BindAndGetRemote(), posture_future.GetCallback());
+  EXPECT_EQ(posture_future.Get(), blink::mojom::DevicePostureType::kContinuous);
+
+  // Enable emulation and override.
+  base::test::TestFuture<blink::mojom::DevicePostureType> change_future_1;
+  EXPECT_CALL(client, OnPostureChanged(_))
+      .WillOnce(base::test::InvokeFuture(change_future_1));
+  provider()->OverrideDevicePostureForEmulation(
+      blink::mojom::DevicePostureType::kFolded);
+  EXPECT_EQ(change_future_1.Take(), blink::mojom::DevicePostureType::kFolded);
+
+  // Hide the web contents.
+  test_web_contents()->WasHidden();
+
+  // Override while hidden.
+  EXPECT_CALL(client, OnPostureChanged(_)).Times(0);
+  provider()->OverrideDevicePostureForEmulation(
+      blink::mojom::DevicePostureType::kContinuous);
+  client.Flush();
+
+  // Show the web contents again.
+  base::test::TestFuture<blink::mojom::DevicePostureType> change_future_2;
+  EXPECT_CALL(client, OnPostureChanged(_))
+      .WillOnce(base::test::InvokeFuture(change_future_2));
+  test_web_contents()->WasShown();
+  EXPECT_EQ(change_future_2.Take(),
+            blink::mojom::DevicePostureType::kContinuous);
+}
+
+}  // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 131e19a..e593cfc3 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -2701,6 +2701,7 @@
     "../browser/cpu_performance/cpu_performance_unittest.cc",
     "../browser/declarative_performance_observer/declarative_performance_observer_store_unittest.cc",
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/device_posture/device_posture_provider_impl_unittest.cc b/content/browser/device_posture/device_posture_provider_impl_unittest.cc
new file mode 100644
index 0000000..ca934abf
--- /dev/null
+++ b/content/browser/device_posture/device_posture_provider_impl_unittest.cc
@@ -0,0 +1,181 @@
+// 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/device_posture/device_posture_provider_impl.h"
+
+#include <optional>
+#include <utility>
+
+#include "base/functional/bind.h"
+#include "base/memory/raw_ptr.h"
+#include "base/test/test_future.h"
+#include "content/public/test/test_renderer_host.h"
+#include "content/test/test_web_contents.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/mojom/device_posture/device_posture_provider.mojom.h"
+
+namespace content {
+
+using ::testing::_;
+
+class MockDevicePostureClient : public blink::mojom::DevicePostureClient {
+ public:
+  MockDevicePostureClient() = default;
+  ~MockDevicePostureClient() override = default;
+
+  mojo::PendingRemote<blink::mojom::DevicePostureClient> BindAndGetRemote() {
+    return receiver_.BindNewPipeAndPassRemote();
+  }
+
+  // blink::mojom::DevicePostureClient:
+  MOCK_METHOD(void,
+              OnPostureChanged,
+              (blink::mojom::DevicePostureType posture),
+              (override));
+
+  void Flush() { receiver_.FlushForTesting(); }
+
+ private:
+  mojo::Receiver<blink::mojom::DevicePostureClient> receiver_{this};
+};
+
+class DevicePostureProviderImplTest : public RenderViewHostTestHarness {
+ public:
+  DevicePostureProviderImplTest() = default;
+
+  void SetUp() override {
+    RenderViewHostTestHarness::SetUp();
+    DevicePostureProviderImpl::GetOrCreate(web_contents());
+  }
+
+  DevicePostureProviderImpl* provider() {
+    return DevicePostureProviderImpl::FromWebContents(web_contents());
+  }
+
+  TestWebContents* test_web_contents() {
+    return static_cast<TestWebContents*>(web_contents());
+  }
+};
+
+TEST_F(DevicePostureProviderImplTest, DeferUpdatesWhileHidden) {
+  MockDevicePostureClient client;
+
+  // Initially visible.
+  test_web_contents()->WasShown();
+
+  // Add listener.
+  base::test::TestFuture<blink::mojom::DevicePostureType> posture_future;
+  blink::mojom::DevicePostureProvider* posture_provider = provider();
+  posture_provider->AddListenerAndGetCurrentPosture(
+      client.BindAndGetRemote(), posture_future.GetCallback());
+  EXPECT_EQ(posture_future.Get(), blink::mojom::DevicePostureType::kContinuous);
+
+  // Trigger posture change while visible.
+  DevicePosturePlatformProvider::Observer* observer = provider();
+  base::test::TestFuture<blink::mojom::DevicePostureType> change_future_1;
+  EXPECT_CALL(client, OnPostureChanged(_))
+      .WillOnce(base::test::InvokeFuture(change_future_1));
+  observer->OnDevicePostureChanged(blink::mojom::DevicePostureType::kFolded);
+  EXPECT_EQ(change_future_1.Take(), blink::mojom::DevicePostureType::kFolded);
+
+  // Hide the web contents.
+  test_web_contents()->WasHidden();
+
+  // Trigger posture change while hidden.
+  EXPECT_CALL(client, OnPostureChanged(_)).Times(0);
+  observer->OnDevicePostureChanged(
+      blink::mojom::DevicePostureType::kContinuous);
+  client.Flush();
+
+  // Show the web contents again.
+  base::test::TestFuture<blink::mojom::DevicePostureType> change_future_2;
+  EXPECT_CALL(client, OnPostureChanged(_))
+      .WillOnce(base::test::InvokeFuture(change_future_2));
+  test_web_contents()->WasShown();
+  EXPECT_EQ(change_future_2.Take(),
+            blink::mojom::DevicePostureType::kContinuous);
+}
+
+TEST_F(DevicePostureProviderImplTest, DeferUpdatesWhileOccluded) {
+  MockDevicePostureClient client;
+
+  // Initially visible.
+  test_web_contents()->WasShown();
+
+  // Add listener.
+  base::test::TestFuture<blink::mojom::DevicePostureType> posture_future;
+  blink::mojom::DevicePostureProvider* posture_provider = provider();
+  posture_provider->AddListenerAndGetCurrentPosture(
+      client.BindAndGetRemote(), posture_future.GetCallback());
+  EXPECT_EQ(posture_future.Get(), blink::mojom::DevicePostureType::kContinuous);
+
+  // Trigger posture change while visible.
+  DevicePosturePlatformProvider::Observer* observer = provider();
+  base::test::TestFuture<blink::mojom::DevicePostureType> change_future_1;
+  EXPECT_CALL(client, OnPostureChanged(_))
+      .WillOnce(base::test::InvokeFuture(change_future_1));
+  observer->OnDevicePostureChanged(blink::mojom::DevicePostureType::kFolded);
+  EXPECT_EQ(change_future_1.Take(), blink::mojom::DevicePostureType::kFolded);
+
+  // Occlude the web contents.
+  test_web_contents()->WasOccluded();
+
+  // Trigger posture change while occluded.
+  EXPECT_CALL(client, OnPostureChanged(_)).Times(0);
+  observer->OnDevicePostureChanged(
+      blink::mojom::DevicePostureType::kContinuous);
+  client.Flush();
+
+  // Show the web contents again.
+  base::test::TestFuture<blink::mojom::DevicePostureType> change_future_2;
+  EXPECT_CALL(client, OnPostureChanged(_))
+      .WillOnce(base::test::InvokeFuture(change_future_2));
+  test_web_contents()->WasShown();
+  EXPECT_EQ(change_future_2.Take(),
+            blink::mojom::DevicePostureType::kContinuous);
+}
+
+TEST_F(DevicePostureProviderImplTest, DeferUpdatesWhileHiddenEmulation) {
+  MockDevicePostureClient client;
+
+  // Initially visible.
+  test_web_contents()->WasShown();
+
+  // Add listener.
+  base::test::TestFuture<blink::mojom::DevicePostureType> posture_future;
+  blink::mojom::DevicePostureProvider* posture_provider = provider();
+  posture_provider->AddListenerAndGetCurrentPosture(
+      client.BindAndGetRemote(), posture_future.GetCallback());
+  EXPECT_EQ(posture_future.Get(), blink::mojom::DevicePostureType::kContinuous);
+
+  // Enable emulation and override.
+  base::test::TestFuture<blink::mojom::DevicePostureType> change_future_1;
+  EXPECT_CALL(client, OnPostureChanged(_))
+      .WillOnce(base::test::InvokeFuture(change_future_1));
+  provider()->OverrideDevicePostureForEmulation(
+      blink::mojom::DevicePostureType::kFolded);
+  EXPECT_EQ(change_future_1.Take(), blink::mojom::DevicePostureType::kFolded);
+
+  // Hide the web contents.
+  test_web_contents()->WasHidden();
+
+  // Override while hidden.
+  EXPECT_CALL(client, OnPostureChanged(_)).Times(0);
+  provider()->OverrideDevicePostureForEmulation(
+      blink::mojom::DevicePostureType::kContinuous);
+  client.Flush();
+
+  // Show the web contents again.
+  base::test::TestFuture<blink::mojom::DevicePostureType> change_future_2;
+  EXPECT_CALL(client, OnPostureChanged(_))
+      .WillOnce(base::test::InvokeFuture(change_future_2));
+  test_web_contents()->WasShown();
+  EXPECT_EQ(change_future_2.Take(),
+            blink::mojom::DevicePostureType::kContinuous);
+}
+
+}  // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 131e19a..e593cfc3 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -2701,6 +2701,7 @@
     "../browser/cpu_performance/cpu_performance_unittest.cc",
     "../browser/declarative_performance_observer/declarative_performance_observer_store_unittest.cc",
     "../browser/declarative_performance_observer/declarative_performance_observer_unittest.cc",
+    "../browser/device_posture/device_posture_provider_impl_unittest.cc",
     "../browser/devtools/devtools_background_services_context_impl_unittest.cc",
     "../browser/devtools/devtools_frontend_host_impl_unittest.cc",
     "../browser/devtools/devtools_http_handler_unittest.cc",
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.