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

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Translate
DescriptionUse after free in Translate
ComponentTranslate
Sub-componentUI>Browser>Language>Translate
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.
Tracker379516109
Fix commit9c299bcdb77c (chromium/src) +9/-3
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Creditedlime(@limeSec_) from TIANGONG Team of Legendsec at QI-ANXIN Group
Reported2024-11-18 (per vendor advisory)
Fix landed2024-11-29 (public git)
Disclosed2024-12-10
Patch gap11 days — fix public in source before the advisory

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 a ModelLoadedCallback to 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 by NotifyModelLoaded().
Iterator invalidation
when a std::vector grows past its capacity during a range-for loop, 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.

Key insight
The function ran untrusted re-entrant callbacks while iterating a member container that those callbacks could grow. Swapping the container into a stack-local vector before dispatch separates the loop’s storage from any changes the callbacks make.

Attack Path

  1. Model load pending While the language detection model is still loading, one or more clients register ModelLoadedCallbacks, which are queued in model_loaded_callbacks_.
  2. Load completes NotifyModelLoaded() runs and walks model_loaded_callbacks_ directly, with loaded_ still false.
  3. Re-entrant registration A callback’s handling causes another load-completion callback to be registered on the same model. Because loaded_ is still false, it is pushed onto model_loaded_callbacks_ (inferred: the add path is not in the diff).
  4. Reallocation frees the iterated buffer If the push exceeds capacity, the vector reallocates, and the range-for iterator keeps reading moved-from ModelLoadedCallback storage in the freed buffer, which is a heap UAF.
  5. 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

The bug is a heap use-after-free of a 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

FunctionChangeNotes
for
components/language_detection/core/language_detection_model.cc
modified

Files Changed

  • components/language_detection/core/language_detection_model.cc

Audit Directions

  • Callback-list dispatch over members
    Search for for (auto&& cb : member_callbacks_) std::move(cb).Run(...) patterns where the callback can reach an Add*Callback/Register* method on the same object. Prefer swap-then-iterate, base::OnceCallbackList, or posting each callback as a task.
  • State flag set after dispatch
    Look 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 notification
    In NotifyModelLoaded() and similar code, check whether a callback can destroy the owning object. The post-loop loaded_ = true write would then touch freed this, and the swap fix does not address that case.
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
Loading diff…

Original Bug Report

reported by li...@gmail.com

AddressSanitizer:heap-use-after-free on LanguageDetectionModel::NotifyModelLoaded

Steps to reproduce the problem

repro:

  1. launch chrome with –enable-features=LanguageDetectionAPI

  2. apply patch.diff which simplify trigger

  3. run poc.html.

Problem Description

  1. In the function LanguageDetectionModel:: NotifyModelLoaded, there is an iterator operation. When AILanguageDetector performs detection, the callback OnDetectComplete will be added to the model_loaded_callbacks_, and the OnDetectComplete function body will have a user-defined callback resolve ->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");
    }
  }
}
  1. LanguageDetectionModel:: NotifyModelLoaded is called in LanguageDetectionAgent:: updateLanguageDetectionModel, which can be traced back to LanguageDetectionAgent:: 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 the model_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()));
}
  1. 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

  2. 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

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

View on issue tracker