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 <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.
References
On This Page