CVE-2026-95298
Overview
Background
- `ModelQualityLogEntry`
- an
optimization_guideobject 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
ThreadPoolsequence rather than the UI thread. - `ContextualTasksContextQuality`
- an
optimization_guide::protomessage carrying the quality data as plain proto fields, safe to move across threads inside a callback. - Teardown race
- during shutdown, pending
ModelHandlercallbacks 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.
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
- Trigger contextual-tasks inference
Some browser flow invokes
ContextualTasksContextServicesuch that aModelQualityLogEntryis created and bound into theSelectRelevantTabscallback. - Induce teardown mid-flight
Shutdown or service destruction occurs while the
ModelHandlerrequest is still outstanding, so pending callbacks are cleaned up. - Off-thread destruction
The
ModelHandlerdestroys the pending callback on a backgroundThreadPoolsequence, running~ModelQualityLogEntryoff the UI thread. - 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/contextual_tasks/contextual_tasks_context_service.cc |
modified |
Files Changed
chrome/browser/contextual_tasks/contextual_tasks_context_service.ccchrome/browser/contextual_tasks/contextual_tasks_context_service.h
Audit Directions
- Non-thread-safe objects owned by async callbacksFlag any
std::make_unique<...LogEntry>or similarly UI-thread-bound object captured into abase::BindOnce/OnceCallbackhanded to aModelHandleror other executor that may run or destroy callbacks off the originating sequence. - Weak-ptr-bound objects crossing threadsAudit callbacks that carry both
GetWeakPtr()and an owned object whose destructor touches that weak state; ensure destruction is guaranteed on the sequence that owns theWeakPtrFactory. - Defer-construction patternPrefer 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_guidecall sites for the pre-fix eager-creation pattern.
Patch
From 275ed0c7788f04fe699ba4df3a27e935b5a48a1f Mon Sep 17 00:00:00 2001
From: Sophie Chang <sophiechang@chromium.org>
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 <abhayprakash@google.com>
Commit-Queue: Sophie Chang <sophiechang@chromium.org>
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.
Original Bug Report
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.cccomponents/optimization_guide/core/model_quality/model_quality_log_entry.cccomponents/optimization_guide/core/model_quality/model_quality_logs_uploader_service.ccchrome/browser/contextual_tasks/contextual_tasks_context_model_handler.cccomponents/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:
- It dereferences and invokes methods on
uploader_(abase::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. - It calls
SetMqlsLogForWebUIwhich performs an unsynchronizedpush_backinto themqls_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
ContextualTasksContextService::OnQueryEmbeddingReady(chrome/browser/contextual_tasks/contextual_tasks_context_service.cc:482) instantiates aModelQualityLogEntryon the UI thread.- This log entry is moved into a callback chain passed to
model_handler_->BatchExecuteModelWithSignals. - The executor (
TFLiteModelExecutor) posts an asynchronousOpenModelFiletask to a background ThreadPool runner (model_loading_task_runner_). The reply callback (transitively owning the log entry) is parked waiting for completion. - Concurrently, the user closes the profile. KeyedService teardown on the UI thread calls
~ModelHandler, which invokesmodel_task_runner_->DeleteSoon(model_executor_). - Because the task runner is sequenced, the
DeleteSoontask runs before theOpenModelFilereply task. This destroys the executor and invalidates its execution-threadWeakPtr. - When the reply task is eventually dequeued, its receiver
WeakPtris invalid. The callback is canceled, causing itsBindState(and thus theModelQualityLogEntry) 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.
- Enable the
ContextualTasksContextfeature (which has active platform testing configs). - Initiate a query that prompts contextual search processing, invoking
BatchExecuteModelWithSignals. - While the background model loading task (
OpenModelFile) is executing on the ThreadPool, close the profile or browser window to trigger service teardown. - Observe potential browser process instability or memory corruption assertions in debug builds due to
~ModelQualityLogEntryexecuting 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