Medium chrome Info Disclosure 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in WebAudio
DescriptionInformation leak in WebAudio
ComponentWebAudio
Sub-componentWebAudio
Bug ClassInfo Disclosure
Tracker514426571
Fix commit0c057c066789 (chromium/src) +114/-33
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/modules/webaudio/audio_playback_stats.cc
modified
TEST_F
third_party/blink/renderer/modules/webaudio/audio_context_test.cc
modified

Files Changed

  • third_party/blink/renderer/modules/webaudio/audio_context_test.cc
  • third_party/blink/renderer/modules/webaudio/audio_playback_stats.cc
From 0c057c066789536845b227a1cf96491af99e328e Mon Sep 17 00:00:00 2001
From: Hongchan Choi <hongchan@google.com>
Date: Tue, 08 Sep 2026 11:47:36 -0700
Subject: [PATCH] [WebAudio] Coarsen time resolution in AudioPlaybackStats

Expose coarsened (clamped) timing metrics in AudioPlaybackStats to
mitigate timing side-channel attacks. Timings are clamped using
Performance::ClampTimeResolution to 100us (or 5us when cross-origin
isolated).

Bug: 514426571
Change-Id: I56818eb7bb5115fb027338f6a96a4f3e453fb79e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7884690
Commit-Queue: Hongchan Choi <hongchan@chromium.org>
Reviewed-by: Fredrik Hernqvist <fhernqvist@google.com>
Cr-Commit-Position: refs/heads/main@{#1693959}
---
diff --git a/third_party/blink/renderer/modules/webaudio/audio_playback_stats.cc b/third_party/blink/renderer/modules/webaudio/audio_playback_stats.cc
index 970d08c..7e37afe 100644
--- a/third_party/blink/renderer/modules/webaudio/audio_playback_stats.cc
+++ b/third_party/blink/renderer/modules/webaudio/audio_playback_stats.cc
@@ -7,17 +7,35 @@
 #include "base/numerics/safe_conversions.h"
 #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
 #include "third_party/blink/renderer/bindings/core/v8/v8_object_builder.h"
+#include "third_party/blink/renderer/core/dom/dom_high_res_time_stamp.h"
 #include "third_party/blink/renderer/core/execution_context/agent.h"
+#include "third_party/blink/renderer/core/execution_context/execution_context.h"
+#include "third_party/blink/renderer/core/timing/performance.h"
 #include "third_party/blink/renderer/platform/wtf/functional.h"
 
 namespace blink {
 
+namespace {
+bool IsCrossOriginIsolated(ScriptState* script_state) {
+  if (!script_state) {
+    return false;
+  }
+  if (ExecutionContext* execution_context =
+          ExecutionContext::From(script_state)) {
+    return execution_context->CrossOriginIsolatedCapability();
+  }
+  return false;
+}
+}  // namespace
+
 AudioPlaybackStats::AudioPlaybackStats(AudioContext* context)
     : context_(context) {}
 
 double AudioPlaybackStats::underrunDuration(ScriptState* script_state) {
   MaybeUpdateStats(script_state);
-  return stats_.glitch_frames_duration().InSecondsF();
+  return ConvertDOMHighResTimeStampToSeconds(
+      Performance::ClampTimeResolution(stats_.glitch_frames_duration(),
+                                       IsCrossOriginIsolated(script_state)));
 }
 
 uint32_t AudioPlaybackStats::underrunEvents(ScriptState* script_state) {
@@ -27,23 +45,31 @@
 
 double AudioPlaybackStats::totalDuration(ScriptState* script_state) {
   MaybeUpdateStats(script_state);
-  return (stats_.glitch_frames_duration() + stats_.observed_frames_duration())
-      .InSecondsF();
+  return ConvertDOMHighResTimeStampToSeconds(
+      Performance::ClampTimeResolution(
+          stats_.glitch_frames_duration() + stats_.observed_frames_duration(),
+          IsCrossOriginIsolated(script_state)));
 }
 
 double AudioPlaybackStats::averageLatency(ScriptState* script_state) {
   MaybeUpdateStats(script_state);
-  return stats_.average_latency().InSecondsF();
+  return ConvertDOMHighResTimeStampToSeconds(
+      Performance::ClampTimeResolution(stats_.average_latency(),
+                                       IsCrossOriginIsolated(script_state)));
 }
 
 double AudioPlaybackStats::minimumLatency(ScriptState* script_state) {
   MaybeUpdateStats(script_state);
-  return stats_.min_latency().InSecondsF();
+  return ConvertDOMHighResTimeStampToSeconds(
+      Performance::ClampTimeResolution(stats_.min_latency(),
+                                       IsCrossOriginIsolated(script_state)));
 }
 
 double AudioPlaybackStats::maximumLatency(ScriptState* script_state) {
   MaybeUpdateStats(script_state);
-  return stats_.max_latency().InSecondsF();
+  return ConvertDOMHighResTimeStampToSeconds(
+      Performance::ClampTimeResolution(stats_.max_latency(),
+                                       IsCrossOriginIsolated(script_state)));
 }
 
 void AudioPlaybackStats::resetLatency(ScriptState* script_state) {
diff --git a/third_party/blink/renderer/modules/webaudio/audio_context_test.cc b/third_party/blink/renderer/modules/webaudio/audio_context_test.cc
index 9122f97..cf84140 100644
--- a/third_party/blink/renderer/modules/webaudio/audio_context_test.cc
+++ b/third_party/blink/renderer/modules/webaudio/audio_context_test.cc
@@ -35,11 +35,13 @@
 #include "third_party/blink/renderer/bindings/modules/v8/v8_union_audiocontextrendersizecategory_unsignedlong.h"
 #include "third_party/blink/renderer/core/core_initializer.h"
 #include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/dom_high_res_time_stamp.h"
 #include "third_party/blink/renderer/core/frame/frame_test_helpers.h"
 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
 #include "third_party/blink/renderer/core/frame/local_frame.h"
 #include "third_party/blink/renderer/core/messaging/message_port.h"
 #include "third_party/blink/renderer/core/testing/page_test_base.h"
+#include "third_party/blink/renderer/core/timing/performance.h"
 #include "third_party/blink/renderer/core/workers/worker_thread.h"
 #include "third_party/blink/renderer/modules/mediastream/sub_capture_target.h"
 #include "third_party/blink/renderer/modules/peerconnection/peer_connection_dependency_factory.h"
@@ -982,28 +984,44 @@
                            base::TimeDelta min_delay,
                            base::TimeDelta max_delay,
                            int source_line) {
+    ExecutionContext* execution_context = ExecutionContext::From(script_state);
+    bool cross_origin_isolated =
+        execution_context ? execution_context->CrossOriginIsolatedCapability()
+                          : false;
+
     EXPECT_EQ(playback_stats->underrunEvents(script_state),
               total_glitches.count)
         << " LINE " << source_line;
-    EXPECT_FLOAT_EQ(playback_stats->underrunDuration(script_state),
-                    total_glitches.duration.InSecondsF())
+    EXPECT_FLOAT_EQ(
+        playback_stats->underrunDuration(script_state),
+        ConvertDOMHighResTimeStampToSeconds(Performance::ClampTimeResolution(
+            total_glitches.duration, cross_origin_isolated)))
         << " LINE " << source_line;
-    EXPECT_FLOAT_EQ(playback_stats->averageLatency(script_state),
-                    average_delay.InSecondsF())
+    EXPECT_FLOAT_EQ(
+        playback_stats->averageLatency(script_state),
+        ConvertDOMHighResTimeStampToSeconds(Performance::ClampTimeResolution(
+            average_delay, cross_origin_isolated)))
         << " LINE " << source_line;
-    EXPECT_FLOAT_EQ(playback_stats->minimumLatency(script_state),
-                    min_delay.InSecondsF())
+    EXPECT_FLOAT_EQ(
+        playback_stats->minimumLatency(script_state),
+        ConvertDOMHighResTimeStampToSeconds(Performance::ClampTimeResolution(
+            min_delay, cross_origin_isolated)))
         << " LINE " << source_line;
-    EXPECT_FLOAT_EQ(playback_stats->maximumLatency(script_state),
-                    max_delay.InSecondsF())
+    EXPECT_FLOAT_EQ(
+        playback_stats->maximumLatency(script_state),
+        ConvertDOMHighResTimeStampToSeconds(Performance::ClampTimeResolution(
+            max_delay, cross_origin_isolated)))
         << " LINE " << source_line;
-    EXPECT_NEAR(
-        playback_stats->totalDuration(script_state),
-        (media::AudioTimestampHelper::FramesToTime(
-             total_processed_frames, platform()->AudioHardwareSampleRate()) +
-         total_glitches.duration)
-            .InSecondsF(),
-        0.00001)
+
+    base::TimeDelta expected_total_duration =
+        media::AudioTimestampHelper::FramesToTime(
+            total_processed_frames, platform()->AudioHardwareSampleRate()) +
+        total_glitches.duration;
+    double coarsened_expected = ConvertDOMHighResTimeStampToSeconds(
+        Performance::ClampTimeResolution(expected_total_duration,
+                                         cross_origin_isolated));
+    EXPECT_NEAR(playback_stats->totalDuration(script_state),
+                coarsened_expected, 0.00011)
         << " LINE " << source_line;
   }
 
@@ -1350,7 +1368,6 @@
 
   AudioPlaybackStats* playback_stats = audio_context->playbackStats();
   int glitches_before = playback_stats->underrunEvents(script_state);
-  double duration_before = playback_stats->totalDuration(script_state);
 
   // 2. Hide the page.
   GetPage().SetVisibilityState(mojom::blink::PageVisibilityState::kHidden,
@@ -1376,17 +1393,22 @@
   // the hidden period.
   int glitches_after = playback_stats->underrunEvents(script_state);
   EXPECT_EQ(glitches_before, glitches_after);
-  double duration_after =
-      duration_before + media::AudioTimestampHelper::FramesToTime(
-                            1000, audio_context->sampleRate())
-                            .InSecondsF();
-  // We use EXPECT_NEAR with a 10 microseconds tolerance to allow for
-  // sub-microsecond rounding errors from integer time conversion in
-  // AudioTimestampHelper::FramesToTime. The tolerance is smaller than 1 audio
-  // frame (~20.8 microseconds at 48kHz), ensuring any actually processed frame
-  // would still trigger a failure.
-  EXPECT_NEAR(playback_stats->totalDuration(script_state),
-              duration_after, 0.00001);
+  ExecutionContext* execution_context = ExecutionContext::From(script_state);
+  bool cross_origin_isolated =
+      execution_context ? execution_context->CrossOriginIsolatedCapability()
+                        : false;
+
+  base::TimeDelta expected_total_duration =
+      media::AudioTimestampHelper::FramesToTime(
+          2000, audio_context->sampleRate()) +
+      base::Milliseconds(10);  // 10ms baseline glitch
+  double duration_after = ConvertDOMHighResTimeStampToSeconds(
+      Performance::ClampTimeResolution(expected_total_duration,
+                                       cross_origin_isolated));
+  // We use EXPECT_NEAR with a 110 microseconds tolerance (0.00011s) to
+  // allow for coarsening grid alignment and sub-microsecond rounding errors.
+  EXPECT_NEAR(playback_stats->totalDuration(script_state), duration_after,
+              0.00011);
 }
 
 TEST_F(AudioContextStatsTest, PlaybackStatsMicrophoneRestrictionStartsDenied) {
@@ -1470,6 +1492,39 @@
   EXPECT_FALSE(audio_context->HasPendingActivity());
 }
 
+TEST_F(AudioContextStatsTest, PlaybackStatsCoarsening) {
+  blink::WebRuntimeFeatures::EnableFeatureFromString(
+      "AudioContextPlaybackStats", true);
+  AudioContextOptions* options = AudioContextOptions::Create();
+  AudioContext* audio_context = AudioContext::Create(
+      GetFrame().DomWindow(), options, ASSERT_NO_EXCEPTION);
+  audio_context->set_clock_for_testing(this);
+
+  ScriptState* script_state = ToScriptStateForMainWorld(&GetFrame());
+  AudioPlaybackStats* playback_stats = audio_context->playbackStats();
+
+  ContextRenderer* renderer =
+      MakeGarbageCollected<ContextRenderer>(audio_context);
+  renderer->Init();
+
+  // Advance time to allow updates.
+  fake_time_now_ += base::Seconds(1);
+
+  // Render with a very specific "fine" latency: 10.123 ms = 10123 us.
+  base::TimeDelta fine_delay = base::Microseconds(10123);
+  renderer->Render(100, fine_delay, media::AudioGlitchInfo{});
+
+  ToEventLoop(script_state).PerformMicrotaskCheckpoint();
+
+  double max_latency = playback_stats->maximumLatency(script_state);
+  int64_t max_latency_us = std::round(max_latency * 1000000.0);
+
+  // It should be a multiple of 100 us (0.1 ms).
+  EXPECT_EQ(max_latency_us % 100, 0) << "max_latency: " << max_latency;
+  EXPECT_TRUE(max_latency_us == 10100 || max_latency_us == 10200)
+      << "max_latency_us: " << max_latency_us;
+}
+
 // Test that AudioWorklet and its messaging proxy are signaled to terminate
 // when the AudioContext is closed.
 TEST_F(AudioContextTest, AudioWorkletTerminatedOnClose) {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/modules/webaudio/audio_context_test.cc b/third_party/blink/renderer/modules/webaudio/audio_context_test.cc
index 9122f97..cf84140 100644
--- a/third_party/blink/renderer/modules/webaudio/audio_context_test.cc
+++ b/third_party/blink/renderer/modules/webaudio/audio_context_test.cc
@@ -35,11 +35,13 @@
 #include "third_party/blink/renderer/bindings/modules/v8/v8_union_audiocontextrendersizecategory_unsignedlong.h"
 #include "third_party/blink/renderer/core/core_initializer.h"
 #include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/dom_high_res_time_stamp.h"
 #include "third_party/blink/renderer/core/frame/frame_test_helpers.h"
 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
 #include "third_party/blink/renderer/core/frame/local_frame.h"
 #include "third_party/blink/renderer/core/messaging/message_port.h"
 #include "third_party/blink/renderer/core/testing/page_test_base.h"
+#include "third_party/blink/renderer/core/timing/performance.h"
 #include "third_party/blink/renderer/core/workers/worker_thread.h"
 #include "third_party/blink/renderer/modules/mediastream/sub_capture_target.h"
 #include "third_party/blink/renderer/modules/peerconnection/peer_connection_dependency_factory.h"
@@ -982,28 +984,44 @@
                            base::TimeDelta min_delay,
                            base::TimeDelta max_delay,
                            int source_line) {
+    ExecutionContext* execution_context = ExecutionContext::From(script_state);
+    bool cross_origin_isolated =
+        execution_context ? execution_context->CrossOriginIsolatedCapability()
+                          : false;
+
     EXPECT_EQ(playback_stats->underrunEvents(script_state),
               total_glitches.count)
         << " LINE " << source_line;
-    EXPECT_FLOAT_EQ(playback_stats->underrunDuration(script_state),
-                    total_glitches.duration.InSecondsF())
+    EXPECT_FLOAT_EQ(
+        playback_stats->underrunDuration(script_state),
+        ConvertDOMHighResTimeStampToSeconds(Performance::ClampTimeResolution(
+            total_glitches.duration, cross_origin_isolated)))
         << " LINE " << source_line;
-    EXPECT_FLOAT_EQ(playback_stats->averageLatency(script_state),
-                    average_delay.InSecondsF())
+    EXPECT_FLOAT_EQ(
+        playback_stats->averageLatency(script_state),
+        ConvertDOMHighResTimeStampToSeconds(Performance::ClampTimeResolution(
+            average_delay, cross_origin_isolated)))
         << " LINE " << source_line;
-    EXPECT_FLOAT_EQ(playback_stats->minimumLatency(script_state),
-                    min_delay.InSecondsF())
+    EXPECT_FLOAT_EQ(
+        playback_stats->minimumLatency(script_state),
+        ConvertDOMHighResTimeStampToSeconds(Performance::ClampTimeResolution(
+            min_delay, cross_origin_isolated)))
         << " LINE " << source_line;
-    EXPECT_FLOAT_EQ(playback_stats->maximumLatency(script_state),
-                    max_delay.InSecondsF())
+    EXPECT_FLOAT_EQ(
+        playback_stats->maximumLatency(script_state),
+        ConvertDOMHighResTimeStampToSeconds(Performance::ClampTimeResolution(
+            max_delay, cross_origin_isolated)))
         << " LINE " << source_line;
-    EXPECT_NEAR(
-        playback_stats->totalDuration(script_state),
-        (media::AudioTimestampHelper::FramesToTime(
-             total_processed_frames, platform()->AudioHardwareSampleRate()) +
-         total_glitches.duration)
-            .InSecondsF(),
-        0.00001)
+
+    base::TimeDelta expected_total_duration =
+        media::AudioTimestampHelper::FramesToTime(
+            total_processed_frames, platform()->AudioHardwareSampleRate()) +
+        total_glitches.duration;
+    double coarsened_expected = ConvertDOMHighResTimeStampToSeconds(
+        Performance::ClampTimeResolution(expected_total_duration,
+                                         cross_origin_isolated));
+    EXPECT_NEAR(playback_stats->totalDuration(script_state),
+                coarsened_expected, 0.00011)
         << " LINE " << source_line;
   }
 
@@ -1350,7 +1368,6 @@
 
   AudioPlaybackStats* playback_stats = audio_context->playbackStats();
   int glitches_before = playback_stats->underrunEvents(script_state);
-  double duration_before = playback_stats->totalDuration(script_state);
 
   // 2. Hide the page.
   GetPage().SetVisibilityState(mojom::blink::PageVisibilityState::kHidden,
@@ -1376,17 +1393,22 @@
   // the hidden period.
   int glitches_after = playback_stats->underrunEvents(script_state);
   EXPECT_EQ(glitches_before, glitches_after);
-  double duration_after =
-      duration_before + media::AudioTimestampHelper::FramesToTime(
-                            1000, audio_context->sampleRate())
-                            .InSecondsF();
-  // We use EXPECT_NEAR with a 10 microseconds tolerance to allow for
-  // sub-microsecond rounding errors from integer time conversion in
-  // AudioTimestampHelper::FramesToTime. The tolerance is smaller than 1 audio
-  // frame (~20.8 microseconds at 48kHz), ensuring any actually processed frame
-  // would still trigger a failure.
-  EXPECT_NEAR(playback_stats->totalDuration(script_state),
-              duration_after, 0.00001);
+  ExecutionContext* execution_context = ExecutionContext::From(script_state);
+  bool cross_origin_isolated =
+      execution_context ? execution_context->CrossOriginIsolatedCapability()
+                        : false;
+
+  base::TimeDelta expected_total_duration =
+      media::AudioTimestampHelper::FramesToTime(
+          2000, audio_context->sampleRate()) +
+      base::Milliseconds(10);  // 10ms baseline glitch
+  double duration_after = ConvertDOMHighResTimeStampToSeconds(
+      Performance::ClampTimeResolution(expected_total_duration,
+                                       cross_origin_isolated));
+  // We use EXPECT_NEAR with a 110 microseconds tolerance (0.00011s) to
+  // allow for coarsening grid alignment and sub-microsecond rounding errors.
+  EXPECT_NEAR(playback_stats->totalDuration(script_state), duration_after,
+              0.00011);
 }
 
 TEST_F(AudioContextStatsTest, PlaybackStatsMicrophoneRestrictionStartsDenied) {
@@ -1470,6 +1492,39 @@
   EXPECT_FALSE(audio_context->HasPendingActivity());
 }
 
+TEST_F(AudioContextStatsTest, PlaybackStatsCoarsening) {
+  blink::WebRuntimeFeatures::EnableFeatureFromString(
+      "AudioContextPlaybackStats", true);
+  AudioContextOptions* options = AudioContextOptions::Create();
+  AudioContext* audio_context = AudioContext::Create(
+      GetFrame().DomWindow(), options, ASSERT_NO_EXCEPTION);
+  audio_context->set_clock_for_testing(this);
+
+  ScriptState* script_state = ToScriptStateForMainWorld(&GetFrame());
+  AudioPlaybackStats* playback_stats = audio_context->playbackStats();
+
+  ContextRenderer* renderer =
+      MakeGarbageCollected<ContextRenderer>(audio_context);
+  renderer->Init();
+
+  // Advance time to allow updates.
+  fake_time_now_ += base::Seconds(1);
+
+  // Render with a very specific "fine" latency: 10.123 ms = 10123 us.
+  base::TimeDelta fine_delay = base::Microseconds(10123);
+  renderer->Render(100, fine_delay, media::AudioGlitchInfo{});
+
+  ToEventLoop(script_state).PerformMicrotaskCheckpoint();
+
+  double max_latency = playback_stats->maximumLatency(script_state);
+  int64_t max_latency_us = std::round(max_latency * 1000000.0);
+
+  // It should be a multiple of 100 us (0.1 ms).
+  EXPECT_EQ(max_latency_us % 100, 0) << "max_latency: " << max_latency;
+  EXPECT_TRUE(max_latency_us == 10100 || max_latency_us == 10200)
+      << "max_latency_us: " << max_latency_us;
+}
+
 // Test that AudioWorklet and its messaging proxy are signaled to terminate
 // when the AudioContext is closed.
 TEST_F(AudioContextTest, AudioWorkletTerminatedOnClose) {
Loading diff…

Original Bug Report

reported by rj...@google.com

Timing side-channel via uncoarsened AudioPlayoutStats maxLatency

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: The experimental AudioContextPlayoutStats feature exposes high-precision, uncoarsened microsecond timing metrics via maximumLatency and minimumLatency. An attacker can potentially bypass the intended 1Hz rate limit by repeatedly creating new AudioContexts to poll the system audio hardware latency, enabling a timing side-channel attack.

Affected files:

  • third_party/blink/renderer/modules/webaudio/audio_playout_stats.cc

Estimated timestamp from git blame: 2024-05-28

Description

The AudioContextPlayoutStats interface (currently an experimental feature available via Origin Trials or feature flags) provides timing metrics to JavaScript. However, the implementation fails to apply time coarsening (clamping) to the DOMHighResTimeStamp values returned by attributes like maximumLatency, minimumLatency, and fallbackFramesDuration.

In Chromium, web-exposed timings that can leak system state or hardware performance must be coarsened using Performance::ClampTimeResolution (typically to 100 µs, or 5 µs if cross-origin isolated). AudioPlayoutStats bypasses this by converting exact base::TimeDelta objects to doubles using InMillisecondsF() without any clamping.

While the API attempts to mitigate high-frequency polling by restricting updates to 1Hz via the StatsUpdateRestrictor class, this restrictor is instantiated per AudioContext. Because an AudioContext initializes the restrictor’s last_update_time_ to zero, the first render callback will always successfully update the stats. An attacker can repeatedly instantiate new AudioContext objects to poll the uncoarsened hardware delay and internal FIFO state at a high frequency.

Impact

The exposure of microsecond-precision timing metrics, combined with the ability to poll them frequently, provides a high-fidelity signal for timing side-channel attacks. An attacker could potentially leverage this to measure the timing of cross-origin events or system resource contention.

Potential Steps to Reproduce

Note: These are suggested steps based on source code analysis. Our tooling agent cannot run active proof-of-concept exploits.

  1. Ensure the page is visible or has microphone permission to pass the StatsUpdateRestrictor::IsVisibleOrHasPermission() check.
  2. In a loop, create a new AudioContext: const ctx = new AudioContext();.
  3. Wait a very brief period (e.g., using setTimeout) to allow the AudioContext to transition to running and process its first render quantum.
  4. Read the uncoarsened timing data: let maxLat = ctx.playoutStats.maximumLatency;
  5. Close the context (ctx.close()) to avoid exhausting resources, and repeat.
  6. Observe high-resolution fluctuations in hardware latency and FIFO state that correspond to system load.

Source Code References

  • Uncoarsened value exposure: third_party/blink/renderer/modules/webaudio/audio_playout_stats.cc lines 48-52.
  • Hardware delay accumulation: third_party/blink/renderer/platform/audio/audio_destination.cc line 617.
  • Per-context restrictor initialization: third_party/blink/renderer/modules/webaudio/audio_context.cc line 604.
  1. All DOMHighResTimeStamp values returned by the AudioPlayoutStats interface should be coarsened using Performance::ClampTimeResolution to ensure compliance with Chromium’s security policies.
  2. Consider making the 1Hz StatsUpdateRestrictor a page-level or origin-level rate limit, or require the AudioContext to be alive for at least 1 second before exposing the first set of statistics, to prevent the bypass via context cycling.

Evaluated with Chrome root at commit: b7d0c4d810da1b31400f198c70d9720fc8f0e5a0


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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