CVE-2026-78964
Overview
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 completioncallback_. - `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.
Attack Path
- Start statistics collection
browser code creates a
DeviceStatisticsTrackerand callsStart()with a callback (this is a sync internal flow, not a web-exposed API). - 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. - 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.
- 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
Files Changed
components/sync/service/device_statistics_tracker.hcomponents/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 contractsfind
Start()/Fetch()-style APIs with comments promising cancellation on destruction, and check every asynchronous exit path (early returns, error branches) against that promise. - `WeakPtrFactory` placementconfirm 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.
Patch
From d2188669d75c4adebf43bffa5a40e822c86067cc Mon Sep 17 00:00:00 2001
From: Marc Treib <treib@chromium.org>
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 <justincohen@google.com>
Commit-Queue: Marc Treib <treib@chromium.org>
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