High watch CVSS 7.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
7.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Browser
DescriptionUse after free in Browser
ComponentBrowser
Sub-componentUI>Browser
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.
Tracker520516206
Fix commit275ed0c7788f (chromium/src) +22/-30
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-06-05 (per vendor advisory)
Fix landed2026-08-26 (public git)
Disclosed2026-09-22
Patch gap27 days — fix public in source before the advisory

Background

`ModelQualityLogEntry`
an optimization_guide object that records model-quality telemetry for a single inference; its lifetime must be managed on a single (UI) thread.
`ModelHandler`
the component that runs the model and invokes result callbacks; it can complete or tear down work on a background ThreadPool sequence rather than the UI thread.
`ContextualTasksContextQuality`
an optimization_guide::proto message carrying the quality data as plain proto fields, safe to move across threads inside a callback.
Teardown race
during shutdown, pending ModelHandler callbacks may be destroyed off the UI thread, dropping objects they own on the wrong sequence.

Root Cause Analysis

Before the fix, ContextualTasksContextService eagerly created a ModelQualityLogEntry (bound to GetWeakPtr()) and stored it inside the base::OnceCallback passed to SelectRelevantTabs/ModelHandler. Because the callback owned the unique_ptr<ModelQualityLogEntry>, the log entry’s lifetime became tied to the callback’s lifetime rather than to the UI thread. If the ModelHandler completed or was torn down on a background ThreadPool sequence, the callback (and therefore the ModelQualityLogEntry it owned) was destroyed off the UI thread. ModelQualityLogEntry is not safe to destroy off-sequence, so its destructor touched UI-thread-owned state and the weak pointer machinery from the wrong thread, producing a use-after-free / heap corruption.

The bug is a lifetime/threading defect in how the log entry was threaded through the async callback, not a logic error in the quality data itself.

Key insight
Moving a plain ContextualTasksContextQuality proto through the callback (which is trivially safe to destroy on any thread) and deferring ModelQualityLogEntry construction to OnRelevantTabsSelected on the UI thread removes the off-thread-destructible object from the callback entirely.

Attack Path

  1. Trigger contextual-tasks inference Some browser flow invokes ContextualTasksContextService such that a ModelQualityLogEntry is created and bound into the SelectRelevantTabs callback.
  2. Induce teardown mid-flight Shutdown or service destruction occurs while the ModelHandler request is still outstanding, so pending callbacks are cleaned up.
  3. Off-thread destruction The ModelHandler destroys the pending callback on a background ThreadPool sequence, running ~ModelQualityLogEntry off the UI thread.
  4. UAF / heap corruption The destructor accesses UI-thread-owned state (and weak-pointer bookkeeping) freed or unsynchronized on the wrong sequence, corrupting the heap.

Impact Assessment

The defect is an in-browser-process use-after-free that manifests during teardown races, which can corrupt the heap and is generally non-deterministic and hard to control. It is not directly web-reachable, so exploitation would require winning a shutdown timing window rather than sending crafted web input, making it far more likely to surface as a crash than as a reliable exploit primitive. Any exploitability would be limited and racy, though heap corruption in the privileged browser process is inherently serious if it can be steered.

Changed Functions

FunctionChangeNotes
if
chrome/browser/contextual_tasks/contextual_tasks_context_service.cc
modified

Files Changed

  • chrome/browser/contextual_tasks/contextual_tasks_context_service.cc
  • chrome/browser/contextual_tasks/contextual_tasks_context_service.h

Audit Directions

  • Non-thread-safe objects owned by async callbacks
    Flag any std::make_unique<...LogEntry> or similarly UI-thread-bound object captured into a base::BindOnce/OnceCallback handed to a ModelHandler or other executor that may run or destroy callbacks off the originating sequence.
  • Weak-ptr-bound objects crossing threads
    Audit callbacks that carry both GetWeakPtr() and an owned object whose destructor touches that weak state; ensure destruction is guaranteed on the sequence that owns the WeakPtrFactory.
  • Defer-construction pattern
    Prefer passing plain movable protos/PODs through async boundaries and constructing thread-affined telemetry objects only inside the final UI-thread callback, and grep other optimization_guide call sites for the pre-fix eager-creation pattern.
