Low watch CVSS 9.6 chrome UAF 🔧 Commit mapped

Overview

Low
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Sync
DescriptionUse after free in Sync
ComponentSync
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.
Tracker539341100
Fix commitd2188669d75c (chromium/src) +19/-2
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-07-27 (per vendor advisory)
Fix landed2026-07-27 (public git)
Disclosed2026-08-25
Patch gap29 days — fix public in source before the advisory

Background

`DeviceStatisticsTracker`
a browser-process sync helper (components/sync/service/device_statistics_tracker.cc) that gathers device and platform statistics for the signed-in accounts and then calls a completion callback_.
`PostTask` of a bare `OnceClosure`
posting std::move(callback_) directly hands the closure to the task runner, so it runs later whether or not the object that posted it still exists.
`base::WeakPtrFactory` / `GetWeakPtr()`
binding a method to a weak pointer makes the posted task a no-op if the object is destroyed before the task runs.
Start() contract
according to the commit message, a comment on Start() promises callers that the callback will not run after the tracker is destroyed.

Root Cause Analysis

On two completion paths in device_statistics_tracker.cc, the tracker posted its stored callback_ straight to base::SequencedTaskRunner::GetCurrentDefault(). One path is the early exit when primary_account_ is no longer in the account list; the other comes after RecordOverallDevicesOutcome() and RecordOverallPlatformsOutcome(). The enclosing function names are not shown in the hunks. Because the closure now belonged to the task queue, destroying the tracker between the post and the task running did not cancel it, and the callback still ran. That breaks the documented promise that callers rely on. A caller that destroys the tracker, and possibly the state the callback refers to, can then have the callback run against freed memory (inferred: the callback’s bound state is not shown in the diff).

The fix adds a weak_ptr_factory_ as the last member, adds a private RunCallback(base::OnceClosure) method, and posts base::BindOnce(&DeviceStatisticsTracker::RunCallback, weak_ptr_factory_.GetWeakPtr(), std::move(callback_)). If the tracker is gone, the weak pointer is invalid, and the bound callback is dropped without running.

Key insight
The mistake was posting a lifetime-sensitive callback as a free-standing task, which separates its execution from the owner’s lifetime. Sending it through a weak-pointer-bound member method makes destroying the tracker cancel the pending callback, as the API contract requires.

Attack Path

  1. Start statistics collection browser code creates a DeviceStatisticsTracker and calls Start() with a callback (this is a sync internal flow, not a web-exposed API).
  2. Reach a posting completion path the tracker finishes, either because the primary account was removed, or because outcome recording completes, and posts callback_ as a task.
  3. Destroy the tracker before the task runs the owner tears the tracker down, for example on sign-out, account change or service shutdown (inferred trigger), relying on the documented guarantee.
  4. Stale callback runs the posted closure runs anyway and uses objects that have already been freed, giving a browser-process use-after-free (inferred; the sink is in the caller’s callback and is not shown).

Impact Assessment

The advisory classifies this as a use-after-free in Sync and gives no in-the-wild status. The affected code runs in the privileged browser process, but triggering it depends on the timing of account or sync lifecycle events, not on web content; no remote, web-reachable trigger is shown. Memory corruption leading to code execution in the browser process would be (inferred).

Files Changed

  • components/sync/service/device_statistics_tracker.h
  • components/sync/service/device_statistics_tracker.cc

Audit Directions

  • `PostTask(FROM_HERE, std::move(callback_))`
    search browser-side components for completion callbacks posted bare from objects whose API promises “no callback after destruction”; each such post needs a weak-pointer or cancelable wrapper.
  • Documented lifetime contracts
    find Start()/Fetch()-style APIs with comments promising cancellation on destruction, and check every asynchronous exit path (early returns, error branches) against that promise.
  • `WeakPtrFactory` placement
    confirm the factory is the last member and that weak pointers are used for every deferred self-reference, including callbacks routed through helper tasks and timers.
From d2188669d75c4adebf43bffa5a40e822c86067cc Mon Sep 17 00:00:00 2001
From: Marc Treib <[email protected]>
Date: Mon, 27 Jul 2026 06:35:34 -0700
Subject: [PATCH] Fix use-after-free in DeviceStatisticsTracker

DeviceStatisticsTracker promises (via a comment on Start() that its
callback is not run anymore if the tracker is destroyed. Before this CL,
there were some edge cases where this was not true, since the callback
was run as a posted task.

This CL fixes this by running the callback through a class method, bound
to a weak ptr.

Fixed: 539341100
Change-Id: Ie95a230ddceea391bdf0e7aadbdf9c8e6a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8154099
Reviewed-by: Justin Cohen <[email protected]>
Commit-Queue: Marc Treib <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1668589}
---
diff --git a/components/sync/service/device_statistics_tracker.cc b/components/sync/service/device_statistics_tracker.cc
index ae37196..0058d69 100644
--- a/components/sync/service/device_statistics_tracker.cc
+++ b/components/sync/service/device_statistics_tracker.cc
@@ -228,8 +228,12 @@
       !std::ranges::contains(accounts, primary_account_)) {
     // The primary account must have been removed between the constructor and
     // now, or something's wrong with the IdentityManager.
+    // Note: This is plumbed through a class method to ensure that the callback
+    // isn't run anymore if `this` gets destroyed first.
     base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
-        FROM_HERE, std::move(callback_));
+        FROM_HERE,
+        base::BindOnce(&DeviceStatisticsTracker::RunCallback,
+                       weak_ptr_factory_.GetWeakPtr(), std::move(callback_)));
     return;
   }
 
@@ -247,8 +251,12 @@
     RecordOverallDevicesOutcome();
     RecordOverallPlatformsOutcome();
 
+    // Note: This is plumbed through a class method to ensure that the callback
+    // isn't run anymore if `this` gets destroyed first.
     base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
-        FROM_HERE, std::move(callback_));
+        FROM_HERE,
+        base::BindOnce(&DeviceStatisticsTracker::RunCallback,
+                       weak_ptr_factory_.GetWeakPtr(), std::move(callback_)));
     return;
   }
 
@@ -757,4 +765,8 @@
   return deduped_devices;
 }
 
+void DeviceStatisticsTracker::RunCallback(base::OnceClosure callback) {
+  std::move(callback).Run();
+}
+
 }  // namespace syncer
diff --git a/components/sync/service/device_statistics_tracker.h b/components/sync/service/device_statistics_tracker.h
index 0ce7777..667ce12 100644
--- a/components/sync/service/device_statistics_tracker.h
+++ b/components/sync/service/device_statistics_tracker.h
@@ -14,6 +14,7 @@
 #include "base/containers/flat_set.h"
 #include "base/functional/callback.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
 #include "base/types/expected.h"
 #include "components/signin/public/identity_manager/account_info.h"
 #include "google_apis/gaia/gaia_id.h"
@@ -260,6 +261,8 @@
       const std::vector<sync_pb::SyncEntity>& entities,
       const base::flat_set<std::string>& current_device_cache_guids);
 
+  void RunCallback(base::OnceClosure callback);
+
   const raw_ptr<signin::IdentityManager> identity_manager_;
 
   const GURL sync_server_url_;
@@ -293,6 +296,8 @@
   // devices (which may be empty).
   base::flat_map<GaiaId, base::expected<std::vector<DeviceData>, RequestFailed>>
       other_devices_by_gaia_;
+
+  base::WeakPtrFactory<DeviceStatisticsTracker> weak_ptr_factory_{this};
 };
 
 }  // namespace syncer
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.