CVE-2026-113057
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
SharedRandomizationStateservices/device/public/cpp/compute_pressure/cpu_pressure_converter.cc |
modified | |
ifservices/device/public/cpp/compute_pressure/cpu_pressure_converter.cc |
modified | |
SharedRandomizationStateservices/device/public/cpp/compute_pressure/cpu_pressure_converter.cc |
modified | |
state_thresholdsservices/device/public/cpp/compute_pressure/cpu_pressure_converter.cc |
modified | |
SharedRandomizationStateservices/device/public/cpp/compute_pressure/cpu_pressure_converter.h |
modified | |
CpuPressureConverterservices/device/public/cpp/compute_pressure/cpu_pressure_converter.h |
modified |
Files Changed
services/device/public/cpp/compute_pressure/cpu_pressure_converter.ccservices/device/public/cpp/compute_pressure/cpu_pressure_converter.hservices/device/public/cpp/compute_pressure/cpu_pressure_converter_unittest.cc
Patch
From 0a76c06d06f89ad8a9fc48e16fa28b7313a7d164 Mon Sep 17 00:00:00 2001 From: Vincent Scheib <[email protected]> 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 <[email protected]> Commit-Queue: Vincent Scheib <[email protected]> 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)
Regression Test / PoC
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
Original Bug Report
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.hcontent/browser/compute_pressure/pressure_service_base.ccservices/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
- Per-Context Instantiation: When a JS context creates a
PressureObserverand callsobserve(), the browser process creates a subclass ofcontent::PressureServiceBase(e.g.,PressureServiceForDedicatedWorker). - Independent Randomization: Each
PressureServiceBaseinitializes its own privatedevice::CpuPressureConverter(content/browser/compute_pressure/pressure_service_base.h:108). - Desynchronized Timers: The constructor calls
converter_.EnableStateRandomizationMitigation(), which starts an independentbase::OneShotTimerwith a random interval between 120 and 240 seconds (services/device/public/cpp/compute_pressure/cpu_pressure_converter.cc:51). - Data Broadcast: The Device Service (
device::ProbesManager) samples the global system CPU load and broadcasts the exact same rawcpu_utilizationvalue to all activePressureServiceBaseclients simultaneously. - Divergent States: Upon receiving the broadcast, each
PressureServiceBasecalculates the state using its ownCpuPressureConverter. Because the random timers have desynchronized across different workers, some converters will usekStateBaseThresholdswhile others usekStateRandomizedThresholdsfor the exact same sample.
Potential Attack Steps
(Note: These are suggested steps; a working proof-of-concept has not been executed yet.)
- A malicious webpage spawns 50 Dedicated Web Workers.
- Each worker starts observing CPU pressure and posts the resulting
PressureStatestrings to the main thread. - 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.
- 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.