CVE-2026-8550
Overview
Background
- Lens Overlay query controller
LensOverlayQueryControllergathers page content and uploads it to the Lens backend, offloading compression to a background task runner.- `base::span<const lens::PageContent>`
- A non-owning view over
underlying_page_contents_; if the underlying storage is cleared or mutated before the viewer is used, the span dangles. - `PostTaskAndReplyWithResult` / `compression_task_tracker_`
- Posts work to another thread and replies on the main thread; the tracker can cancel outstanding tasks via
TryCancelAll(). - `page_content_request_in_progress_`
- A flag intended to record that a page-content request is underway so overlapping requests/state can be reasoned about.
Root Cause Analysis
PrepareAndFetchPageContentRequest (hunk around line 1318, enclosing name not shown in the header) posted CreatePageContentPayload / CreatePageContentPayloadForChunks to a background thread while passing underlying_page_contents_ as a base::span<const lens::PageContent> — a borrowed view into a member container. If underlying_page_contents_ was cleared, reassigned, or the controller’s state reset on the main thread before the background compression task ran, the span dangled and the background task read freed PageContent memory, a use-after-free.
The fix changes CreatePageContentPayload to take std::vector<lens::PageContent> by value and constructs a contents_copy that is std::moved into the bound task, giving the background work its own owned copy with guaranteed lifetime; CreatePageContentPayloadForChunks drops the page_content span parameter entirely. The CL also fixes a cancellation-ordering bug: compression_task_tracker_->TryCancelAll() was moved before the underlying_page_contents_.empty() early-return (previously it ran only after, so early returns skipped cancellation), and adds page_content_request_in_progress_ = false to the reset path plus folds that flag into IsPageContentUploadInProgress() so in-flight requests are tracked correctly.
base::span borrowing underlying_page_contents_ whose lifetime the task could outlive; the fix passes an owned std::vector<lens::PageContent> copy by value (plus reorders TryCancelAll() and tracks page_content_request_in_progress_) so the task never references freed content.Attack Path
- Start a page-content upload
A Lens Overlay page-content request is initiated, posting a background compression task that references
underlying_page_contents_by span. - Free/reset the backing content
Before the background task runs, main-thread activity (a new request, state reset, or cancellation that pre-patch was skipped on the early-return path) clears or replaces
underlying_page_contents_, dangling the span. - Background task reads freed memory
The compression task executes
CreatePageContentPayload*over the danglingPageContentview, a use-after-free. - Corruption/control (inferred) Exploiting the UAF for information disclosure or control is (inferred); the diff demonstrates only the lifetime violation and its fix.
Impact Assessment
Files Changed
chrome/browser/ui/lens/lens_overlay_query_controller.ccchrome/browser/ui/lens/lens_overlay_query_controller.h
Audit Directions
- `base::span`/raw refs bound into cross-thread tasksAudit
PostTask*/BindOncecalls that pass abase::spanor reference to a member container; such borrows must outlive the task or be copied by value / moved. - Cancellation ordering around early returnsVerify
TryCancelAll()/teardown runs on all exit paths (including earlyreturns) so outstanding tasks referencing soon-to-be-freed state are actually cancelled. - In-progress flags vs. object presenceWhere “is a request in progress” is inferred from a pointer being non-null, check for gaps (e.g., posted-but-not-yet-constructed fetcher) that an explicit boolean like
page_content_request_in_progress_must cover.
Patch
From 4af1bce9f2ecb4ad514d3fc4e3f170f707180ced Mon Sep 17 00:00:00 2001 From: Duncan Mercer <[email protected]> Date: Thu, 02 Apr 2026 12:29:55 -0700 Subject: [PATCH] [Lens Overlay] Fix Use-After-Free in query controller Pass PageContent by value to background threads to ensure lifetime safety. Also fix task cancellation bypass and state reset. Fixed: 498322453 Change-Id: If2282782cf97630604f5e6220666035f0294c989 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7723469 Reviewed-by: Juan Mojica <[email protected]> Commit-Queue: Duncan Mercer <[email protected]> Cr-Commit-Position: refs/heads/main@{#1609432} --- diff --git a/chrome/browser/ui/lens/lens_overlay_query_controller.cc b/chrome/browser/ui/lens/lens_overlay_query_controller.cc index 24a417ab..789cffd0 100644 --- a/chrome/browser/ui/lens/lens_overlay_query_controller.cc +++ b/chrome/browser/ui/lens/lens_overlay_query_controller.cc @@ -297,7 +297,6 @@ // Returns the lens::Payload to be sent after uploading chunked data using the // repeated Content field instead of the deprecated payload fields. lens::Payload CreatePageContentPayloadForChunks( - base::span<const lens::PageContent> page_content, lens::MimeType primary_content_type, GURL page_url, std::optional<std::string> page_title, @@ -328,7 +327,7 @@ // Returns the lens::Payload using the repeated Content field instead of the // deprecated payload fields. lens::Payload CreatePageContentPayload( - base::span<const lens::PageContent> page_contents, + std::vector<lens::PageContent> page_contents, GURL page_url, std::optional<std::string> page_title) { lens::Payload payload; @@ -505,6 +504,7 @@ page_url_ = GURL(); page_title_ = std::nullopt; partial_content_ = base::span<const std::u16string>(); + page_content_request_in_progress_ = false; } void LensOverlayQueryController::SendUpdatedPageContent( @@ -1270,13 +1270,13 @@ return; } + compression_task_tracker_->TryCancelAll(); + if (underlying_page_contents_.empty() || underlying_page_contents_.front().bytes_.empty()) { // No need to send the request without underlying content bytes. return; } - - compression_task_tracker_->TryCancelAll(); page_contents_request_start_time_ = base::TimeTicks::Now(); page_content_request_in_progress_ = true; chunk_upload_in_progress_ = false; @@ -1318,9 +1318,11 @@ } else { // Post CreatePageContentPayload to a task off the main thread so // compression does not throttle the main thread. + std::vector<lens::PageContent> contents_copy( + underlying_page_contents_.begin(), underlying_page_contents_.end()); compression_task_tracker_->PostTaskAndReplyWithResult( compression_task_runner_.get(), FROM_HERE, - base::BindOnce(&CreatePageContentPayload, underlying_page_contents_, + base::BindOnce(&CreatePageContentPayload, std::move(contents_copy), page_url_, page_title_), base::BindOnce( &LensOverlayQueryController::PrepareAndFetchPageContentRequestPart2, @@ -1416,9 +1418,8 @@ base::SequencedTaskRunner::GetCurrentDefault()->PostTaskAndReplyWithResult( FROM_HERE, base::BindOnce(&CreatePageContentPayloadForChunks, - underlying_page_contents_, primary_content_type_, - page_url_, page_title_, total_chunks, - retrying_page_content_upload_), + primary_content_type_, page_url_, page_title_, + total_chunks, retrying_page_content_upload_), base::BindOnce( &LensOverlayQueryController::PrepareAndFetchPageContentRequestPart2, weak_ptr_factory_.GetWeakPtr(), request_id)); diff --git a/chrome/browser/ui/lens/lens_overlay_query_controller.h b/chrome/browser/ui/lens/lens_overlay_query_controller.h index 20302998..a9877b2 100644 --- a/chrome/browser/ui/lens/lens_overlay_query_controller.h +++ b/chrome/browser/ui/lens/lens_overlay_query_controller.h @@ -199,7 +199,8 @@ lens::mojom::SemanticEvent event); bool IsPageContentUploadInProgress() const { - return page_content_endpoint_fetcher_.get() != nullptr; + return page_content_request_in_progress_ || + page_content_endpoint_fetcher_.get() != nullptr; } // Returns whether the query controller is off.