Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper access control in ComputePressure
DescriptionImproper access control in ComputePressure
ComponentComputePressure
Sub-componentBlink>PerformanceAPIs>ComputePressure
Bug ClassLogic Error
Tracker502710681
Fix commit0a76c06d06f8 (chromium/src) +203/-47
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
SharedRandomizationState
services/device/public/cpp/compute_pressure/cpu_pressure_converter.cc
modified
if
services/device/public/cpp/compute_pressure/cpu_pressure_converter.cc
modified
SharedRandomizationState
services/device/public/cpp/compute_pressure/cpu_pressure_converter.cc
modified
state_thresholds
services/device/public/cpp/compute_pressure/cpu_pressure_converter.cc
modified
SharedRandomizationState
services/device/public/cpp/compute_pressure/cpu_pressure_converter.h
modified
CpuPressureConverter
services/device/public/cpp/compute_pressure/cpu_pressure_converter.h
modified

Files Changed

  • services/device/public/cpp/compute_pressure/cpu_pressure_converter.cc
  • services/device/public/cpp/compute_pressure/cpu_pressure_converter.h
  • services/device/public/cpp/compute_pressure/cpu_pressure_converter_unittest.cc
From 0a76c06d06f89ad8a9fc48e16fa28b7313a7d164 Mon Sep 17 00:00:00 2001
From: Vincent Scheib <scheib@chromium.org>
Date: Tue, 15 Sep 2026 11:14:17 -0700
Subject: [PATCH] compute pressure: Share break-calibration randomization state

Each PressureServiceBase (one per frame / dedicated worker /
shared worker) owns its own CpuPressureConverter with an
independent randomization timer. Once the per-context timers
desynchronize, different execution contexts convert the same
cpu_utilization broadcast against different threshold tables
and report different PressureState values for identical
samples.

Make the randomization timer, interval and active threshold state
shared across all CpuPressureConverter instances via a ref-counted
file-scope timer so that every consumer observes threshold switches
at the same time. Per-instance hysteresis is retained. The shared
timer is created when the first instance enables the mitigation and
destroyed when the last one disables it. Explicit transition methods
(ActivateRandomizedThresholds and ActivateBaseThresholds) are used
to manage threshold state rather than blind toggles.

Add unit tests verifying that converters remain in sync across
transitions and that shared timer lifecycle cleanup operates correctly.

Bug: 502710681
TAG=agy
CONV=5f1afc96-732d-4e1b-8768-6de1b9901961

Change-Id: I3e6498a53e5f2ebfca3bce453977de46d8b57c0b
Fixed: 502710681
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8397199
Reviewed-by: Reilly Grant <reillyg@chromium.org>
Commit-Queue: Vincent Scheib <scheib@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1698248}
---
diff --git a/services/device/public/cpp/compute_pressure/cpu_pressure_converter.cc b/services/device/public/cpp/compute_pressure/cpu_pressure_converter.cc
index 9d8727d..d17e804 100644
--- a/services/device/public/cpp/compute_pressure/cpu_pressure_converter.cc
+++ b/services/device/public/cpp/compute_pressure/cpu_pressure_converter.cc
@@ -4,7 +4,15 @@
 
 #include "services/device/public/cpp/compute_pressure/cpu_pressure_converter.h"
 
+#include <algorithm>
+#include <array>
+
+#include "base/check_op.h"
+#include "base/functional/bind.h"
+#include "base/memory/ref_counted.h"
+#include "base/notreached.h"
 #include "base/rand_util.h"
+#include "base/timer/timer.h"
 
 namespace device {
 
@@ -13,7 +21,7 @@
 // Delta for the state decision hysteresis.
 constexpr double kThresholdDelta = 0.03;
 
-// |randomization_timer_| boundaries in second.
+// |randomization_timer_| boundaries in seconds.
 constexpr uint64_t kMinRandomizationTimeInSeconds = 120;
 constexpr uint64_t kMaxRandomizationTimeInSeconds = 240;
 
@@ -35,11 +43,113 @@
 
 }  // namespace
 
