Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Updater
DescriptionUse after free in Updater
ComponentUpdater
Bug ClassUAF
Tracker516926115
Fix commitef27618f9cee (chromium/src) +18/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
chrome/updater/app/app_server_win.cc
modified

Files Changed

  • chrome/updater/app/app_server_win.cc
  • chrome/updater/app/app_server_win.h
From ef27618f9ceea2e9e4d83a419d50b9705ef0c082 Mon Sep 17 00:00:00 2001
From: Noah Rose Ledesma <noahrose@google.com>
Date: Wed, 27 May 2026 10:20:06 -0700
Subject: [PATCH] Synchronize access to `on_service_stopping_`

Access to the `on_service_stopping_` callback in AppServerWin occurs on
COM RPC threads. It's already known that `Stop` can be invoked multiple
times; given that the updater uses MTA, we ought to be careful about
concurrent access too.

Bug: 516926115
Change-Id: I9080f5d89ba195a01a5a14ed84e44f0c6a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7879166
Reviewed-by: Sorin Jianu <sorin@chromium.org>
Reviewed-by: S Ganesh <ganesh@chromium.org>
Commit-Queue: Noah Rose Ledesma <noahrose@google.com>
Cr-Commit-Position: refs/heads/main@{#1637052}
---

diff --git a/chrome/updater/app/app_server_win.cc b/chrome/updater/app/app_server_win.cc
index 325e1d5..4a02886 100644
--- a/chrome/updater/app/app_server_win.cc
+++ b/chrome/updater/app/app_server_win.cc
@@ -27,6 +27,7 @@
 #include "base/notreached.h"
 #include "base/strings/strcat.h"
 #include "base/strings/utf_string_conversions.h"
+#include "base/synchronization/lock.h"
 #include "base/task/sequenced_task_runner.h"
 #include "base/types/expected_macros.h"
 #include "base/win/atl.h"
@@ -266,8 +267,13 @@
     // service process.
     // It is possible for `Stop` to be called multiple times, so check for a
     // valid `on_service_stopping_` callback before calling `Run`.
-    if (on_service_stopping_) {
-      std::move(on_service_stopping_).Run();
+    base::OnceClosure on_service_stopping;
+    {
+      base::AutoLock lock(on_service_stopping_lock_);
+      on_service_stopping = std::move(on_service_stopping_);
+    }
+    if (on_service_stopping) {
+      std::move(on_service_stopping).Run();
     }
   }
   UnregisterClassObjects();
@@ -283,8 +289,12 @@
 }
 
 HRESULT AppServerWin::RunCOMServer(base::OnceClosure on_service_stopping) {
-  on_service_stopping_ = std::move(on_service_stopping);
+  {
+    base::AutoLock lock(on_service_stopping_lock_);
+    on_service_stopping_ = std::move(on_service_stopping);
+  }
   absl::Cleanup reset_on_service_stopping = [&] {
+    base::AutoLock lock(on_service_stopping_lock_);
     on_service_stopping_.Reset();
   };
   return Run();
diff --git a/chrome/updater/app/app_server_win.h b/chrome/updater/app/app_server_win.h
index d76d060ec..a5a7d0a7 100644
--- a/chrome/updater/app/app_server_win.h
+++ b/chrome/updater/app/app_server_win.h
@@ -11,7 +11,9 @@
 
 #include "base/functional/callback.h"
 #include "base/memory/scoped_refptr.h"
+#include "base/synchronization/lock.h"
 #include "base/task/sequenced_task_runner.h"
+#include "base/thread_annotations.h"
 #include "chrome/updater/app/app_server.h"
 #include "chrome/updater/update_service.h"
 #include "chrome/updater/update_service_internal.h"
@@ -110,7 +112,9 @@
   std::unique_ptr<UpdateServiceInternalStub> active_duty_internal_stub_;
   std::unique_ptr<UpdateServiceStub> active_duty_stub_;
 
-  base::OnceClosure on_service_stopping_;
+  // Guards `on_service_stopping_`, which is accessed on COM RPC threads.
+  base::Lock on_service_stopping_lock_;
+  base::OnceClosure on_service_stopping_ GUARDED_BY(on_service_stopping_lock_);
 };
 
 // Returns the singleton AppServerWin instance.
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Race Condition in AppServerWin::Stop() Leads to Use-After-Free in GoogleUpdater

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 in GoogleUpdater’s AppServerWin::Stop() method on Windows allows unsynchronized concurrent execution of the on_service_stopping_ callback. This can lead to a double-release of the underlying BindState, causing a potential Use-After-Free (UAF) vulnerability. If successfully exploited, a local non-admin user could potentially achieve local privilege escalation to SYSTEM.

