Low chrome UAF 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Media
DescriptionUse after free in Media
ComponentMedia
Bug ClassUAF
Tracker562970433
Fix commitdac329323eba (chromium/src) +164/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
MEDIA_EXPORT
media/base/wall_clock_time_source.h
modified
TEST_F
media/base/wall_clock_time_source_unittest.cc
modified

Files Changed

  • media/base/media_switches.cc
  • media/base/wall_clock_time_source.cc
  • media/base/wall_clock_time_source.h
  • media/base/wall_clock_time_source_unittest.cc
  • media/base/media_switches.h
From dac329323ebaba6deda1496d7e619d5085e1dda9 Mon Sep 17 00:00:00 2001
From: Dale Curtis <dalecurtis@chromium.org>
Date: Wed, 23 Sep 2026 16:49:55 -0700
Subject: [PATCH] Coarsen WallClockTimeSource::CurrentMediaTime resolution.

WallClockTimeSource interpolates media time directly from
base::TickClock (base::TimeTicks::Now()) at 1 microsecond granularity.
For media without an audio track (such as muted video-only playback),
this uncoarsened time was exposed directly to script via
HTMLMediaElement.currentTime on every microtask checkpoint, bypassing
TimeClamper timer coarsening mitigations (100 microseconds
non-cross-origin-isolated).

This CL coarsens the media time returned by
WallClockTimeSource::CurrentMediaTime() to 100 microseconds, matching
blink::TimeClamper's coarse resolution:
- Clamps the returned time to at least the seek time to prevent backward
  time jumps immediately following a seek.
- Paused time remains coarsened to avoid leaking the pause timestamp.

Internal interpolation state (reference_time_, base_timestamp_, and
CurrentMediaTime_Locked()) remains exact so that video frame scheduling
via GetWallClockTimes() and pause/rate changes do not accumulate
rounding drift.

A base::Feature killswitch (kCoarseWallClockTimeSource) in
media_switches allows remotely disabling this coarsening if
compatibility issues arise.

Fixed: 562970433
Change-Id: I4763f4419dfe2722bbebf95d00ea2eaeb45a0fa5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8431624
Commit-Queue: Dale Curtis <dalecurtis@chromium.org>
Reviewed-by: Michael Wilson <mjwilson@chromium.org>
Auto-Submit: Dale Curtis <dalecurtis@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1703921}
---
diff --git a/media/base/media_switches.cc b/media/base/media_switches.cc
index fd2e15c..b962e38 100644
--- a/media/base/media_switches.cc
+++ b/media/base/media_switches.cc
@@ -482,6 +482,10 @@
 BASE_FEATURE(kClearPipCachedBoundsWhenPermissionPromptVisible,
              base::FEATURE_ENABLED_BY_DEFAULT);
 
+// Killswitch for coarsening CurrentMediaTime() to prevent high-resolution
+// timing attacks (crbug.com/562970433). Remove after M157 reaches stable.
+BASE_FEATURE(kCoarseWallClockTimeSource, base::FEATURE_ENABLED_BY_DEFAULT);
+
 // Enables the "Copy Video Frame" context menu item.
 BASE_FEATURE(kContextMenuCopyVideoFrame, base::FEATURE_ENABLED_BY_DEFAULT);
 
diff --git a/media/base/media_switches.h b/media/base/media_switches.h
index 0575ed2..7eb81498 100644
--- a/media/base/media_switches.h
+++ b/media/base/media_switches.h
@@ -173,6 +173,7 @@
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kCdmThreadPriorityElevation);
 MEDIA_EXPORT BASE_DECLARE_FEATURE(
     kClearPipCachedBoundsWhenPermissionPromptVisible);
+MEDIA_EXPORT BASE_DECLARE_FEATURE(kCoarseWallClockTimeSource);
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kContextMenuCopyVideoFrame);
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kContextMenuSaveVideoFrameAs);
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kContextMenuSearchForVideoFrame);
diff --git a/media/base/wall_clock_time_source.cc b/media/base/wall_clock_time_source.cc
index 7fab242e..fb76f39 100644
--- a/media/base/wall_clock_time_source.cc
+++ b/media/base/wall_clock_time_source.cc
@@ -4,7 +4,11 @@
 
 #include "media/base/wall_clock_time_source.h"
 
+#include <algorithm>
+
+#include "base/feature_list.h"
 #include "base/logging.h"