+class SharedRandomizationState;
+
+namespace {
+SharedRandomizationState* g_shared_state = nullptr;
+}  // namespace
+
+// Manages the shared "Break Calibration" privacy mitigation timer and threshold
+// states across all active CpuPressureConverter instances. All
+// CpuPressureConverter instances enabling the mitigation are sequence-affine
+// to the sequence where the shared state is bound.
+//
+// The "Break Calibration" privacy mitigation alternates between base thresholds
+// and randomized thresholds at randomized intervals (120-240 seconds) to
+// prevent calibration attacks by malicious contexts. Starting and stopping the
+// mitigation is requested explicitly via EnableStateRandomizationMitigation()
+// and DisableStateRandomizationMitigation(). While active, the shared state
+// alternates between base and randomized threshold sets using explicit state
+// transitions rather than a blind toggle.
+class SharedRandomizationState
+    : public base::RefCounted<SharedRandomizationState> {
+ public:
+  static scoped_refptr<SharedRandomizationState> CreateOrGet() {
+    if (!g_shared_state) {
+      g_shared_state = new SharedRandomizationState();
+    } else {
+      DCHECK_CALLED_ON_VALID_SEQUENCE(g_shared_state->sequence_checker_);
+    }
+    return base::WrapRefCounted(g_shared_state);
+  }
+
+  SharedRandomizationState(const SharedRandomizationState&) = delete;
+  SharedRandomizationState& operator=(const SharedRandomizationState&) = delete;
+
+  bool is_randomized_thresholds_active() const {
+    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+    return is_randomized_thresholds_active_;
+  }
+
+  base::TimeDelta randomization_time() const {
+    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+    return randomization_time_;
+  }
+
+ private:
+  friend class base::RefCounted<SharedRandomizationState>;
+
+  SharedRandomizationState() {
+    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+    // Start with base thresholds and schedule activation of randomized
+    // thresholds.
+    ScheduleNextTimer(/*activate_randomized=*/true);
+  }
+
+  ~SharedRandomizationState() {
+    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+    randomization_timer_.Stop();
+    DCHECK_EQ(g_shared_state, this);
+    g_shared_state = nullptr;
+  }
+
+  void ActivateRandomizedThresholds() {
+    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+    is_randomized_thresholds_active_ = true;
+    ScheduleNextTimer(/*activate_randomized=*/false);
+  }
+
+  void ActivateBaseThresholds() {
+    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+    is_randomized_thresholds_active_ = false;
+    ScheduleNextTimer(/*activate_randomized=*/true);
+  }
+
+  void ScheduleNextTimer(bool activate_randomized) {
+    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+    randomization_time_ = base::Seconds(base::RandIntInclusive(
+        kMinRandomizationTimeInSeconds, kMaxRandomizationTimeInSeconds));
+    randomization_timer_.Start(
+        FROM_HERE, randomization_time_,
+        base::BindOnce(
+            activate_randomized
+                ? &SharedRandomizationState::ActivateRandomizedThresholds
+                : &SharedRandomizationState::ActivateBaseThresholds,
+            base::Unretained(this)));
+  }
+
+  SEQUENCE_CHECKER(sequence_checker_);
+  base::TimeDelta randomization_time_ GUARDED_BY_CONTEXT(sequence_checker_);
+  base::OneShotTimer randomization_timer_ GUARDED_BY_CONTEXT(sequence_checker_);
+  bool is_randomized_thresholds_active_ GUARDED_BY_CONTEXT(sequence_checker_) =
+      false;
+};
+
+CpuPressureConverter::CpuPressureConverter() = default;
+
+CpuPressureConverter::~CpuPressureConverter() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  DisableStateRandomizationMitigation();
+}
+
 const std::array<double,
                  static_cast<size_t>(mojom::PressureState::kMaxValue) + 1>&
 CpuPressureConverter::state_thresholds() const {
-  return state_randomization_requested_ ? kStateRandomizedThresholds
-                                        : kStateBaseThresholds;
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  return (shared_randomization_state_ &&
+          shared_randomization_state_->is_randomized_thresholds_active())
+             ? kStateRandomizedThresholds
+             : kStateBaseThresholds;
 }
 
 double CpuPressureConverter::hysteresis_threshold_delta() const {
@@ -48,41 +158,36 @@
 
 void CpuPressureConverter::EnableStateRandomizationMitigation() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  randomization_time_ = base::Seconds(base::RandIntInclusive(
-      kMinRandomizationTimeInSeconds, kMaxRandomizationTimeInSeconds));
-  randomization_timer_.Start(
-      FROM_HERE, randomization_time_,
-      base::BindRepeating(&CpuPressureConverter::ToggleStateRandomization,
-                          base::Unretained(this)));
+  if (shared_randomization_state_) {
+    return;
+  }
+  shared_randomization_state_ = SharedRandomizationState::CreateOrGet();
 }
 