From 275ed0c7788f04fe699ba4df3a27e935b5a48a1f Mon Sep 17 00:00:00 2001
From: Sophie Chang <[email protected]>
Date: Tue, 25 Aug 2026 22:27:30 -0700
Subject: [PATCH] Fix potential UAF in ContextualTasksContextService by deferring ModelQualityLogEntry creation.

The ModelQualityLogEntry was being held in callbacks passed to ModelHandler, which could lead to it being destroyed off the UI thread (e.g. on background ThreadPool) during teardown, causing UAF/heap corruption.
This change defers the creation of ModelQualityLogEntry until OnRelevantTabsSelected is called on the UI thread, and passes ContextualTasksContextQuality proto instead.

Bug: b:520516206
Change-Id: I6b82b15a86fb09e8bbfe74c8cae81203b7e28b6b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8291770
Reviewed-by: Abhay Prakash <[email protected]>
Commit-Queue: Sophie Chang <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1686160}
---
diff --git a/chrome/browser/contextual_tasks/contextual_tasks_context_service.cc b/chrome/browser/contextual_tasks/contextual_tasks_context_service.cc
index 89dde994..bfb8e99 100644
--- a/chrome/browser/contextual_tasks/contextual_tasks_context_service.cc
+++ b/chrome/browser/contextual_tasks/contextual_tasks_context_service.cc
@@ -669,14 +669,10 @@
     return;
   }
 
-  auto log_entry = std::make_unique<optimization_guide::ModelQualityLogEntry>(
-      optimization_guide_keyed_service_->GetModelQualityLogsUploaderService()
-          ->GetWeakPtr());
+  auto quality_log = std::make_unique<
+      optimization_guide::proto::ContextualTasksContextQuality>();
 
   passage_embeddings::Embedding query_embedding = embeddings[0];
-  auto* quality_log = log_entry->log_ai_data_request()
-                          ->mutable_contextual_tasks_context()
-                          ->mutable_quality();
   quality_log->set_embedding_model_version(
       embedder_model_version_.value_or(-1));
   quality_log->set_tab_selection_model_version(-1);
@@ -688,13 +684,14 @@
     }
   }
 
+  auto* quality_log_ptr = quality_log.get();
   SelectRelevantTabs(
       query, options, query_embedding, active_tab_at_query_time,
       all_eligible_tabs, explicit_urls,
       base::BindOnce(&ContextualTasksContextService::OnRelevantTabsSelected,
                      weak_ptr_factory_.GetWeakPtr(), query, options, start_time,
-                     explicit_urls, request_id, std::move(log_entry)),
-      quality_log);
+                     explicit_urls, request_id, std::move(quality_log)),
+      quality_log_ptr);
 }
 
 void ContextualTasksContextService::OnRelevantTabsSelected(
@@ -703,14 +700,12 @@
     base::TimeTicks start_time,
     const std::vector<GURL>& explicit_urls,
     int64_t request_id,
-    std::unique_ptr<optimization_guide::ModelQualityLogEntry> log_entry,
+    std::unique_ptr<optimization_guide::proto::ContextualTasksContextQuality>
+        quality_log,
     std::vector<base::WeakPtr<content::WebContents>> relevant_tabs) {
   auto request_it = pending_requests_.find(request_id);
   if (request_it == pending_requests_.end()) {
     // We had timed out already and the callback was already invoked.
-    if (log_entry) {
-      optimization_guide::ModelQualityLogEntry::Drop(std::move(log_entry));
-    }
     return;
   }
 
@@ -740,15 +735,14 @@
         std::set<GURL>(explicit_urls.begin(), explicit_urls.end()));
   }
 