Affected files:

  • chrome/updater/app/app_server_win.cc
  • chrome/updater/app/app_server_win.h

Estimated timestamp from git blame: 2025-12-11

Summary

A potential race condition exists in the Chromium updater’s AppServerWin::Stop() implementation due to unsynchronized access to the on_service_stopping_ callback. This callback is executed when the system-level COM server is shutting down. Because the callback is invoked and moved without thread synchronization, concurrent execution across multiple threads (such as COM RPC threads and the Service Control Manager thread) can cause a double-release of the underlying BindStateBase, potentially leading to a Use-After-Free (UAF) vulnerability.

Root Cause Analysis

In chrome/updater/app/app_server_win.cc, AppServerWin::CreateWRLModule() registers AppServerWin::Stop as the WRL Module release notifier:

void AppServerWin::CreateWRLModule() {
  Microsoft::WRL::Module<Microsoft::WRL::OutOfProc>::Create(
      this, &AppServerWin::Stop);
}

AppServerWin::Stop() can also be called via the SCM (Service Control Manager) dispatcher thread when the service is stopped by the OS.

The Stop() method is implemented as follows:

void AppServerWin::Stop() {
  VLOG(2) << __func__ << ": COM server is shutting down.";
  if (IsSystemInstall(updater_scope())) {
    if (on_service_stopping_) {
      std::move(on_service_stopping_).Run();
    }
  }
  UnregisterClassObjects();
  ...
}

If Thread A (e.g., an SCM stop thread) and Thread B (e.g., a COM RPC thread executing due to a transition to zero objects) call Stop() concurrently, the following sequence of events can occur:

  1. Both threads evaluate the if (on_service_stopping_) check as true concurrently before either can clear or move the member variable.
  2. Both threads proceed to execute std::move(on_service_stopping_).Run().
  3. In base/functional/callback.h, OnceCallback::Run() && moves the internal state holder:
    internal::BindStateHolder holder = std::move(holder_);
    
  4. This invokes the move constructor of scoped_refptr<BindStateBase> in base/memory/scoped_refptr.h:
    scoped_refptr(scoped_refptr&& r) noexcept : ptr_(r.ptr_) { r.ptr_ = nullptr; }
    
  5. Because this move operation is non-atomic, both threads can copy the same raw pointer address r.ptr_ into their respective local holder variables before either thread assigns nullptr to the source pointer.
  6. Both threads successfully execute the check CHECK(!is_null()) and invoke the callback.
  7. When the local holder variables on both threads go out of scope, their destructors invoke Release(ptr_) on the same BindStateBase pointer.
  8. The first destructor decrements the reference count to 0, deleting the BindState object. The second destructor attempts to release the already-deleted pointer, causing a Use-After-Free.

Because scoped_refptr::ptr_ is annotated with the RAW_PTR_EXCLUSION macro, this smart pointer is not protected by MiraclePtr/BackupRefPtr.

Potential Exploitation Path

Note: These are suggested and potential steps, as our analysis is static and we have not executed a live proof-of-concept.

  1. An unprivileged local user logged into an interactive session (who is explicitly granted COM execute permissions via the Interactive SID) activates the updater’s COM classes.
  2. The attacker triggers rapid reference additions and releases (or coordinates a COM release with an SCM service stop request) to trigger concurrent invocations of AppServerWin::Stop() on separate threads.
  3. If the race condition is successfully triggered, the underlying BindState is double-released.
  4. By reclaiming the freed heap memory of the BindState before the second thread’s destructor runs, the attacker could overwrite the destructor_ function pointer.
  5. When the second thread executes destructor_(this), control flow is redirected, potentially allowing arbitrary code execution in the context of the service (NT AUTHORITY\SYSTEM). This would result in Local Privilege Escalation (LPE).

Suggested Fix

Protect the access to on_service_stopping_ with a mutex or base::Lock to perform a synchronized move-and-invoke, similar to the pattern utilized in ModuleReleaseHelper within chrome/windows_services/service_program/process_wrl_module.cc:

void AppServerWin::Stop() {
  VLOG(2) << __func__ << ": COM server is shutting down.";
  base::OnceClosure on_stopping;
  {
    base::AutoLock lock(lock_);
    on_stopping = std::move(on_service_stopping_);
  }
  if (on_stopping) {
    std::move(on_stopping).Run();
  }
  UnregisterClassObjects();
  ...
}

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