-void CpuPressureConverter::DisableStateRandomizationMigitation() {
+void CpuPressureConverter::DisableStateRandomizationMitigation() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  randomization_timer_.Stop();
-  state_randomization_requested_ = false;
+  shared_randomization_state_.reset();
 }
 
-void CpuPressureConverter::ToggleStateRandomization() {
+base::TimeDelta CpuPressureConverter::GetRandomizationTimeForTesting() const {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-
-  state_randomization_requested_ = !state_randomization_requested_;
-  randomization_time_ = base::Seconds(base::RandIntInclusive(
-      kMinRandomizationTimeInSeconds, kMaxRandomizationTimeInSeconds));
-  randomization_timer_.Start(
-      FROM_HERE, randomization_time_,
-      base::BindRepeating(&CpuPressureConverter::ToggleStateRandomization,
-                          base::Unretained(this)));
+  return shared_randomization_state_
+             ? shared_randomization_state_->randomization_time()
+             : base::TimeDelta();
 }
 
 mojom::PressureState CpuPressureConverter::CalculateState(
     const double cpu_utilization) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   // TODO(crbug.com/40231044): A more advanced algorithm that calculates
   // PressureState using PressureSample needs to be determined.
   // At this moment the algorithm is the simplest possible
   // with thresholds defining the state.
-  const auto& kStateThresholds = state_randomization_requested_
-                                     ? kStateRandomizedThresholds
-                                     : kStateBaseThresholds;
+  const auto& kStateThresholds =
+      (shared_randomization_state_ &&
+       shared_randomization_state_->is_randomized_thresholds_active())
+          ? kStateRandomizedThresholds
+          : kStateBaseThresholds;
 
   auto it = std::ranges::lower_bound(kStateThresholds, cpu_utilization);
   if (it == kStateThresholds.end()) {
diff --git a/services/device/public/cpp/compute_pressure/cpu_pressure_converter.h b/services/device/public/cpp/compute_pressure/cpu_pressure_converter.h
index 3999d8f3..f85ac7f 100644
--- a/services/device/public/cpp/compute_pressure/cpu_pressure_converter.h
+++ b/services/device/public/cpp/compute_pressure/cpu_pressure_converter.h
@@ -7,20 +7,24 @@
 
 #include <array>
 
+#include "base/memory/scoped_refptr.h"
 #include "base/sequence_checker.h"
-#include "base/timer/timer.h"
+#include "base/thread_annotations.h"
+#include "base/time/time.h"
 #include "services/device/public/mojom/pressure_update.mojom-shared.h"
 
 namespace device {
 
+class SharedRandomizationState;
+
 class CpuPressureConverter final {
  public:
-  CpuPressureConverter() = default;
+  CpuPressureConverter();
 
   CpuPressureConverter(const CpuPressureConverter&) = delete;
   CpuPressureConverter& operator=(const CpuPressureConverter&) = delete;
 
-  ~CpuPressureConverter() = default;
+  ~CpuPressureConverter();
 
   // Returns the current thresholds being used for each mojom::PressureState,
   // taking state randomization into account.
@@ -34,35 +38,22 @@
 
   void EnableStateRandomizationMitigation();
 
-  void DisableStateRandomizationMigitation();
+  void DisableStateRandomizationMitigation();
 
-  base::TimeDelta GetRandomizationTimeForTesting() const {
-    return randomization_time_;
-  }
+  base::TimeDelta GetRandomizationTimeForTesting() const;
 
   // Calculate PressureState based on cpu_utilization.
   // The range is between 0.0 and 1.0.
   mojom::PressureState CalculateState(const double cpu_utilization);
 
  private:
-  // Implements the "break calibration" mitigation by toggling the
-  // |state_randomization_requested_| flag every |randomization_time_|
-  // interval.
-  void ToggleStateRandomization();
-
   SEQUENCE_CHECKER(sequence_checker_);
 
-  // Variable storing |randomization_timer_| time.
-  base::TimeDelta randomization_time_;
-
-  // Drive randomization interval by invoking `ToggleStateRandomization()`.
-  base::OneShotTimer randomization_timer_ GUARDED_BY_CONTEXT(sequence_checker_);
-
-  // Flag to indicate that state randomization has been requested.
-  bool state_randomization_requested_ = false;
+  scoped_refptr<SharedRandomizationState> shared_randomization_state_
+      GUARDED_BY_CONTEXT(sequence_checker_);
 
   // Last state stored as index instead of value.
-  size_t last_state_index_ =
+  size_t last_state_index_ GUARDED_BY_CONTEXT(sequence_checker_) =
       static_cast<size_t>(mojom::PressureState::kNominal);
 };
 
diff --git a/services/device/public/cpp/compute_pressure/cpu_pressure_converter_unittest.cc b/services/device/public/cpp/compute_pressure/cpu_pressure_converter_unittest.cc
index 95c8b0a..12d177a4 100644
--- a/services/device/public/cpp/compute_pressure/cpu_pressure_converter_unittest.cc
+++ b/services/device/public/cpp/compute_pressure/cpu_pressure_converter_unittest.cc
@@ -4,10 +4,13 @@
 
 #include "services/device/public/cpp/compute_pressure/cpu_pressure_converter.h"
 
+#include <memory>
+
 #include "base/sequence_checker.h"
 #include "base/test/gtest_util.h"
 #include "base/test/task_environment.h"
 #include "base/test/test_timeouts.h"
+#include "base/time/time.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest-spi.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -162,14 +165,71 @@
   EXPECT_THAT(converter_.CalculateState(0.86),
               mojom::PressureState(mojom::PressureState::kSerious));
 
-  // First toggling.
+  // First transition: switch to randomized thresholds.
   task_environment_.FastForwardBy(converter_.GetRandomizationTimeForTesting());
   EXPECT_THAT(converter_.CalculateState(0.86),
               mojom::PressureState(mojom::PressureState::kCritical));
-  // Second toggling.
+  // Second transition: switch back to base thresholds.
   task_environment_.FastForwardBy(converter_.GetRandomizationTimeForTesting());
   EXPECT_THAT(converter_.CalculateState(0.86),
               mojom::PressureState(mojom::PressureState::kSerious));
 }
 
+TEST_F(CpuPressureConverterTest,
+       BreakCalibrationMitigationSharedAcrossInstances) {
+  CpuPressureConverter converter2;
+
+  converter_.EnableStateRandomizationMitigation();
+  converter2.EnableStateRandomizationMitigation();
+
+  // Utilization of 0.55 lies in the divergence band:
+  // - Base thresholds: 0.55 <= 0.6 -> kNominal
+  // - Randomized thresholds: 0.55 > 0.5 and <= 0.8 -> kFair
+
+  // Initially, both converters are in base threshold state.
+  EXPECT_EQ(converter_.CalculateState(0.55), mojom::PressureState::kNominal);
+  EXPECT_EQ(converter2.CalculateState(0.55), mojom::PressureState::kNominal);
+
+  // First transition: both converters synchronously switch to randomized
+  // thresholds.
+  task_environment_.FastForwardBy(converter_.GetRandomizationTimeForTesting());
+  EXPECT_EQ(converter_.CalculateState(0.55), mojom::PressureState::kFair);
+  EXPECT_EQ(converter2.CalculateState(0.55), mojom::PressureState::kFair);
+
+  // Second transition: both converters synchronously switch back to base
+  // thresholds.
+  task_environment_.FastForwardBy(converter_.GetRandomizationTimeForTesting());
+  EXPECT_EQ(converter_.CalculateState(0.55), mojom::PressureState::kNominal);
+  EXPECT_EQ(converter2.CalculateState(0.55), mojom::PressureState::kNominal);
+}
+
+TEST_F(CpuPressureConverterTest,
+       BreakCalibrationMitigationSharedStateLifecycle) {
+  auto converter1 = std::make_unique<CpuPressureConverter>();
+  auto converter2 = std::make_unique<CpuPressureConverter>();
+
+  converter1->EnableStateRandomizationMitigation();
+  converter2->EnableStateRandomizationMitigation();
+
+  // Advance time partially.
+  task_environment_.FastForwardBy(base::Seconds(30));
+
+  // Destroy converter1. converter2 must remain valid and keep the shared timer
+  // alive.
+  converter1.reset();
+  EXPECT_EQ(converter2->CalculateState(0.55), mojom::PressureState::kNominal);
+
+  // Destroy converter2. All references are released; g_shared_state must be
+  // cleaned up.
+  converter2.reset();
+
+  // A newly created converter starts cleanly with base thresholds and a new
+  // timer.
+  CpuPressureConverter converter3;
+  converter3.EnableStateRandomizationMitigation();
+  EXPECT_EQ(converter3.CalculateState(0.55), mojom::PressureState::kNominal);
+  task_environment_.FastForwardBy(converter3.GetRandomizationTimeForTesting());
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/services/device/public/cpp/compute_pressure/cpu_pressure_converter_unittest.cc b/services/device/public/cpp/compute_pressure/cpu_pressure_converter_unittest.cc
index 95c8b0a..12d177a4 100644
--- a/services/device/public/cpp/compute_pressure/cpu_pressure_converter_unittest.cc
+++ b/services/device/public/cpp/compute_pressure/cpu_pressure_converter_unittest.cc
@@ -4,10 +4,13 @@
 
 #include "services/device/public/cpp/compute_pressure/cpu_pressure_converter.h"
 
+#include <memory>
+
 #include "base/sequence_checker.h"
 #include "base/test/gtest_util.h"
 #include "base/test/task_environment.h"
 #include "base/test/test_timeouts.h"
+#include "base/time/time.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest-spi.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -162,14 +165,71 @@
   EXPECT_THAT(converter_.CalculateState(0.86),
               mojom::PressureState(mojom::PressureState::kSerious));
 
-  // First toggling.
+  // First transition: switch to randomized thresholds.
   task_environment_.FastForwardBy(converter_.GetRandomizationTimeForTesting());
   EXPECT_THAT(converter_.CalculateState(0.86),
               mojom::PressureState(mojom::PressureState::kCritical));
-  // Second toggling.
+  // Second transition: switch back to base thresholds.
   task_environment_.FastForwardBy(converter_.GetRandomizationTimeForTesting());
   EXPECT_THAT(converter_.CalculateState(0.86),
               mojom::PressureState(mojom::PressureState::kSerious));
 }
 
+TEST_F(CpuPressureConverterTest,
+       BreakCalibrationMitigationSharedAcrossInstances) {
+  CpuPressureConverter converter2;
+
+  converter_.EnableStateRandomizationMitigation();
+  converter2.EnableStateRandomizationMitigation();
+
+  // Utilization of 0.55 lies in the divergence band:
+  // - Base thresholds: 0.55 <= 0.6 -> kNominal
+  // - Randomized thresholds: 0.55 > 0.5 and <= 0.8 -> kFair
+
+  // Initially, both converters are in base threshold state.
+  EXPECT_EQ(converter_.CalculateState(0.55), mojom::PressureState::kNominal);
+  EXPECT_EQ(converter2.CalculateState(0.55), mojom::PressureState::kNominal);
+
+  // First transition: both converters synchronously switch to randomized
+  // thresholds.
+  task_environment_.FastForwardBy(converter_.GetRandomizationTimeForTesting());
+  EXPECT_EQ(converter_.CalculateState(0.55), mojom::PressureState::kFair);
+  EXPECT_EQ(converter2.CalculateState(0.55), mojom::PressureState::kFair);
+
+  // Second transition: both converters synchronously switch back to base
+  // thresholds.
+  task_environment_.FastForwardBy(converter_.GetRandomizationTimeForTesting());
+  EXPECT_EQ(converter_.CalculateState(0.55), mojom::PressureState::kNominal);
+  EXPECT_EQ(converter2.CalculateState(0.55), mojom::PressureState::kNominal);
+}
+
+TEST_F(CpuPressureConverterTest,
+       BreakCalibrationMitigationSharedStateLifecycle) {
+  auto converter1 = std::make_unique<CpuPressureConverter>();
+  auto converter2 = std::make_unique<CpuPressureConverter>();
+
+  converter1->EnableStateRandomizationMitigation();
+  converter2->EnableStateRandomizationMitigation();
+
+  // Advance time partially.
+  task_environment_.FastForwardBy(base::Seconds(30));
+
+  // Destroy converter1. converter2 must remain valid and keep the shared timer
+  // alive.
+  converter1.reset();
+  EXPECT_EQ(converter2->CalculateState(0.55), mojom::PressureState::kNominal);
+
+  // Destroy converter2. All references are released; g_shared_state must be
+  // cleaned up.
+  converter2.reset();
+
+  // A newly created converter starts cleanly with base thresholds and a new
+  // timer.
+  CpuPressureConverter converter3;
+  converter3.EnableStateRandomizationMitigation();
+  EXPECT_EQ(converter3.CalculateState(0.55), mojom::PressureState::kNominal);
+  task_environment_.FastForwardBy(converter3.GetRandomizationTimeForTesting());
+  EXPECT_EQ(converter3.CalculateState(0.55), mojom::PressureState::kFair);
+}
+
 }  // namespace device
Loading diff…

Original Bug Report

reported by rj...@google.com

Privacy mitigation bypass in Compute Pressure API via multiple workers

Flapjack, 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 without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.

Overview: The ‘Break Calibration’ privacy mitigation in the Compute Pressure API can be bypassed because state randomization is managed independently per execution context. An attacker can spawn multiple workers and correlate their reported states to de-randomize the thresholds and infer precise system CPU utilization.

Affected files:

  • content/browser/compute_pressure/pressure_service_base.h
  • content/browser/compute_pressure/pressure_service_base.cc
  • services/device/public/cpp/compute_pressure/cpu_pressure_converter.cc

Estimated timestamp from git blame: 2026-01-21

Summary

The Compute Pressure API implements a “Break Calibration Mitigation” to prevent sites from using high-precision CPU load measurements for side-channel attacks or hardware fingerprinting. This mitigation works by periodically and randomly toggling the thresholds used to map raw CPU utilization to a discrete PressureState (“nominal”, “fair”, etc.).

However, the randomization state and its timer are instantiated independently for every execution context (e.g., iframe, Dedicated Worker, Shared Worker). Because these timers desynchronize, an attacker can spawn multiple same-origin workers, observe their divergent states for the exact same underlying global CPU load, and deduce which thresholds are active for each worker. This completely neutralizes the privacy mitigation.

Technical Details

  1. Per-Context Instantiation: When a JS context creates a PressureObserver and calls observe(), the browser process creates a subclass of content::PressureServiceBase (e.g., PressureServiceForDedicatedWorker).
  2. Independent Randomization: Each PressureServiceBase initializes its own private device::CpuPressureConverter (content/browser/compute_pressure/pressure_service_base.h:108).
  3. Desynchronized Timers: The constructor calls converter_.EnableStateRandomizationMitigation(), which starts an independent base::OneShotTimer with a random interval between 120 and 240 seconds (services/device/public/cpp/compute_pressure/cpu_pressure_converter.cc:51).
  4. Data Broadcast: The Device Service (device::ProbesManager) samples the global system CPU load and broadcasts the exact same raw cpu_utilization value to all active PressureServiceBase clients simultaneously.
  5. Divergent States: Upon receiving the broadcast, each PressureServiceBase calculates the state using its own CpuPressureConverter. Because the random timers have desynchronized across different workers, some converters will use kStateBaseThresholds while others use kStateRandomizedThresholds for the exact same sample.

Potential Attack Steps

(Note: These are suggested steps; a working proof-of-concept has not been executed yet.)

  1. A malicious webpage spawns 50 Dedicated Web Workers.
  2. Each worker starts observing CPU pressure and posts the resulting PressureState strings to the main thread.
  3. The main thread correlates the states. For a given CPU sample, if worker A reports “nominal” (base threshold < 0.6) and worker B reports “fair” (randomized threshold > 0.5), the attacker mathematically deduces the active threshold state for both workers.
  4. The attacker selects a worker whose threshold state is known and stable, and uses it to perform a high-precision active calibration side-channel attack, bypassing the intended API design constraints.

Suggested Fix

The randomization state should be synchronized globally rather than managed per-client.

One approach is to move the randomization timer and the state_randomization_requested_ flag into a centralized location (e.g., in the Device Service alongside ProbesManager, or a singleton in the Browser process) and broadcast the active threshold flag alongside the raw cpu_utilization. CpuPressureConverter would then use this shared flag for its state calculation, ensuring that all execution contexts observe threshold toggles at the exact same time.

Evaluated with Chrome root at commit: c0eb5541aebfa4ea08806eaf6e94bcc69f87ab2f


Results 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.

View on issue tracker