-  if (!ShouldLogContextualTasksContextQuality() || !log_entry ||
-      log_entry->log_ai_data_request()
-              ->contextual_tasks_context()
-              .quality()
-              .eligible_tabs()
-              .size() == 0) {
-    // Explicitly drop when we don't want to log. Otherwise, the destructor of
-    // the log entry will trigger an upload.
-    optimization_guide::ModelQualityLogEntry::Drop(std::move(log_entry));
+  if (ShouldLogContextualTasksContextQuality() && quality_log &&
+      quality_log->eligible_tabs().size() > 0) {
+    auto log_entry = std::make_unique<optimization_guide::ModelQualityLogEntry>(
+        optimization_guide_keyed_service_->GetModelQualityLogsUploaderService()
+            ->GetWeakPtr());
+    *log_entry->log_ai_data_request()
+         ->mutable_contextual_tasks_context()
+         ->mutable_quality() = std::move(*quality_log);
   }
 
   std::move(callback).Run(std::move(relevant_tabs));
@@ -1290,13 +1284,9 @@
     return;
   }
 
-  auto log_entry = std::make_unique<optimization_guide::ModelQualityLogEntry>(
-      optimization_guide_keyed_service_->GetModelQualityLogsUploaderService()
-          ->GetWeakPtr());
+  auto quality_log = std::make_unique<
+      optimization_guide::proto::ContextualTasksContextQuality>();
 
-  auto* quality_log = log_entry->log_ai_data_request()
-                          ->mutable_contextual_tasks_context()
-                          ->mutable_quality();
   quality_log->set_embedding_model_version(
       embedder_model_version_.value_or(-1));
   quality_log->set_tab_selection_model_version(-1);
@@ -1308,14 +1298,15 @@
     }
   }
 
+  auto* quality_log_ptr = quality_log.get();
   SelectRelevantTabsForConversationThread(
       conversation_thread, options, embeddings, context_tab_at_query_time,
       all_eligible_tabs, explicit_urls,
       base::BindOnce(&ContextualTasksContextService::OnRelevantTabsSelected,
                      weak_ptr_factory_.GetWeakPtr(), conversation_thread.query,
                      options, start_time, explicit_urls, request_id,
-                     std::move(log_entry)),
-      quality_log);
+                     std::move(quality_log)),
+      quality_log_ptr);
 }
 
 void ContextualTasksContextService::SelectRelevantTabsForConversationThread(
diff --git a/chrome/browser/contextual_tasks/contextual_tasks_context_service.h b/chrome/browser/contextual_tasks/contextual_tasks_context_service.h
index 5280c170..e3feb22 100644
--- a/chrome/browser/contextual_tasks/contextual_tasks_context_service.h
+++ b/chrome/browser/contextual_tasks/contextual_tasks_context_service.h
@@ -210,7 +210,8 @@
       base::TimeTicks start_time,
       const std::vector<GURL>& explicit_urls,
       int64_t request_id,
-      std::unique_ptr<optimization_guide::ModelQualityLogEntry> log_entry,
+      std::unique_ptr<optimization_guide::proto::ContextualTasksContextQuality>
+          quality_log,
       std::vector<base::WeakPtr<content::WebContents>> relevant_tabs);
 
   // Intermediate state for asynchronous scoring.
Loading diff…

Original Bug Report

reported by [email protected]

Potential browser-process UAF and heap corruption via off-thread ModelQualityLogEntry destruction

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential thread safety issue exists where a ModelQualityLogEntry can be deleted off the UI thread on a ThreadPool sequence during profile teardown. Its destructor performs cross-sequence dereferencing of a UI-thread-bound WeakPtr and modifies an unsynchronized std::vector on a background thread. This can result in a potential browser-process Use-After-Free (UAF) or heap corruption.

Affected files:

  • chrome/browser/contextual_tasks/contextual_tasks_context_service.cc
  • components/optimization_guide/core/model_quality/model_quality_log_entry.cc
  • components/optimization_guide/core/model_quality/model_quality_logs_uploader_service.cc
  • chrome/browser/contextual_tasks/contextual_tasks_context_model_handler.cc
  • components/optimization_guide/core/inference/model_handler.h

