High watch CVSS 6.5 chrome UAF 🔧 Commit mapped

Overview

High
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Google Lens
DescriptionUse after free in Google Lens
ComponentGoogle Lens
Sub-componentLens Overlay
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker498322453
Fix commit4af1bce9f2ec (chromium/src) +11/-9
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-31 (per vendor advisory)
Fix landed2026-04-02 (public git)
Disclosed2026-05-12
Patch gap40 days — fix public in source before the advisory

Background

Lens Overlay query controller
LensOverlayQueryController gathers 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.

Key insight
The core mistake was handing a background thread a 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

  1. 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.
  2. 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.
  3. Background task reads freed memory The compression task executes CreatePageContentPayload* over the dangling PageContent view, a use-after-free.
  4. 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

An attacker able to drive Lens Overlay page-content requests and race the backing-store lifetime could trigger a use-after-free during background compression, which the advisory classifies as “Use after free in Google Lens” at high severity. The effect is in the browser process (the Lens UI/query controller); the preconditions involve timing between the main thread’s state changes and the background task, and any escalation beyond the UAF is (inferred). The advisory states no exploited-in-the-wild status.

Files Changed

  • chrome/browser/ui/lens/lens_overlay_query_controller.cc
  • chrome/browser/ui/lens/lens_overlay_query_controller.h

Audit Directions

  • `base::span`/raw refs bound into cross-thread tasks
    Audit PostTask*/BindOnce calls that pass a base::span or reference to a member container; such borrows must outlive the task or be copied by value / moved.
  • Cancellation ordering around early returns
    Verify TryCancelAll()/teardown runs on all exit paths (including early returns) so outstanding tasks referencing soon-to-be-freed state are actually cancelled.
  • In-progress flags vs. object presence
    Where “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.
From 4af1bce9f2ecb4ad514d3fc4e3f170f707180ced Mon Sep 17 00:00:00 2001
From: Duncan Mercer <mercerd@google.com>
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 <juanmojica@google.com>
Commit-Queue: Duncan Mercer <mercerd@google.com>
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.
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.