CVE-2026-12323
Overview
Background
- `GeckoSession`
- the GeckoView Java class that represents a single web content session and routes content events to registered delegates.
- `SelectionActionDelegate`
- the delegate interface an embedding app implements to show and hide text-selection action UI and clipboard permission prompts.
- `onDismissClipboardPermissionRequest`
- the
SelectionActionDelegatecallback that tells the app to tear down any in-flight clipboard permission request UI. - `onHideAction`
- the
SelectionActionDelegatecallback that hides existing selection-action UI, here invoked withHIDE_REASON_NO_SELECTION.
Root Cause Analysis
When an app swaps or clears the SelectionActionDelegate on a GeckoSession, the code path in setSelectionActionDelegate explicitly cleans up the outgoing delegate because Gecko does not track the old delegate itself. The pre-fix code invoked only onHideAction on the old delegate, so it dismissed the selection-action UI but left any outstanding clipboard permission request UI still displayed. This violated the invariant that all UI owned by a delegate must be dismissed when that delegate is retired, letting a stale clipboard-permission prompt survive a delegate change.
The fix captures the outgoing delegate in oldDelegate and additionally calls oldDelegate.onDismissClipboardPermissionRequest(this), ensuring the clipboard prompt is torn down alongside the selection UI. This works because both dismissal callbacks are now driven against the same old delegate at the moment it is replaced, closing the cleanup gap.
SelectionActionDelegate can own during delegate replacement; the fix adds the missing onDismissClipboardPermissionRequest call so the clipboard permission prompt cannot outlive the delegate that raised it.Attack Path
- Trigger a clipboard prompt
Attacker content induces a clipboard permission request so the current
SelectionActionDelegatedisplays its permission UI. - Force a delegate change
A subsequent action causes the app to set or clear the
SelectionActionDelegateviasetSelectionActionDelegate. - Stale UI persists
Because only
onHideActionfired, the clipboard permission request UI from the old delegate remains on screen after the delegate is gone. - Spoofing window The lingering, orphaned prompt can misrepresent state or context to the user, who may act on a request no longer backed by a live delegate.
Impact Assessment
SelectionActionDelegate change; impact is rated low and limited to misleading UI rather than memory corruption.Files Changed
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
Audit Directions
- Delegate replacement cleanupWhenever a delegate is swapped or cleared, verify every UI surface it can present is dismissed, not just the most obvious one.
- Multi-callback lifecyclesAudit delegate interfaces with more than one show/hide pair (
onHideActionplusonDismissClipboardPermissionRequest) to confirm all teardown callbacks fire together. - Orphaned permission promptsLook for permission or consent UI that can outlive the object that raised it, especially across ownership or context transitions in embedding-layer glue code.
Patch
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
index fdab52f889b..5e47ed4f1f9 100644
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -3593,8 +3593,9 @@ public class GeckoSession {
// When the delegate is changed or cleared, make sure onHideAction is called
// one last time to hide any existing selection action UI. Gecko doesn't keep
// track of the old delegate, so we can't rely on Gecko to do that for us.
- getSelectionActionDelegate()
- .onHideAction(this, GeckoSession.SelectionActionDelegate.HIDE_REASON_NO_SELECTION);
+ final SelectionActionDelegate oldDelegate = getSelectionActionDelegate();
+ oldDelegate.onHideAction(this, GeckoSession.SelectionActionDelegate.HIDE_REASON_NO_SELECTION);
+ oldDelegate.onDismissClipboardPermissionRequest(this);
}
mSelectionActionDelegate.setDelegate(delegate, this);
}