Estimated timestamp from git blame: 2026-04-16

Description

A potential security issue exists in the interaction between ContextualTasksContextService and the ModelHandler infrastructure. During profile or browser teardown, a callback chain containing a std::unique_ptr<ModelQualityLogEntry> can be silently discarded and destroyed on a background BEST_EFFORT ThreadPool sequence instead of the UI thread.

When ModelQualityLogEntry is destroyed off the UI thread, its destructor (components/optimization_guide/core/model_quality/model_quality_log_entry.cc:37) executes non-thread-safe operations:

  1. It dereferences and invokes methods on uploader_ (a base::WeakPtr<ModelQualityLogsUploaderService> bound to the UI thread), violating sequence safety constraints and potentially leading to a Use-After-Free (UAF) if the uploader is concurrently destroyed on the UI thread.
  2. It calls SetMqlsLogForWebUI which performs an unsynchronized push_back into the mqls_logs_for_web_ui_ vector (components/optimization_guide/core/model_quality/model_quality_logs_uploader_service.cc:287), risking concurrent mutation and heap corruption.

Potential Mechanism of Off-Thread Destruction

  1. ContextualTasksContextService::OnQueryEmbeddingReady (chrome/browser/contextual_tasks/contextual_tasks_context_service.cc:482) instantiates a ModelQualityLogEntry on the UI thread.
  2. This log entry is moved into a callback chain passed to model_handler_->BatchExecuteModelWithSignals.
  3. The executor (TFLiteModelExecutor) posts an asynchronous OpenModelFile task to a background ThreadPool runner (model_loading_task_runner_). The reply callback (transitively owning the log entry) is parked waiting for completion.
  4. Concurrently, the user closes the profile. KeyedService teardown on the UI thread calls ~ModelHandler, which invokes model_task_runner_->DeleteSoon(model_executor_).
  5. Because the task runner is sequenced, the DeleteSoon task runs before the OpenModelFile reply task. This destroys the executor and invalidates its execution-thread WeakPtr.
  6. When the reply task is eventually dequeued, its receiver WeakPtr is invalid. The callback is canceled, causing its BindState (and thus the ModelQualityLogEntry) to be destroyed synchronously on the background ThreadPool sequence instead of returning to the UI thread.

Potential Steps to Reproduce

Note: Our tooling agent does not currently have the capability to execute code or run a live proof of concept; therefore, these are suggested and potential steps based on code analysis.

  1. Enable the ContextualTasksContext feature (which has active platform testing configs).
  2. Initiate a query that prompts contextual search processing, invoking BatchExecuteModelWithSignals.
  3. While the background model loading task (OpenModelFile) is executing on the ThreadPool, close the profile or browser window to trigger service teardown.
  4. Observe potential browser process instability or memory corruption assertions in debug builds due to ~ModelQualityLogEntry executing off-sequence.

Suggested Fix

Ensure that the ModelQualityLogEntry is always deleted on the thread where its bound WeakPtr and uploader service live (the UI thread).

One robust fix is to wrap the callback or the log entry destructor using a helper that enforces UI-thread deletion, such as wrapping the pointer in a custom deleter that posts destruction to the UI thread if executed off-sequence:

struct UIThreadDeleter {
  void operator()(optimization_guide::ModelQualityLogEntry* ptr) const {
    if (content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)) {
      delete ptr;
    } else {
      content::GetUIThreadTaskRunner({})->DeleteSoon(FROM_HERE, ptr);
    }
  }
};

Alternatively, ensure that the callbacks passed to ModelHandler that own UI-thread-bound resources are safely bound using base::BindPostTask or destroyed via custom helpers that handle off-sequence cancellation gracefully.

Evaluated with Chrome root at commit: e9507a33bb4148ee071aaaf8a7e9ad68770359bf


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

Raised in root component due to access or custom field issues on 1516765

View on issue tracker