Chrome · Session
CVE-2026-17961
Logic Error in Session
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/android/java/src/org/chromium/chrome/browser/media/FullscreenVideoPictureInPictureController.java |
modified |
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/media/FullscreenVideoPictureInPictureController.javachrome/android/javatests/src/org/chromium/chrome/browser/media/ui/MediaSessionTest.javachrome/android/junit/src/org/chromium/chrome/browser/media/FullscreenVideoPictureInPictureControllerUnitTest.javacomponents/browser_ui/media/android/java/src/org/chromium/components/browser_ui/media/MediaSessionHelper.javacontent/browser/media/session/media_session_android.cccontent/browser/media/session/media_session_android.hcontent/public/android/BUILD.gn
Patch
From 7d4d340cc45fc1660e1423c126b8be0d1a9dead9 Mon Sep 17 00:00:00 2001
From: Phil Yan <philyan@chromium.org>
Date: Fri, 05 Jun 2026 16:50:38 -0700
Subject: [PATCH] [Media] Thread SuspendType through MediaSession JNI
When PauseMediaOnSystemSleepAndroid is enabled on Android, system sleep
triggers programmatic media suspension. However, the JNI layer
unconditionally forwarded this with SuspendType::kUI (UI-initiated).
This propagation incorrectly granted user activation to the renderer
frame, bypassing the user gesture security boundary.
This CL resolves the issue by:
1. Exposing SuspendType parameter to the Java MediaSession API.
2. Updating JNI bridge to accept and pass the suspend type.
3. Passing SuspendType.SYSTEM when pausing on system sleep.
4. Adding an integration test assertion verifying that user activation
remains false after screen wake suspension.
Bug: 517700791
Fix: 517700791
Change-Id: Ief307e3d03227c28db26676c8f4358c3d8badc44
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7904458
Reviewed-by: Ted Choc <tedchoc@chromium.org>
Commit-Queue: Phil Yan <philyan@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1642677}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/FullscreenVideoPictureInPictureController.java b/chrome/android/java/src/org/chromium/chrome/browser/media/FullscreenVideoPictureInPictureController.java
index 10c1f99..fc3e602 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/media/FullscreenVideoPictureInPictureController.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/FullscreenVideoPictureInPictureController.java
@@ -35,6 +35,7 @@
import org.chromium.content_public.browser.MediaSession;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsObserver;
+import org.chromium.media_session.mojom.MediaSession.SuspendType;
import org.chromium.ui.base.WindowAndroid;
import java.lang.annotation.Retention;
@@ -306,7 +307,7 @@
// `stashed`. Also note that `mIsSuspendedForStash` can be reset elsewhere, to help to
// account for missed events from Android.
if (mIsPlaying && stashed && !mIsSuspendedForStash) {
- mediaSession.suspend();
+ mediaSession.suspend(SuspendType.UI);
mIsSuspendedForStash = true;
} else if (!mIsPlaying && !stashed && mIsSuspendedForStash) {
// Don't resume if we didn't pause it on the transition into stash. For example, don't
@@ -635,7 +636,7 @@
final MediaSession mediaSession = getMediaSession();
if (mediaSession != null && mIsPlaying) {
- mediaSession.suspend();
+ mediaSession.suspend(SuspendType.UI);
}
}
}
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/media/ui/MediaSessionTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/media/ui/MediaSessionTest.java
index 9bfdc0fa..3768c0455 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/media/ui/MediaSessionTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/media/ui/MediaSessionTest.java
@@ -41,6 +41,7 @@
import org.chromium.components.url_formatter.SchemeDisplay;
import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.content_public.browser.test.util.DOMUtils;
+import org.chromium.content_public.browser.test.util.JavaScriptUtils;
import org.chromium.media.MediaSwitches;
import org.chromium.net.test.EmbeddedTestServer;
@@ -194,6 +195,12 @@
mFakeTimeTestRule.deepSleepMillis(1500);
simulateScreenOn();
DOMUtils.waitForMediaPauseBeforeEnd(tab.getWebContents(), VIDEO_ID);
+
+ // Verify that the system sleep pause did not grant user activation.
+ String result =
+ JavaScriptUtils.executeJavaScriptAndWaitForResult(
+ tab.getWebContents(), "navigator.userActivation.isActive");
+ Assert.assertEquals("false", result);
}
@Test
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/media/FullscreenVideoPictureInPictureControllerUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/media/FullscreenVideoPictureInPictureControllerUnitTest.java
index 9f3a409..3197fab9 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/media/FullscreenVideoPictureInPictureControllerUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/media/FullscreenVideoPictureInPictureControllerUnitTest.java
@@ -41,6 +41,7 @@
import org.chromium.content_public.browser.MediaSession;
import org.chromium.content_public.browser.WebContentsObserver;
import org.chromium.content_public.browser.test.mock.MockWebContents;
+import org.chromium.media_session.mojom.MediaSession.SuspendType;
import java.util.concurrent.TimeUnit;
@@ -181,7 +182,7 @@
// Stash while media is playing.
mWebContentsObserverCaptor.getValue().mediaStartedPlaying(0, true, true);
mController.onStashReported(true);
- verify(mMediaSession, times(1)).suspend();
+ verify(mMediaSession, times(1)).suspend(SuspendType.UI);
mWebContentsObserverCaptor.getValue().mediaStoppedPlaying(0);
// Un-stash while media is still paused.
@@ -206,7 +207,7 @@
// Stashing paused video should do nothing.
mController.onStashReported(true);
- verify(mMediaSession, times(0)).suspend();
+ verify(mMediaSession, times(0)).suspend(SuspendType.UI);
// Un-stash should also do nothing.
mController.onStashReported(false);
@@ -225,7 +226,7 @@
// Stash normally.
mWebContentsObserverCaptor.getValue().mediaStartedPlaying(0, true, true);
mController.onStashReported(true);
- verify(mMediaSession, times(1)).suspend();
+ verify(mMediaSession, times(1)).suspend(SuspendType.UI);
mWebContentsObserverCaptor.getValue().mediaStoppedPlaying(0);
// Restart playback while still stashed.
@@ -253,7 +254,7 @@
mController.onResume();
verify(mActivity, times(0)).moveTaskToBack(true);
// The media should be paused, though, just as if pip had closed.
- verify(mMediaSession, times(1)).suspend();
+ verify(mMediaSession, times(1)).suspend(SuspendType.UI);
// When the device is unlocked, we will get `onStart`. This should cause pip to close
// because it's still deferred from the `onResume` call, above.
diff --git a/components/browser_ui/media/android/java/src/org/chromium/components/browser_ui/media/MediaSessionHelper.java b/components/browser_ui/media/android/java/src/org/chromium/components/browser_ui/media/MediaSessionHelper.java
index 8759ecc..95d1e9ed 100644
--- a/components/browser_ui/media/android/java/src/org/chromium/components/browser_ui/media/MediaSessionHelper.java
+++ b/components/browser_ui/media/android/java/src/org/chromium/components/browser_ui/media/MediaSessionHelper.java
@@ -38,6 +38,7 @@
import org.chromium.content_public.browser.Visibility;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsObserver;
+import org.chromium.media_session.mojom.MediaSession.SuspendType;
import org.chromium.media_session.mojom.MediaSessionAction;
import org.chromium.services.media_session.MediaImage;
import org.chromium.services.media_session.MediaMetadata;
@@ -146,7 +147,7 @@
&& mMediaSessionObserver != null
&& mMediaSessionObserver.getMediaSession() != null) {
MediaSessionUma.recordPause(MediaSessionActionSource.SYSTEM_SLEEP);
- mMediaSessionObserver.getMediaSession().suspend();
+ mMediaSessionObserver.getMediaSession().suspend(SuspendType.SYSTEM);
}
}
};
@@ -174,7 +175,7 @@
if (mMediaSessionObserver.getMediaSession() == null) return;
- mMediaSessionObserver.getMediaSession().suspend();
+ mMediaSessionObserver.getMediaSession().suspend(SuspendType.UI);
}
@Override
diff --git a/content/browser/media/session/media_session_android.cc b/content/browser/media/session/media_session_android.cc
index 1817577..5aa7602 100644
--- a/content/browser/media/session/media_session_android.cc
+++ b/content/browser/media/session/media_session_android.cc
@@ -167,9 +167,11 @@
media_session_->Resume(MediaSession::SuspendType::kUI);
}
-void MediaSessionAndroid::Suspend(JNIEnv* env) {
+void MediaSessionAndroid::Suspend(
+ JNIEnv* env,
+ media_session::mojom::MediaSession::SuspendType suspend_type) {
DCHECK(media_session_);
- media_session_->Suspend(MediaSession::SuspendType::kUI);
+ media_session_->Suspend(suspend_type);
}
void MediaSessionAndroid::Stop(JNIEnv* env) {
diff --git a/content/browser/media/session/media_session_android.h b/content/browser/media/session/media_session_android.h
index 85241fe..0dfd959b1 100644
--- a/content/browser/media/session/media_session_android.h
+++ b/content/browser/media/session/media_session_android.h
@@ -54,7 +54,8 @@
// MediaSession method wrappers.
void Resume(JNIEnv* env);
- void Suspend(JNIEnv* env);
+ void Suspend(JNIEnv* env,
+ media_session::mojom::MediaSession::SuspendType suspend_type);
void Stop(JNIEnv* env);
void Seek(JNIEnv* env, const int64_t millis);
void SeekTo(JNIEnv* env, const int64_t millis);
diff --git a/content/public/android/BUILD.gn b/content/public/android/BUILD.gn
index 8236fc4..db7f7d1 100644
--- a/content/public/android/BUILD.gn
+++ b/content/public/android/BUILD.gn
@@ -195,6 +195,7 @@
"//services/device/public/java:nfc_java",
"//services/device/public/mojom:mojom_java",
"//services/media_session/public/cpp/android:media_session_java",
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