CVE-2024-12382
Overview
Background
- `LanguageDetectionModel`
- the Translate component’s on-device language-identification model wrapper in
language_detection_model.cc, which loads its model asynchronously and lets clients register aModelLoadedCallbackto run once loading finishes. - `model_loaded_callbacks_`
- a
std::vector<ModelLoadedCallback>member holding callbacks that are queued while the model is not yet loaded and are drained byNotifyModelLoaded(). - Iterator invalidation
- when a
std::vectorgrows past its capacity during a range-forloop, it reallocates its backing store, so the loop’s iterator still points into the freed buffer. - Re-entrancy
- a callback invoked from inside a loop can call back into the same object and change the container that the loop is still walking.
Root Cause Analysis
Before the patch, LanguageDetectionModel::NotifyModelLoaded() ran a range-for directly over the member model_loaded_callbacks_, invoked std::move(callback_).Run(*this) for each entry, and only afterwards set loaded_ = true and called model_loaded_callbacks_.clear(). Because loaded_ was still false while the callbacks ran, a callback that re-entered the model and registered another ModelLoadedCallback could append to model_loaded_callbacks_ in the middle of the iteration. The registration path itself is not in the diff. An append that forced reallocation freed the buffer that the loop’s iterator was still using, so the next increment or dereference touched freed heap memory (the “Use after free in Translate”). The new comment says this directly: “the callbacks could result in modification of model_loaded_callbacks_, it’s not safe to iterate over the member.”
The fix swaps the member into a local model_loaded_callbacks vector before the loop and iterates over the local copy, so nothing a callback appends to the member can invalidate the iterator in use.
Attack Path
- Model load pending
While the language detection model is still loading, one or more clients register
ModelLoadedCallbacks, which are queued inmodel_loaded_callbacks_. - Load completes
NotifyModelLoaded()runs and walksmodel_loaded_callbacks_directly, withloaded_stillfalse. - Re-entrant registration
A callback’s handling causes another load-completion callback to be registered on the same model. Because
loaded_is stillfalse, it is pushed ontomodel_loaded_callbacks_(inferred: the add path is not in the diff). - Reallocation frees the iterated buffer
If the push exceeds capacity, the vector reallocates, and the range-
foriterator keeps reading moved-fromModelLoadedCallbackstorage in the freed buffer, which is a heap UAF. - Memory corruption Running a callback object read from freed, possibly reclaimed memory could redirect control flow (inferred; the patch shows only the iterator-invalidation precondition).
Impact Assessment
std::vector backing store holding ModelLoadedCallback objects, in whichever process hosts LanguageDetectionModel for Translate. The diff does not say which process that is. Triggering it requires a callback registration to happen again during NotifyModelLoaded(), which page-driven Translate/language-detection activity could plausibly cause (inferred). The advisory rates it high severity and does not report in-the-wild exploitation. Turning it into code execution is plausible for a callback-object UAF but is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
forcomponents/language_detection/core/language_detection_model.cc |
modified |
Files Changed
components/language_detection/core/language_detection_model.cc
Audit Directions
- Callback-list dispatch over membersSearch for
for (auto&& cb : member_callbacks_) std::move(cb).Run(...)patterns where the callback can reach anAdd*Callback/Register*method on the same object. Prefer swap-then-iterate,base::OnceCallbackList, or posting each callback as a task. - State flag set after dispatchLook for flags like
loaded_/ready_/initialized_that are set only after notifying observers. Re-entrant calls see the stale state and take the “queue it” path, which can mutate containers or drop callbacks. - Self-destruction during notificationIn
NotifyModelLoaded()and similar code, check whether a callback can destroy the owning object. The post-looploaded_ = truewrite would then touch freedthis, and the swap fix does not address that case.
Patch
diff --git a/components/language_detection/core/language_detection_model.cc b/components/language_detection/core/language_detection_model.cc
index ae10567..a745247 100644
--- a/components/language_detection/core/language_detection_model.cc
+++ b/components/language_detection/core/language_detection_model.cc
@@ -288,11 +288,17 @@
void LanguageDetectionModel::NotifyModelLoaded() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
- for (auto&& callback_ : model_loaded_callbacks_) {
- std::move(callback_).Run(*this);
+ std::vector<ModelLoadedCallback> model_loaded_callbacks;
+
+ // Since the callbacks could result in modification of
+ // `model_loaded_callbacks_`, it's not safe to iterate over the member.
+ // TODO(https://crbug.com/381461495): Post a task for each callback.
+ model_loaded_callbacks.swap(model_loaded_callbacks_);
+
+ for (auto&& callback : model_loaded_callbacks) {
+ std::move(callback).Run(*this);
}
loaded_ = true;
- model_loaded_callbacks_.clear();
}
} // namespace language_detection
Original Bug Report
AddressSanitizer:heap-use-after-free on LanguageDetectionModel::NotifyModelLoaded
Steps to reproduce the problem
repro:
-
launch chrome with –enable-features=LanguageDetectionAPI
-
apply patch.diff which simplify trigger
-
run poc.html.
Problem Description
- In the function
LanguageDetectionModel:: NotifyModelLoaded, there is an iterator operation. When AILanguageDetector performs detection, the callbackOnDetectCompletewill be added to themodel_loaded_callbacks_, and the OnDetectComplete function body will have a user-defined callbackresolve ->Resolve. The following is the reverse reference of the chain.
void LanguageDetectionModel::NotifyModelLoaded() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (auto&& callback_ : model_loaded_callbacks_) {
std::move(callback_).Run(*this);
}
loaded_ = true;
model_loaded_callbacks_.clear();
}
------------------------
void LanguageDetectionModel::AddOnModelLoadedCallback(
ModelLoadedCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (loaded_ || model_loaded_callbacks_.size() >= kMaxPendingCallbacksCount) {
std::move(callback).Run(*this);
} else {
model_loaded_callbacks_.emplace_back(std::move(callback));
}
}
------------------------
void DetectLanguage(const WTF::String& text,
DetectLanguageCallback on_complete) {
auto& model = language_detection::GetLanguageDetectionModel();
model.AddOnModelLoadedCallback(
WTF::BindOnce(DetectLanguageWithModel, text, std::move(on_complete)));
}
------------------------
ScriptPromise<IDLSequence<LanguageDetectionResult>> AILanguageDetector::detect(
ScriptState* script_state,
const WTF::String& input,
AILanguageDetectorDetectOptions* options,
ExceptionState& exception_state) {
[...]
DetectLanguage(input, WTF::BindOnce(AILanguageDetector::OnDetectComplete,
WrapPersistent(resolver)));
return resolver->Promise();
}
-----------------------
void AILanguageDetector::OnDetectComplete(
ScriptPromiseResolver<IDLSequence<LanguageDetectionResult>>* resolver,
base::expected<WTF::Vector<LanguagePrediction>, DetectLanguageError>
result) {
if (result.has_value()) {
// Order the result from most to least confident.
std::sort(result.value().rbegin(), result.value().rend());
resolver->Resolve(ConvertResult(result.value()));
} else {
switch (result.error()) {
case DetectLanguageError::kUnavailable:
resolver->Reject("Model not available");
}
}
}
LanguageDetectionModel:: NotifyModelLoadedis called inLanguageDetectionAgent:: updateLanguageDetectionModel, which can be traced back toLanguageDetectionAgent:: WasShown. However, in thisWasShown, Mojo calls are made to communicate with the browser throughVNet anguageDetectionHandler () ->VNet anguageDetectionModel `` The finalcallbackreturns therendererfor calling, which represents an asynchronous process in the middle of therendererexecution. This means that we can construct a POC. Before running theLanguageDetectionAgent:: Updating LanguageDetectionModel, we will fill the corresponding quantity and contain user-defined iterator invalidation code in themodel_loaded_callback`. Ultimately, it will lead to the failure of iterators. Causing UAF.
void LanguageDetectionAgent::WasShown() {
// Check if the the render frame was initially hidden and
// the model request was delayed until the frame was in
// the foreground.
[...]
GetLanguageDetectionHandler()->GetLanguageDetectionModel(
base::BindOnce(&LanguageDetectionAgent::UpdateLanguageDetectionModel,
weak_pointer_factory_.GetWeakPtr()));
}
bitset: https://source.chromium.org/chromium/chromium/src/+/6742751ccf591d9ba1670c3082efbf345084b7d0
why need patch? simplify trigger progress. No impact on the vulnerability principle.
fix sugesstions: swap the model_loaded_callbacks_. see fix.diff
Summary
AddressSanitizer:heap-use-after-free on LanguageDetectionModel::NotifyModelLoaded
Custom Questions
Type of crash:
tab
Crash state:
see asan.log
Reporter credit:
lime(@limeSec)
Additional Data
Category: Security
Chrome Channel: Not sure
Regression: N/A
asan-2.logasan.logdemonstration.movexploit_demonstration.mdfix.diffpoc.htmlstable_poc.htmluaf15.patch
- https://source.chromium.org/chromium/chromium/src/+/6742751ccf591d9ba1670c3082efbf345084b7d0
- https://source.chromium.org/chromium/chromium/src/+/main:components/language_detection/content/renderer/language_detection_agent.cc;l=107?q=LanguageDetectionAgent::WasShown&ss=chromium%2Fchromium%2Fsrc
- https://source.chromium.org/chromium/chromium/src/+/main:components/language_detection/content/renderer/language_detection_agent.cc;l=107?q=LanguageDetectionAgent::WasShown&ss=chromium/chromium/src
- https://source.chromium.org/chromium/chromium/src/+/main:components/language_detection/core/language_detection_model.cc;l=230?q=LanguageDetectionModel::NotifyModelLoaded&ss=chromium%2Fchromium%2Fsrc
- https://source.chromium.org/chromium/chromium/src/+/main:components/language_detection/core/language_detection_model.cc;l=230?q=LanguageDetectionModel::NotifyModelLoaded&ss=chromium/chromium/src