CVE-2025-2476
Overview
Background
- `LensPermissionBubbleController`
- the browser-side object that shows the Lens Overlay permission dialog and handles its accept/cancel callbacks.
- `OnPermissionDialogAccept`
- the callback invoked when the user presses the accept button in the permission bubble; it enables Lens prefs and tears down the dialog widget.
- `CloseUISync`
- a
LensOverlayControllerteardown routine that synchronously destroys Lens UI, including resetting theLensPermissionBubbleController. - `base::WeakPtr`
- a non-owning pointer that becomes null once its referent is destroyed, allowing a callback to detect that
thishas been freed.
Root Cause Analysis
OnPermissionDialogAccept calls pref_service_->SetBoolean(prefs::kLensSharingPageScreenshotEnabled, true) and then continues to touch member state (pref_service_, dialog_widget_) on this. Setting that preference synchronously fires a chain of pref-change observers that can reach LensOverlayController, and if the controller then fails to capture a page screenshot it calls CloseUISync(), which resets and destroys the owning LensPermissionBubbleController. Because SetBoolean re-enters and can free this before returning, the subsequent statements in OnPermissionDialogAccept execute on a destroyed object. Specifically, the second SetBoolean(prefs::kLensSharingPageContentEnabled, true) and the dialog_widget_ = nullptr assignment dereference freed memory, producing a use-after-free.
The fix captures a base::WeakPtr to the controller before the first SetBoolean and guards each post-SetBoolean access with a .get() liveness check.
Attack Path
- Trigger the permission flow
Induce the Lens Overlay to present its permission bubble, instantiating a
LensPermissionBubbleController. - Accept the dialog
Cause
OnPermissionDialogAcceptto run, invokingSetBooleanonprefs::kLensSharingPageScreenshotEnabled. - Force re-entrant teardown
Arrange conditions where
LensOverlayControllercannot capture a page screenshot, so the pref-change callbacks reachCloseUISync()and free the controller. - Dereference freed object
Control returns into
OnPermissionDialogAccept, which touchespref_service_/dialog_widget_on the now-freedthis, exercising the UAF.
Impact Assessment
Files Changed
chrome/browser/ui/lens/lens_permission_bubble_controller.cc
Audit Directions
- Re-entrant pref mutationAudit callbacks that call
PrefService::SetBoolean/SetValueand then continue to accessthis; pref observers can synchronously destroy the caller. - Post-callback member accessFlag any method that invokes an external callback or observer-notifying call and afterward dereferences members without re-checking object liveness via
base::WeakPtr. - Synchronous UI teardown pathsTrace
CloseUISync-style synchronous destructors to find all callers whose stack frames may still reference the destroyed object after they return.
Patch
From 2690d69b56b764bfb1997cfb0715ac22b5976648 Mon Sep 17 00:00:00 2001 From: Nihar Majmudar <[email protected]> Date: Fri, 07 Mar 2025 10:53:29 -0800 Subject: [PATCH] [LensOverlay] Check if controller exists in OnPermissionDialogAccept. There's a chance LensPermissionBubbleController gets destroyed before the OnPermissionDialogAccepted callback finishes. Check if the controller still exists at this point. This risk occurs after SetBoolean() is called on the pref service since that then invokes a set of callbacks that could eventually lead to CloseUISync() being called (more thorough RCA in bug). Change-Id: I3d1842c3531fad474200a27ef054a30b753ccd61 Bug: 401029609 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6333695 Reviewed-by: Duncan Mercer <[email protected]> Commit-Queue: Nihar Majmudar <[email protected]> Cr-Commit-Position: refs/heads/main@{#1429619} --- diff --git a/chrome/browser/ui/lens/lens_permission_bubble_controller.cc b/chrome/browser/ui/lens/lens_permission_bubble_controller.cc index 314f74f..73c8a3cb 100644 --- a/chrome/browser/ui/lens/lens_permission_bubble_controller.cc +++ b/chrome/browser/ui/lens/lens_permission_bubble_controller.cc @@ -164,11 +164,21 @@ void LensPermissionBubbleController::OnPermissionDialogAccept() { RecordPermissionUserAction(LensPermissionUserAction::kAcceptButtonPressed, invocation_source_); + base::WeakPtr<LensPermissionBubbleController> + lens_permission_bubble_controller = weak_ptr_factory_.GetWeakPtr(); pref_service_->SetBoolean(prefs::kLensSharingPageScreenshotEnabled, true); - if (lens::features::IsLensOverlayContextualSearchboxEnabled()) { + // TODO(crbug.com/401029609): Rethink permission bubble lifetime. + // Must check WeakPtr in case CloseUISync() is called in + // LensOverlayController. This happens if the LensOverlayController cannot + // successfully take a screenshot of the page. If CloseUISync() is called, + // LensPermissionBubbleController gets reset before the following. + if (lens_permission_bubble_controller.get() && + lens::features::IsLensOverlayContextualSearchboxEnabled()) { pref_service_->SetBoolean(prefs::kLensSharingPageContentEnabled, true); } - dialog_widget_ = nullptr; + if (lens_permission_bubble_controller.get()) { + dialog_widget_ = nullptr; + } } void LensPermissionBubbleController::OnPermissionDialogCancel() {