Chrome · Media
CVE-2026-113314
UAF in Media
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
MEDIA_EXPORTmedia/base/wall_clock_time_source.h |
modified | |
TEST_Fmedia/base/wall_clock_time_source_unittest.cc |
modified |
Files Changed
media/base/media_switches.ccmedia/base/wall_clock_time_source.ccmedia/base/wall_clock_time_source.hmedia/base/wall_clock_time_source_unittest.ccmedia/base/media_switches.h
Patch
From dac329323ebaba6deda1496d7e619d5085e1dda9 Mon Sep 17 00:00:00 2001 From: Dale Curtis <[email protected]> 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 <[email protected]> Reviewed-by: Michael Wilson <[email protected]> Auto-Submit: Dale Curtis <[email protected]> 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.
References
On This Page