Chrome · Actor
CVE-2026-79090
Logic Error in Actor
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
IN_PROC_BROWSER_TEST_Fchrome/browser/actor/tools/media_control_tool_browsertest.cc |
modified | |
IN_PROC_BROWSER_TEST_Fchrome/browser/glic/host/glic_actor_media_control_tool_interactive_uitest.cc |
modified | |
ifchrome/browser/ui/webui/ai_overlay_dialog/tools/tools.cc |
modified | |
ifchrome/test/data/actor/media.html |
modified |
Files Changed
chrome/browser/actor/tools/media_control_tool.ccchrome/browser/actor/tools/media_control_tool_browsertest.ccchrome/browser/glic/host/glic_actor_media_control_tool_interactive_uitest.ccchrome/browser/ui/webui/ai_overlay_dialog/tools/tools.ccchrome/test/data/actor/media.html
Patch
From 01dc0c8102c05be7b6fb72a311513dfb2d3959d7 Mon Sep 17 00:00:00 2001 From: Yiren Wang <[email protected]> Date: Mon, 29 Jun 2026 17:02:30 -0700 Subject: [PATCH] Use SuspendType of kSystem for media session MediaControlTool and AI Overlay WebUI tools currently control media sessions using SuspendType::kUI, but SuspendType::kUI is reserved for physical human actions and unconditionally grants user activation gesture on the page. This CL switches to use SuspendType::kSystem to suppress transient user activation, and updates the corresponding tests. The downside of using SuspendType::kSystem is that it will skip the media action handlers registered by the sites. Bug: 517673944 Change-Id: I6197826843c79962bb351a843b518cd47d1d308a Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8013147 Commit-Queue: Yiren Wang <[email protected]> Reviewed-by: David Bokan <[email protected]> Cr-Commit-Position: refs/heads/main@{#1654478} --- diff --git a/chrome/browser/actor/tools/media_control_tool.cc b/chrome/browser/actor/tools/media_control_tool.cc index 8129df27..27968a4 100644 --- a/chrome/browser/actor/tools/media_control_tool.cc +++ b/chrome/browser/actor/tools/media_control_tool.cc @@ -61,11 +61,11 @@ absl::Overload( [media_session](const PlayMedia& arg) { // Resume media playback. - media_session->Resume(content::MediaSession::SuspendType::kUI); + media_session->Resume(content::MediaSession::SuspendType::kSystem); }, [media_session](const PauseMedia& arg) { // Suspend media playback. - media_session->Suspend(content::MediaSession::SuspendType::kUI); + media_session->Suspend(content::MediaSession::SuspendType::kSystem); }, [media_session](const SeekMedia& arg) { // Seek to a specific time in the media. diff --git a/chrome/browser/actor/tools/media_control_tool_browsertest.cc b/chrome/browser/actor/tools/media_control_tool_browsertest.cc index 426383f8..5a58a90 100644 --- a/chrome/browser/actor/tools/media_control_tool_browsertest.cc +++ b/chrome/browser/actor/tools/media_control_tool_browsertest.cc @@ -61,7 +61,7 @@ MakeMediaControlRequest(*active_tab(), PauseMedia()); actor_task().Act(ToRequestList(pause_request), pause_result.GetCallback()); ExpectOkResult(pause_result); - EXPECT_EQ("pause", content::EvalJs(web_contents(), "event_log.join(',')")); + EXPECT_EQ(true, content::EvalJs(web_contents(), "waitForEvent('pause')")); // Play the media. ActResultFuture play_result; @@ -69,8 +69,7 @@ MakeMediaControlRequest(*active_tab(), PlayMedia()); actor_task().Act(ToRequestList(play_request), play_result.GetCallback()); ExpectOkResult(play_result); - EXPECT_EQ("pause,play", - content::EvalJs(web_contents(), "event_log.join(',')")); + EXPECT_EQ(true, content::EvalJs(web_contents(), "waitForEvent('play')")); } IN_PROC_BROWSER_TEST_F(ActorMediaControlToolBrowserTest, SeekMedia) { @@ -78,8 +77,13 @@ ASSERT_TRUE(content::NavigateToURL(web_contents(), url)); ASSERT_TRUE(WaitForLoadStop(web_contents())); - // Start playback. + // Start playback to initialize media session. ASSERT_TRUE(content::ExecJs(web_contents(), "play()")); + EXPECT_EQ(true, content::EvalJs(web_contents(), "waitForEvent('play')")); + + // Pause it so the currentTime doesn't drift during seek. + ASSERT_TRUE(content::ExecJs(web_contents(), "video.pause()")); + EXPECT_EQ(true, content::EvalJs(web_contents(), "waitForEvent('pause')")); // Seek the media. ActResultFuture result; @@ -93,7 +97,10 @@ ToRequestList(request, request_negative_time, request_unreachable_time), result.GetCallback()); ExpectOkResult(result); - EXPECT_EQ("seek 1", content::EvalJs(web_contents(), "event_log.join(',')")); + EXPECT_EQ(true, content::EvalJs(web_contents(), "waitForSeek(1.0)")); + EXPECT_EQ( + 1.0, + content::EvalJs(web_contents(), "video.currentTime").ExtractDouble()); } } // namespace diff --git a/chrome/browser/glic/host/glic_actor_media_control_tool_interactive_uitest.cc b/chrome/browser/glic/host/glic_actor_media_control_tool_interactive_uitest.cc index 7d448ae..86ef34f9 100644 --- a/chrome/browser/glic/host/glic_actor_media_control_tool_interactive_uitest.cc +++ b/chrome/browser/glic/host/glic_actor_media_control_tool_interactive_uitest.cc @@ -50,12 +50,11 @@ InitializeWithOpenGlicWindow(), StartActorTaskInNewTab(url, kNewActorTabId), ExecuteJs(kNewActorTabId, "play"), + WaitForJsResult(kNewActorTabId, "() => waitForEvent('play')"), MediaControlAction(actor::PauseMedia()), - WaitForJsResult(kNewActorTabId, "() => { return event_log.join(','); }", - "pause"), + WaitForJsResult(kNewActorTabId, "() => waitForEvent('pause')"), MediaControlAction(actor::PlayMedia()), - WaitForJsResult(kNewActorTabId, "() => { return event_log.join(','); }", - "pause,play")); + WaitForJsResult(kNewActorTabId, "() => waitForEvent('play')")); } IN_PROC_BROWSER_TEST_F(GlicActorMediaControlToolUiTest, SeekMedia) { @@ -65,9 +64,11 @@ InitializeWithOpenGlicWindow(), StartActorTaskInNewTab(url, kNewActorTabId), ExecuteJs(kNewActorTabId, "play"), + WaitForJsResult(kNewActorTabId, "() => waitForEvent('play')"), + ExecuteJs(kNewActorTabId, "() => { video.pause(); }"), + WaitForJsResult(kNewActorTabId, "() => waitForEvent('pause')"), MediaControlAction(actor::SeekMedia{.seek_time_milliseconds = 1000}), - WaitForJsResult(kNewActorTabId, "() => { return event_log.join(','); }", - "seek 1")); + WaitForJsResult(kNewActorTabId, "() => waitForSeek(1.0)")); } } // namespace diff --git a/chrome/browser/ui/webui/ai_overlay_dialog/tools/tools.cc b/chrome/browser/ui/webui/ai_overlay_dialog/tools/tools.cc index aa2a688..34d34d01 100644 --- a/chrome/browser/ui/webui/ai_overlay_dialog/tools/tools.cc +++ b/chrome/browser/ui/webui/ai_overlay_dialog/tools/tools.cc @@ -350,7 +350,7 @@ content::MediaSession* media_session = content::MediaSession::GetIfExists(contents); if (media_session) { - media_session->Resume(content::MediaSession::SuspendType::kUI); + media_session->Resume(content::MediaSession::SuspendType::kSystem); std::move(callback).Run(std::monostate()); } else { std::move(callback).Run(base::unexpected("No active media session")); @@ -369,7 +369,7 @@ content::MediaSession* media_session = content::MediaSession::GetIfExists(contents); if (media_session) { - media_session->Suspend(content::MediaSession::SuspendType::kUI); + media_session->Suspend(content::MediaSession::SuspendType::kSystem); std::move(callback).Run(std::monostate()); } else { std::move(callback).Run(base::unexpected("No active media session")); diff --git a/chrome/test/data/actor/media.html b/chrome/test/data/actor/media.html index 118ab9d..ff120f87 100644 --- a/chrome/test/data/actor/media.html +++ b/chrome/test/data/actor/media.html @@ -9,26 +9,36 @@ </video> <script> const video = document.getElementById('video'); - let event_log = []; - setupActionHandlers(); // Called by the test to start media playback. function play() { video.play(); } - function setupActionHandlers() { - navigator.mediaSession.setActionHandler('play', _ => { - video.play(); - event_log.push('play'); + // Helper function used by tests to wait for playback state transitions. + // Returns `true` immediately if the video is already in the expected state. + // Otherwise, returns a Promise that resolves when the corresponding event is fired. + function waitForEvent(eventName) { + if (eventName === 'pause' && video.paused) { + return true; + } + if (eventName === 'play' && !video.paused) { + return true; + } + return new Promise(resolve => { + video.addEventListener(eventName, () => resolve(true), {once: true}); }); - navigator.mediaSession.setActionHandler('pause', _ => { - video.pause(); - event_log.push('pause'); - }); - navigator.mediaSession.setActionHandler('seekto', (details) => { - video.currentTime = details.seekTime; - event_log.push('seek ' + details.seekTime); + } + + // Helper function used by tests to wait for seek transitions. + // Returns `true` immediately if the video has already seeked to `targetTime`. + // Otherwise, returns a Promise that resolves when the next native 'seeked' event fires. + function waitForSeek(targetTime) { + if (Math.abs(video.currentTime - targetTime) < 0.1) { + return true; + } + return new Promise(resolve => { + video.addEventListener('seeked', () => resolve(true), {once: true}); }); }
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/actor/tools/media_control_tool_browsertest.cc b/chrome/browser/actor/tools/media_control_tool_browsertest.cc
index 426383f8..5a58a90 100644
--- a/chrome/browser/actor/tools/media_control_tool_browsertest.cc
+++ b/chrome/browser/actor/tools/media_control_tool_browsertest.cc
@@ -61,7 +61,7 @@
MakeMediaControlRequest(*active_tab(), PauseMedia());
actor_task().Act(ToRequestList(pause_request), pause_result.GetCallback());
ExpectOkResult(pause_result);
- EXPECT_EQ("pause", content::EvalJs(web_contents(), "event_log.join(',')"));
+ EXPECT_EQ(true, content::EvalJs(web_contents(), "waitForEvent('pause')"));
// Play the media.
ActResultFuture play_result;
@@ -69,8 +69,7 @@
MakeMediaControlRequest(*active_tab(), PlayMedia());
actor_task().Act(ToRequestList(play_request), play_result.GetCallback());
ExpectOkResult(play_result);
- EXPECT_EQ("pause,play",
- content::EvalJs(web_contents(), "event_log.join(',')"));
+ EXPECT_EQ(true, content::EvalJs(web_contents(), "waitForEvent('play')"));
}
IN_PROC_BROWSER_TEST_F(ActorMediaControlToolBrowserTest, SeekMedia) {
@@ -78,8 +77,13 @@
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
ASSERT_TRUE(WaitForLoadStop(web_contents()));
- // Start playback.
+ // Start playback to initialize media session.
ASSERT_TRUE(content::ExecJs(web_contents(), "play()"));
+ EXPECT_EQ(true, content::EvalJs(web_contents(), "waitForEvent('play')"));
+
+ // Pause it so the currentTime doesn't drift during seek.
+ ASSERT_TRUE(content::ExecJs(web_contents(), "video.pause()"));
+ EXPECT_EQ(true, content::EvalJs(web_contents(), "waitForEvent('pause')"));
// Seek the media.
ActResultFuture result;
@@ -93,7 +97,10 @@
ToRequestList(request, request_negative_time, request_unreachable_time),
result.GetCallback());
ExpectOkResult(result);
- EXPECT_EQ("seek 1", content::EvalJs(web_contents(), "event_log.join(',')"));
+ EXPECT_EQ(true, content::EvalJs(web_contents(), "waitForSeek(1.0)"));
+ EXPECT_EQ(
+ 1.0,
+ content::EvalJs(web_contents(), "video.currentTime").ExtractDouble());
}
} // namespace
diff --git a/chrome/browser/glic/host/glic_actor_media_control_tool_interactive_uitest.cc b/chrome/browser/glic/host/glic_actor_media_control_tool_interactive_uitest.cc
index 7d448ae..86ef34f9 100644
--- a/chrome/browser/glic/host/glic_actor_media_control_tool_interactive_uitest.cc
+++ b/chrome/browser/glic/host/glic_actor_media_control_tool_interactive_uitest.cc
@@ -50,12 +50,11 @@
InitializeWithOpenGlicWindow(),
StartActorTaskInNewTab(url, kNewActorTabId),
ExecuteJs(kNewActorTabId, "play"),
+ WaitForJsResult(kNewActorTabId, "() => waitForEvent('play')"),
MediaControlAction(actor::PauseMedia()),
- WaitForJsResult(kNewActorTabId, "() => { return event_log.join(','); }",
- "pause"),
+ WaitForJsResult(kNewActorTabId, "() => waitForEvent('pause')"),
MediaControlAction(actor::PlayMedia()),
- WaitForJsResult(kNewActorTabId, "() => { return event_log.join(','); }",
- "pause,play"));
+ WaitForJsResult(kNewActorTabId, "() => waitForEvent('play')"));
}
IN_PROC_BROWSER_TEST_F(GlicActorMediaControlToolUiTest, SeekMedia) {
@@ -65,9 +64,11 @@
InitializeWithOpenGlicWindow(),
StartActorTaskInNewTab(url, kNewActorTabId),
ExecuteJs(kNewActorTabId, "play"),
+ WaitForJsResult(kNewActorTabId, "() => waitForEvent('play')"),
+ ExecuteJs(kNewActorTabId, "() => { video.pause(); }"),
+ WaitForJsResult(kNewActorTabId, "() => waitForEvent('pause')"),
MediaControlAction(actor::SeekMedia{.seek_time_milliseconds = 1000}),
- WaitForJsResult(kNewActorTabId, "() => { return event_log.join(','); }",
- "seek 1"));
+ WaitForJsResult(kNewActorTabId, "() => waitForSeek(1.0)"));
}
} // namespace
diff --git a/chrome/test/data/actor/media.html b/chrome/test/data/actor/media.html
index 118ab9d..ff120f87 100644
--- a/chrome/test/data/actor/media.html
+++ b/chrome/test/data/actor/media.html
@@ -9,26 +9,36 @@
</video>
<script>
const video = document.getElementById('video');
- let event_log = [];
- setupActionHandlers();
// Called by the test to start media playback.
function play() {
video.play();
}
- function setupActionHandlers() {
- navigator.mediaSession.setActionHandler('play', _ => {
- video.play();
- event_log.push('play');
+ // Helper function used by tests to wait for playback state transitions.
+ // Returns `true` immediately if the video is already in the expected state.
+ // Otherwise, returns a Promise that resolves when the corresponding event is fired.
+ function waitForEvent(eventName) {
+ if (eventName === 'pause' && video.paused) {
+ return true;
+ }
+ if (eventName === 'play' && !video.paused) {
+ return true;
+ }
+ return new Promise(resolve => {
+ video.addEventListener(eventName, () => resolve(true), {once: true});
});
- navigator.mediaSession.setActionHandler('pause', _ => {
- video.pause();
- event_log.push('pause');
- });
- navigator.mediaSession.setActionHandler('seekto', (details) => {
- video.currentTime = details.seekTime;
- event_log.push('seek ' + details.seekTime);
+ }
+
+ // Helper function used by tests to wait for seek transitions.
+ // Returns `true` immediately if the video has already seeked to `targetTime`.
+ // Otherwise, returns a Promise that resolves when the next native 'seeked' event fires.
+ function waitForSeek(targetTime) {
+ if (Math.abs(video.currentTime - targetTime) < 0.1) {
+ return true;
+ }
+ return new Promise(resolve => {
+ video.addEventListener('seeked', () => resolve(true), {once: true});
});
}
</script>
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