+#include "media/base/media_switches.h"
 
 namespace media {
 
@@ -51,12 +55,18 @@
   base::AutoLock auto_lock(lock_);
   CHECK(!ticking_);
   base_timestamp_ = time;
+  seek_time_ = time;
   reference_time_ = base::TimeTicks();
 }
 
 base::TimeDelta WallClockTimeSource::CurrentMediaTime() {
   base::AutoLock auto_lock(lock_);
-  return CurrentMediaTime_Locked();
+  const base::TimeDelta current_time = CurrentMediaTime_Locked();
+  if (base::FeatureList::IsEnabled(kCoarseWallClockTimeSource)) {
+    return std::max(seek_time_,
+                    current_time.FloorToMultiple(kCoarseResolution));
+  }
+  return current_time;
 }
 
 bool WallClockTimeSource::GetWallClockTimes(
diff --git a/media/base/wall_clock_time_source.h b/media/base/wall_clock_time_source.h
index 785e355..eeda3cb3 100644
--- a/media/base/wall_clock_time_source.h
+++ b/media/base/wall_clock_time_source.h
@@ -17,6 +17,12 @@
 // A time source that uses interpolation based on the system clock.
 class MEDIA_EXPORT WallClockTimeSource : public TimeSource {
  public:
+  // Resolution to which CurrentMediaTime() is coarsened to prevent
+  // high-resolution timing attacks (e.g. crbug.com/562970433) when media lacks
+  // an audio track. Matches blink::TimeClamper's coarse resolution of 100
+  // microseconds.
+  static constexpr base::TimeDelta kCoarseResolution = base::Microseconds(100);
+
   WallClockTimeSource();
 
   WallClockTimeSource(const WallClockTimeSource&) = delete;
@@ -53,6 +59,7 @@
   // that time by the playback rate.
   double playback_rate_ GUARDED_BY(lock_) = 1.0;
   base::TimeDelta base_timestamp_ GUARDED_BY(lock_);
+  base::TimeDelta seek_time_ GUARDED_BY(lock_);
   base::TimeTicks reference_time_ GUARDED_BY(lock_);
 
   // TODO(scherkus): Remove internal locking from this class after access to
diff --git a/media/base/wall_clock_time_source_unittest.cc b/media/base/wall_clock_time_source_unittest.cc
index d66420e2..50590bc 100644
--- a/media/base/wall_clock_time_source_unittest.cc
+++ b/media/base/wall_clock_time_source_unittest.cc
@@ -2,11 +2,14 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "media/base/wall_clock_time_source.h"
+
 #include <memory>
 
+#include "base/test/scoped_feature_list.h"
 #include "base/test/simple_test_tick_clock.h"
 #include "base/time/time.h"
-#include "media/base/wall_clock_time_source.h"
+#include "media/base/media_switches.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace media {
@@ -179,4 +182,141 @@
   EXPECT_EQ(base::TimeTicks(), wall_clock_times[0]);
 }
 
+TEST_F(WallClockTimeSourceTest, CoarseMediaTimeWhenTicking) {
+  time_source_.StartTicking();
+
+  // Initially 0.
+  EXPECT_EQ(base::TimeDelta(), time_source_.CurrentMediaTime());
+
+  // Advance by less than kCoarseResolution (e.g., 50 us); should floor to 0.
+  tick_clock_->Advance(base::Microseconds(50));
+  EXPECT_EQ(base::TimeDelta(), time_source_.CurrentMediaTime());
+
+  // Advance up to 99 us; should still floor to 0.
+  tick_clock_->Advance(base::Microseconds(49));
+  EXPECT_EQ(base::TimeDelta(), time_source_.CurrentMediaTime());
+
+  // Advance to 100 us; now reaches kCoarseResolution.
+  tick_clock_->Advance(base::Microseconds(1));
+  EXPECT_EQ(WallClockTimeSource::kCoarseResolution,
+            time_source_.CurrentMediaTime());
+
+  // Advance to 199 us; still 100 us.
+  tick_clock_->Advance(base::Microseconds(99));
+  EXPECT_EQ(WallClockTimeSource::kCoarseResolution,
+            time_source_.CurrentMediaTime());
+
+  // Advance to 200 us; becomes 200 us.
+  tick_clock_->Advance(base::Microseconds(1));
+  EXPECT_EQ(WallClockTimeSource::kCoarseResolution * 2,
+            time_source_.CurrentMediaTime());
+}
+
+TEST_F(WallClockTimeSourceTest, CoarseMediaTimeWhenStopped) {
+  time_source_.StartTicking();
+  tick_clock_->Advance(base::Microseconds(150));
+  time_source_.StopTicking();
+
+  // Stopped at 150 us; CurrentMediaTime() should be floored to 100 us.
+  EXPECT_EQ(WallClockTimeSource::kCoarseResolution,
+            time_source_.CurrentMediaTime());
+}
+
+TEST_F(WallClockTimeSourceTest, CoarseMediaTimeKillSwitch) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndDisableFeature(kCoarseWallClockTimeSource);
+
+  time_source_.StartTicking();
+  tick_clock_->Advance(base::Microseconds(50));
+
+  // With feature disabled, CurrentMediaTime() returns exact microsecond time.
+  EXPECT_EQ(base::Microseconds(50), time_source_.CurrentMediaTime());
+}
+
+TEST_F(WallClockTimeSourceTest, CoarseMediaTimeSeekOffset) {
+  // Seek to an unaligned timestamp (150 us).
+  time_source_.SetMediaTime(base::Microseconds(150));
+
+  // While stopped, CurrentMediaTime() should not jump backwards below the seek
+  // target.
+  EXPECT_EQ(base::Microseconds(150), time_source_.CurrentMediaTime());
+
+  time_source_.StartTicking();
+
+  // Elapsed 40 us (media time 190 us); still clamped to seek time.
+  tick_clock_->Advance(base::Microseconds(40));
+  EXPECT_EQ(base::Microseconds(150), time_source_.CurrentMediaTime());
+
+  // Elapsed 50 us (media time 200 us); reaches the next coarse boundary.
+  tick_clock_->Advance(base::Microseconds(10));
+  EXPECT_EQ(base::Microseconds(200), time_source_.CurrentMediaTime());
+
+  // Elapsed 149 us (media time 299 us); stays at 200 us.
+  tick_clock_->Advance(base::Microseconds(99));
+  EXPECT_EQ(base::Microseconds(200), time_source_.CurrentMediaTime());
+
+  // Elapsed 150 us (media time 300 us); reaches 300 us.
+  tick_clock_->Advance(base::Microseconds(1));
+  EXPECT_EQ(base::Microseconds(300), time_source_.CurrentMediaTime());
+
+  // Stop ticking at 350 us. Paused time should be coarsened to 300 us rather
+  // than leaking the exact uncoarsened pause time.
+  tick_clock_->Advance(base::Microseconds(50));
+  time_source_.StopTicking();
+  EXPECT_EQ(base::Microseconds(300), time_source_.CurrentMediaTime());
+}
+
+TEST_F(WallClockTimeSourceTest, CoarseMediaTimePlaybackRate) {
+  // At playback rate 2.0, media time advances at 2x wall clock. Coarsening
+  // resolution remains fixed at 100 us in media time.
+  time_source_.SetPlaybackRate(2.0);
+  time_source_.StartTicking();
+
+  // Wall-clock advance 49 us -> media time 98 us (< 100 us coarse boundary).
+  tick_clock_->Advance(base::Microseconds(49));
+  EXPECT_EQ(base::TimeDelta(), time_source_.CurrentMediaTime());
+
+  // Wall-clock advance 1 us -> media time 100 us (reaches boundary).
+  tick_clock_->Advance(base::Microseconds(1));
+  EXPECT_EQ(base::Microseconds(100), time_source_.CurrentMediaTime());
+
+  // Wall-clock advance 49 us -> media time 198 us (< 200 us coarse boundary).
+  tick_clock_->Advance(base::Microseconds(49));
+  EXPECT_EQ(base::Microseconds(100), time_source_.CurrentMediaTime());
+
+  // Wall-clock advance 1 us -> media time 200 us.
+  tick_clock_->Advance(base::Microseconds(1));
+  EXPECT_EQ(base::Microseconds(200), time_source_.CurrentMediaTime());
+
+  time_source_.StopTicking();
+
+  // Test high playback rate 16.0: media time advances 16x faster.
+  time_source_.SetMediaTime(base::TimeDelta());
+  time_source_.SetPlaybackRate(16.0);
+  time_source_.StartTicking();
+
+  // Wall-clock advance 6 us -> media time 96 us (< 100 us coarse boundary).
+  tick_clock_->Advance(base::Microseconds(6));
+  EXPECT_EQ(base::TimeDelta(), time_source_.CurrentMediaTime());
+
+  // Wall-clock advance 1 us (total 7 us wall clock) -> media time 112 us.
+  tick_clock_->Advance(base::Microseconds(1));
+  EXPECT_EQ(base::Microseconds(100), time_source_.CurrentMediaTime());
+
+  time_source_.StopTicking();
+
+  // Test rate 0.5: media time advances at half the rate of wall clock.
+  time_source_.SetMediaTime(base::TimeDelta());
+  time_source_.SetPlaybackRate(0.5);
+  time_source_.StartTicking();
+
+  // Wall-clock advance 100 us -> media time 50 us (< 100 us coarse boundary).
+  tick_clock_->Advance(base::Microseconds(100));
+  EXPECT_EQ(base::TimeDelta(), time_source_.CurrentMediaTime());
+
+  // Wall-clock advance 100 us (total 200 us wall clock) -> media time 100 us.
+  tick_clock_->Advance(base::Microseconds(100));
+  EXPECT_EQ(base::Microseconds(100), time_source_.CurrentMediaTime());
+}
+
 }  // namespace media
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/media/base/wall_clock_time_source_unittest.cc b/media/base/wall_clock_time_source_unittest.cc
index d66420e2..50590bc 100644
--- a/media/base/wall_clock_time_source_unittest.cc
+++ b/media/base/wall_clock_time_source_unittest.cc
@@ -2,11 +2,14 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "media/base/wall_clock_time_source.h"
+
 #include <memory>
 
