Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Updater
DescriptionUse after free in Updater
ComponentUpdater
Bug ClassUAF
Tracker517350251
Fix commit4fac70822550 (chromium/src) +60/-16
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
chrome/updater/app/server/win/com_classes_legacy.cc
modified

Files Changed

  • chrome/updater/app/app_server.cc
  • chrome/updater/app/app_server.h
  • chrome/updater/app/server/win/com_classes_legacy.cc
From 4fac708225508abff10cd79ee1d71e3fbd72b4b1 Mon Sep 17 00:00:00 2001
From: S Ganesh <ganesh@chromium.org>
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 <xiaolingbao@chromium.org>
Commit-Queue: S Ganesh <ganesh@chromium.org>
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;
+  }
+
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • chrome/updater/app/app_server.cc
  • chrome/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

  1. Lack of Thread Safety: The scoped_refptr<T> class is not thread-safe for concurrent read/write operations on the smart pointer itself. In base/memory/scoped_refptr.h, copy-constructing a scoped_refptr performs a non-atomic load of the internal raw pointer (ptr_), followed by AddRef().
  2. Race Window: While the main thread resets config_ to nullptr inside Uninitialize() (which releases the last reference to the Configurator object, causing it to be deleted), the COM RPC thread can concurrently copy config_ inside AppServer::config().
  3. Use-After-Free: If the COM RPC thread loads the raw pointer of Configurator right before the main thread clears and deletes it, the COM thread will call AddRef() or subsequent virtual methods (such as GetPolicyService()) on the freed Configurator memory. This results in a Use-After-Free.
  4. No MiraclePtr Protection: Because scoped_refptr manages 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).

  1. From a non-privileged client process, initialize COM MTA (CoInitializeEx(NULL, COINIT_MULTITHREADED)).
  2. Call CoCreateInstance(CLSID_PolicyStatusSystemClass, NULL, CLSCTX_LOCAL_SERVER, ...) to demand-start the system-scope GoogleUpdater service (running as NT AUTHORITY/SYSTEM).
  3. Obtain a class-factory proxy for CLSID_PolicyStatusSystemClass via CoGetClassObject.
  4. On a background thread, repeatedly trigger factory->CreateInstance(NULL, ...) to execute the PolicyStatusImpl constructor on a COM RPC thread.
  5. 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().
  6. Observe if the concurrent MTA thread retrieves the stale raw pointer of Configurator from config_ and triggers a UAF upon calling AddRef() or Release() 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.

View on issue tracker