CVE-2026-14018
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/updater/app/server/win/com_classes_legacy.cc |
modified |
Files Changed
chrome/updater/app/app_server.ccchrome/updater/app/app_server.hchrome/updater/app/server/win/com_classes_legacy.cc
Patch
From 4fac708225508abff10cd79ee1d71e3fbd72b4b1 Mon Sep 17 00:00:00 2001 From: S Ganesh <[email protected]> Date: Fri, 29 May 2026 15:16:58 -0700 Subject: [PATCH] updater: Fix shutdown race condition in PolicyStatus COM class During service shutdown, AppServer::Uninitialize() resets its scoped_refptr<Configurator> member config_ to nullptr on the main sequence thread. However, if a COM client concurrently calls CoCreateInstance/CreateInstance on an MTA RPC thread, the PolicyStatusImpl constructor could synchronously fetch the Configurator pointer via AppServer::config() to obtain the policy service, causing a Use-After-Free (UAF) data race on the scoped_refptr. This CL resolves this vulnerability by: 1. Protecting config_ reads and assignments inside AppServer under base::Lock config_lock_, ensuring thread safety when WRL COM threads query for configurator instance concurrently. 2. Guarding the config_ member in Clang with GUARDED_BY(config_lock_) annotations to enforce thread safety at compile time. 3. Moving the policy_service_ initialization from the PolicyStatusImpl constructor initializer list into RuntimeClassInitialize(). 4. Returning E_FAIL from RuntimeClassInitialize() if config_ has already been deallocated during shutdown, gracefully rejecting incoming client connections and preventing downstream nullptr dereferences. Bug: 517350251 Change-Id: If17bd561f75d11ccef86364e85a2c9e3160e8cb6 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7882258 Reviewed-by: Xiaoling Bao <[email protected]> Commit-Queue: S Ganesh <[email protected]> Cr-Commit-Position: refs/heads/main@{#1638757} --- diff --git a/chrome/updater/app/app_server.cc b/chrome/updater/app/app_server.cc index b7196744..064323d 100644 --- a/chrome/updater/app/app_server.cc +++ b/chrome/updater/app/app_server.cc @@ -21,6 +21,7 @@ #include "base/process/launch.h" #include "base/process/process.h" #include "base/run_loop.h" +#include "base/synchronization/lock.h" #include "base/time/time.h" #include "base/version.h" #include "build/build_config.h" @@ -98,12 +99,17 @@ if (!local_prefs->GetQualified()) { global_prefs = nullptr; prefs_ = local_prefs; - config_ = base::MakeRefCounted<Configurator>(prefs_, external_constants_, - updater_scope()); + scoped_refptr<Configurator> config; + { + base::AutoLock lock(config_lock_); + config_ = base::MakeRefCounted<Configurator>( + prefs_, external_constants_, updater_scope()); + config = config_; + } if (IsInternalService()) { return base::BindOnce( &AppServer::ActiveDutyInternal, this, - MakeQualifyingUpdateServiceInternal(config_, local_prefs)); + MakeQualifyingUpdateServiceInternal(config, local_prefs)); } #if BUILDFLAG(IS_WIN) @@ -140,11 +146,16 @@ server_starts_ = global_prefs->CountServerStarts(); prefs_ = global_prefs; - config_ = base::MakeRefCounted<Configurator>(prefs_, external_constants_, - updater_scope()); + scoped_refptr<Configurator> config; + { + base::AutoLock lock(config_lock_); + config_ = base::MakeRefCounted<Configurator>(prefs_, external_constants_, + updater_scope()); + config = config_; + } return base::BindOnce( &AppServer::ActiveDuty, this, - base::MakeRefCounted<UpdateServiceImpl>(updater_scope(), config_)); + base::MakeRefCounted<UpdateServiceImpl>(updater_scope(), config)); } void AppServer::TaskStarted() { @@ -176,9 +187,14 @@ } void AppServer::Uninitialize() { - if (config_ && config_->GetEventLogger()) { + scoped_refptr<Configurator> config; + { + base::AutoLock lock(config_lock_); + config = config_; + } + if (config && config->GetEventLogger()) { base::RunLoop run_loop; - config_->GetEventLogger()->Flush(run_loop.QuitClosure()); + config->GetEventLogger()->Flush(run_loop.QuitClosure()); run_loop.Run(); } // Simply stopping the timer does not destroy its task. The task holds a @@ -199,16 +215,24 @@ // Because this instance is leaky when running on Windows, the following // references must be reset to destroy the objects, otherwise `Prefs` leaks. prefs_ = nullptr; - config_ = nullptr; + { + base::AutoLock lock(config_lock_); + config_ = nullptr; + } } void AppServer::MaybeUninstall() { - if (!config_ || IsInternalService()) { + scoped_refptr<Configurator> config; + { + base::AutoLock lock(config_lock_); + config = config_; + } + if (!config || IsInternalService()) { return; } scoped_refptr<PersistedData> persisted_data = - config_->GetUpdaterPersistedData(); + config->GetUpdaterPersistedData(); if (ShouldUninstall(persisted_data->GetAppIds(), server_starts_, persisted_data->GetHadApps())) { std::optional<base::FilePath> executable = diff --git a/chrome/updater/app/app_server.h b/chrome/updater/app/app_server.h index 5bf028f..03f7d77 100644 --- a/chrome/updater/app/app_server.h +++ b/chrome/updater/app/app_server.h @@ -8,6 +8,8 @@ #include "base/functional/callback_forward.h" #include "base/memory/scoped_refptr.h" #include "base/sequence_checker.h" +#include "base/synchronization/lock.h" +#include "base/thread_annotations.h" #include "base/timer/timer.h" #include "chrome/updater/app/app.h" #include "chrome/updater/configurator.h" @@ -41,7 +43,12 @@ scoped_refptr<const UpdaterPrefs> prefs() const { return prefs_; } - scoped_refptr<Configurator> config() const { return config_; } + // Thread-safe: COM MTA RPC threads access this getter concurrently + // with main sequence writes during shutdown. + scoped_refptr<Configurator> config() const { + base::AutoLock lock(config_lock_); + return config_; + } void TaskStarted(); void TaskCompleted(); @@ -104,7 +111,8 @@ CreateExternalConstants(); base::OnceClosure first_task_; scoped_refptr<UpdaterPrefs> prefs_; - scoped_refptr<Configurator> config_; + mutable base::Lock config_lock_; + scoped_refptr<Configurator> config_ GUARDED_BY(config_lock_); base::RepeatingTimer hang_timer_; // If true, this version of the updater uninstalls itself during shutdown. diff --git a/chrome/updater/app/server/win/com_classes_legacy.cc b/chrome/updater/app/server/win/com_classes_legacy.cc index 50696aca..8941239 100644 --- a/chrome/updater/app/server/win/com_classes_legacy.cc +++ b/chrome/updater/app/server/win/com_classes_legacy.cc @@ -1416,14 +1416,26 @@ {IID_MAP_ENTRY_SYSTEM(IPolicyStatus4), IID_MAP_ENTRY_SYSTEM(IPolicyStatus3), IID_MAP_ENTRY_SYSTEM(IPolicyStatus2), - IID_MAP_ENTRY_SYSTEM(IPolicyStatus)}), - policy_service_(GetAppServerWinInstance()->config()->GetPolicyService()) { -} + IID_MAP_ENTRY_SYSTEM(IPolicyStatus)}) {} PolicyStatusImpl::~PolicyStatusImpl() = default; HRESULT PolicyStatusImpl::RuntimeClassInitialize() { VLOG(2) << __func__; LogComCaller(__FUNCTION__); + + scoped_refptr<AppServerWin> app_server = GetAppServerWinInstance(); + if (!app_server) { + return E_FAIL; + } + scoped_refptr<Configurator> config = app_server->config(); + if (!config) { + return E_FAIL; + } + policy_service_ = config->GetPolicyService(); + if (!policy_service_) { + return E_FAIL; + } +
Original Bug Report
Potential Use-After-Free in GoogleUpdater due to unsynchronized access of config_ during shutdown
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 race condition exists in the GoogleUpdater service on Windows during shutdown between a COM MTA RPC thread and the main sequence thread. While the COM thread accesses and copy-constructs a scoped_refptr to Configurator via AppServer::config(), the main thread can simultaneously clear config_ inside AppServer::Uninitialize(). This unsynchronized access can lead to a Use-After-Free (UAF) of the Configurator object.
Affected files:
chrome/updater/app/server/win/com_classes_legacy.ccchrome/updater/app/app_server.ccchrome/updater/app/app_server.h
Estimated timestamp from git blame: 2023-07-13
Description
A potential race condition exists in the GoogleUpdater (Omaha) service on Windows between a COM Multithreaded Apartment (MTA) RPC thread and the main sequence thread during service shutdown, potentially leading to a Use-After-Free (UAF) vulnerability.
Specifically, the PolicyStatusImpl constructor executes on a COM RPC thread (inside SimpleClassFactory<PolicyStatusImpl>::CreateInstance) and retrieves the Configurator instance via GetAppServerWinInstance()->config() to obtain the policy service:
// chrome/updater/app/server/win/com_classes_legacy.cc
PolicyStatusImpl::PolicyStatusImpl()
: IDispatchImpl<IPolicyStatus4,
...
IPolicyStatus>({IID_MAP_ENTRY_USER(IPolicyStatus4),
...}),
policy_service_(GetAppServerWinInstance()->config()->GetPolicyService()) {
}
Concurrently, the main sequence’s AppServer::Uninitialize() executes as part of the shutdown process, setting config_ to nullptr:
// chrome/updater/app/app_server.cc
void AppServer::Uninitialize() {
...
prefs_ = nullptr;
config_ = nullptr; // Releases and potentially deletes Configurator
}
Root Cause Analysis
- Lack of Thread Safety: The
scoped_refptr<T>class is not thread-safe for concurrent read/write operations on the smart pointer itself. Inbase/memory/scoped_refptr.h, copy-constructing ascoped_refptrperforms a non-atomic load of the internal raw pointer (ptr_), followed byAddRef(). - Race Window: While the main thread resets
config_tonullptrinsideUninitialize()(which releases the last reference to theConfiguratorobject, causing it to be deleted), the COM RPC thread can concurrently copyconfig_insideAppServer::config(). - Use-After-Free: If the COM RPC thread loads the raw pointer of
Configuratorright before the main thread clears and deletes it, the COM thread will callAddRef()or subsequent virtual methods (such asGetPolicyService()) on the freedConfiguratormemory. This results in a Use-After-Free. - No MiraclePtr Protection: Because
scoped_refptrmanages ownership via reference-counting and holds a standard raw C++ pointer internally, it is completely exempt from BackupRefPtr (BRP) / MiraclePtr protection.
Suggested / Potential Attack Steps
Note: Our analysis was performed using static review tools; we do not currently have the ability to run code or provide a working proof of concept (PoC).
- From a non-privileged client process, initialize COM MTA (
CoInitializeEx(NULL, COINIT_MULTITHREADED)). - Call
CoCreateInstance(CLSID_PolicyStatusSystemClass, NULL, CLSCTX_LOCAL_SERVER, ...)to demand-start the system-scope GoogleUpdater service (running as NT AUTHORITY/SYSTEM). - Obtain a class-factory proxy for
CLSID_PolicyStatusSystemClassviaCoGetClassObject. - On a background thread, repeatedly trigger
factory->CreateInstance(NULL, ...)to execute thePolicyStatusImplconstructor on a COM RPC thread. - On the main thread, release the initially obtained instance to force the service’s WRL module count to drop to 0. This initiates service shutdown on the main sequence via
AppServer::Uninitialize(). - Observe if the concurrent MTA thread retrieves the stale raw pointer of
Configuratorfromconfig_and triggers a UAF upon callingAddRef()orRelease()on the freed object.
Suggested Fix
To resolve this issue, access to config_ must be synchronized, or initialization of policy_service_ should be marshaled to the main sequence.
For example, we can protect config_ with a lock inside AppServer, or ensure PolicyStatusImpl retrieves policy_service_ via a posted task running on the main sequence instead of calling config() directly on the COM RPC thread.
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
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.