+#include "base/test/scoped_feature_list.h"
 #include "base/test/simple_test_tick_clock.h"
 #include "base/time/time.h"
-#include "media/base/wall_clock_time_source.h"
+#include "media/base/media_switches.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace media {
@@ -179,4 +182,141 @@
   EXPECT_EQ(base::TimeTicks(), wall_clock_times[0]);
 }
 
+TEST_F(WallClockTimeSourceTest, CoarseMediaTimeWhenTicking) {
+  time_source_.StartTicking();
+
+  // Initially 0.
+  EXPECT_EQ(base::TimeDelta(), time_source_.CurrentMediaTime());
+
+  // Advance by less than kCoarseResolution (e.g., 50 us); should floor to 0.
+  tick_clock_->Advance(base::Microseconds(50));
+  EXPECT_EQ(base::TimeDelta(), time_source_.CurrentMediaTime());
+
+  // Advance up to 99 us; should still floor to 0.
+  tick_clock_->Advance(base::Microseconds(49));
+  EXPECT_EQ(base::TimeDelta(), time_source_.CurrentMediaTime());
+
+  // Advance to 100 us; now reaches kCoarseResolution.
+  tick_clock_->Advance(base::Microseconds(1));
+  EXPECT_EQ(WallClockTimeSource::kCoarseResolution,
+            time_source_.CurrentMediaTime());
+
+  // Advance to 199 us; still 100 us.
+  tick_clock_->Advance(base::Microseconds(99));
+  EXPECT_EQ(WallClockTimeSource::kCoarseResolution,
+            time_source_.CurrentMediaTime());
+
+  // Advance to 200 us; becomes 200 us.
+  tick_clock_->Advance(base::Microseconds(1));
+  EXPECT_EQ(WallClockTimeSource::kCoarseResolution * 2,
+            time_source_.CurrentMediaTime());
+}
+
+TEST_F(WallClockTimeSourceTest, CoarseMediaTimeWhenStopped) {
+  time_source_.StartTicking();
+  tick_clock_->Advance(base::Microseconds(150));
+  time_source_.StopTicking();
+
+  // Stopped at 150 us; CurrentMediaTime() should be floored to 100 us.
+  EXPECT_EQ(WallClockTimeSource::kCoarseResolution,
+            time_source_.CurrentMediaTime());
+}
+
+TEST_F(WallClockTimeSourceTest, CoarseMediaTimeKillSwitch) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndDisableFeature(kCoarseWallClockTimeSource);
+
+  time_source_.StartTicking();
+  tick_clock_->Advance(base::Microseconds(50));
+
+  // With feature disabled, CurrentMediaTime() returns exact microsecond time.
+  EXPECT_EQ(base::Microseconds(50), time_source_.CurrentMediaTime());
+}
+
+TEST_F(WallClockTimeSourceTest, CoarseMediaTimeSeekOffset) {
+  // Seek to an unaligned timestamp (150 us).
+  time_source_.SetMediaTime(base::Microseconds(150));
+
+  // While stopped, CurrentMediaTime() should not jump backwards below the seek
+  // target.
+  EXPECT_EQ(base::Microseconds(150), time_source_.CurrentMediaTime());
+
+  time_source_.StartTicking();
+
+  // Elapsed 40 us (media time 190 us); still clamped to seek time.
+  tick_clock_->Advance(base::Microseconds(40));
+  EXPECT_EQ(base::Microseconds(150), time_source_.CurrentMediaTime());
+
+  // Elapsed 50 us (media time 200 us); reaches the next coarse boundary.
+  tick_clock_->Advance(base::Microseconds(10));
+  EXPECT_EQ(base::Microseconds(200), time_source_.CurrentMediaTime());
+
+  // Elapsed 149 us (media time 299 us); stays at 200 us.
+  tick_clock_->Advance(base::Microseconds(99));
+  EXPECT_EQ(base::Microseconds(200), time_source_.CurrentMediaTime());
+
+  // Elapsed 150 us (media time 300 us); reaches 300 us.
+  tick_clock_->Advance(base::Microseconds(1));
+  EXPECT_EQ(base::Microseconds(300), time_source_.CurrentMediaTime());
+
+  // Stop ticking at 350 us. Paused time should be coarsened to 300 us rather
+  // than leaking the exact uncoarsened pause time.
+  tick_clock_->Advance(base::Microseconds(50));
+  time_source_.StopTicking();
+  EXPECT_EQ(base::Microseconds(300), time_source_.CurrentMediaTime());
+}
+
+TEST_F(WallClockTimeSourceTest, CoarseMediaTimePlaybackRate) {
+  // At playback rate 2.0, media time advances at 2x wall clock. Coarsening
+  // resolution remains fixed at 100 us in media time.
+  time_source_.SetPlaybackRate(2.0);
+  time_source_.StartTicking();
+
+  // Wall-clock advance 49 us -> media time 98 us (< 100 us coarse boundary).
+  tick_clock_->Advance(base::Microseconds(49));
+  EXPECT_EQ(base::TimeDelta(), time_source_.CurrentMediaTime());
+
+  // Wall-clock advance 1 us -> media time 100 us (reaches boundary).
+  tick_clock_->Advance(base::Microseconds(1));
+  EXPECT_EQ(base::Microseconds(100), time_source_.CurrentMediaTime());
+
+  // Wall-clock advance 49 us -> media time 198 us (< 200 us coarse boundary).
+  tick_clock_->Advance(base::Microseconds(49));
+  EXPECT_EQ(base::Microseconds(100), time_source_.CurrentMediaTime());
+
+  // Wall-clock advance 1 us -> media time 200 us.
+  tick_clock_->Advance(base::Microseconds(1));
+  EXPECT_EQ(base::Microseconds(200), time_source_.CurrentMediaTime());
+
+  time_source_.StopTicking();
+
+  // Test high playback rate 16.0: media time advances 16x faster.
+  time_source_.SetMediaTime(base::TimeDelta());
+  time_source_.SetPlaybackRate(16.0);
+  time_source_.StartTicking();
+
+  // Wall-clock advance 6 us -> media time 96 us (< 100 us coarse boundary).
+  tick_clock_->Advance(base::Microseconds(6));
+  EXPECT_EQ(base::TimeDelta(), time_source_.CurrentMediaTime());
+
+  // Wall-clock advance 1 us (total 7 us wall clock) -> media time 112 us.
+  tick_clock_->Advance(base::Microseconds(1));
+  EXPECT_EQ(base::Microseconds(100), time_source_.CurrentMediaTime());
+
+  time_source_.StopTicking();
+
+  // Test rate 0.5: media time advances at half the rate of wall clock.
+  time_source_.SetMediaTime(base::TimeDelta());
+  time_source_.SetPlaybackRate(0.5);
+  time_source_.StartTicking();
+
+  // Wall-clock advance 100 us -> media time 50 us (< 100 us coarse boundary).
+  tick_clock_->Advance(base::Microseconds(100));
+  EXPECT_EQ(base::TimeDelta(), time_source_.CurrentMediaTime());
+
+  // Wall-clock advance 100 us (total 200 us wall clock) -> media time 100 us.
+  tick_clock_->Advance(base::Microseconds(100));
+  EXPECT_EQ(base::Microseconds(100), time_source_.CurrentMediaTime());
+}
+
 }  // namespace media
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.