Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Updater
DescriptionUse after free in Updater
ComponentUpdater
Bug ClassUAF
Tracker501370283
Fix commit06e8dee7abd5 (chromium/src) +107/-85
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

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

Files Changed

  • chrome/updater/app/server/win/com_classes_legacy.cc
From 06e8dee7abd51aead77a853d2028e79b9df663a2 Mon Sep 17 00:00:00 2001
From: Noah Rose Ledesma <noahrose@google.com>
Date: Fri, 10 Apr 2026 13:18:40 -0700
Subject: [PATCH] Use PolicyStatusResult consistently in legacy COM

Sequence all access to the policy service object. To facilitate usage in
COM implementations, implement the AreUpdatesSuppressedNow predicate as
a free function.

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

diff --git a/chrome/updater/app/server/win/com_classes_legacy.cc b/chrome/updater/app/server/win/com_classes_legacy.cc
index 608c6097..9974f975 100644
--- a/chrome/updater/app/server/win/com_classes_legacy.cc
+++ b/chrome/updater/app/server/win/com_classes_legacy.cc
@@ -65,6 +65,8 @@
 #include "components/update_client/protocol_definition.h"
 #include "components/update_client/update_client.h"
 
+namespace updater {
+
 namespace {
 
 HRESULT OpenCallerProcessHandle(DWORD proc_id,
@@ -87,9 +89,42 @@
   return {};
 }
 
-}  // namespace
+// Holds the result of the IPC to retrieve PolicyService data.
+template <typename T>
+class PolicyStatusResult
+    : public base::RefCountedThreadSafe<PolicyStatusResult<T>> {
+ public:
+  using ValueGetter = base::RepeatingCallback<PolicyStatus<T>()>;
 
-namespace updater {
+  static auto Get(ValueGetter value_getter) {
+    auto result = base::WrapRefCounted(new PolicyStatusResult<T>(value_getter));
+    AppServerWin::PostRpcTask(
+        base::BindOnce(&PolicyStatusResult::GetValueOnSequence, result));
+    result->completion_event.TimedWait(base::Seconds(60));
+    return result->value;
+  }
+
+ private:
+  friend base::RefCountedThreadSafe<PolicyStatusResult<T>>;
+  virtual ~PolicyStatusResult() = default;
+
+  explicit PolicyStatusResult(ValueGetter value_getter)
+      : value_getter(value_getter) {}
+
+  void GetValueOnSequence() {
+    PolicyStatus<T> policy_status = value_getter.Run();
+    if (policy_status) {
+      value = policy_status;
+    }
+    completion_event.Signal();
+  }
+
+  ValueGetter value_getter;
+  std::optional<PolicyStatus<T>> value;
+  base::WaitableEvent completion_event;
+};
+
+}  // namespace
 
 // Implements `IAppVersionWeb`.
 class AppVersionWebImpl : public IDispatchImpl<IAppVersionWeb> {
@@ -1398,12 +1433,13 @@
     return E_INVALIDARG;
   }
 
-  PolicyStatus<base::TimeDelta> period = policy_service_->GetLastCheckPeriod();
+  auto period = PolicyStatusResult<base::TimeDelta>::Get(
+      base::BindRepeating(&PolicyService::GetLastCheckPeriod, policy_service_));
   if (!period) {
     return E_FAIL;
   }
 
-  *minutes = period.policy().InMinutes();
+  *minutes = period->policy().InMinutes();
   return S_OK;
 }
 
@@ -1420,17 +1456,21 @@
     return E_INVALIDARG;
   }
 
-  PolicyStatus<UpdatesSuppressedTimes> updates_suppressed_times =
-      policy_service_->GetUpdatesSuppressedTimes();
-  if (!updates_suppressed_times || !updates_suppressed_times.policy().valid()) {
+  auto updates_suppressed_times =
+      PolicyStatusResult<UpdatesSuppressedTimes>::Get(base::BindRepeating(
+          &PolicyService::GetUpdatesSuppressedTimes, policy_service_));
+  if (!updates_suppressed_times ||
+      !updates_suppressed_times->policy().valid()) {
     return E_FAIL;
   }
 
-  *start_hour = updates_suppressed_times.policy().start_hour_;
-  *start_min = updates_suppressed_times.policy().start_minute_;
-  *duration_min = updates_suppressed_times.policy().duration_minute_;
+  *start_hour = updates_suppressed_times->policy().start_hour_;
+  *start_min = updates_suppressed_times->policy().start_minute_;
+  *duration_min = updates_suppressed_times->policy().duration_minute_;
   *are_updates_suppressed =
-      policy_service_->AreUpdatesSuppressedNow() ? VARIANT_TRUE : VARIANT_FALSE;
+      AreUpdatesSuppressedNow(updates_suppressed_times->policy())
+          ? VARIANT_TRUE
+          : VARIANT_FALSE;
 
   return S_OK;
 }
@@ -1440,13 +1480,14 @@
     return E_INVALIDARG;
   }
 
-  PolicyStatus<std::string> download_preference =
-      policy_service_->GetDownloadPreference();
+  auto download_preference =
+      PolicyStatusResult<std::string>::Get(base::BindRepeating(
+          &PolicyService::GetDownloadPreference, policy_service_));
   if (!download_preference) {
     return E_FAIL;
   }
 
-  *pref = base::win::ScopedBstr(base::UTF8ToWide(download_preference.policy()))
+  *pref = base::win::ScopedBstr(base::UTF8ToWide(download_preference->policy()))
               .Release();
   return S_OK;
 }
@@ -1456,13 +1497,13 @@
     return E_INVALIDARG;
   }
 
-  PolicyStatus<int> cache_size_limit =
-      policy_service_->GetPackageCacheSizeLimitMBytes();
+  auto cache_size_limit = PolicyStatusResult<int>::Get(base::BindRepeating(
+      &PolicyService::GetPackageCacheSizeLimitMBytes, policy_service_));
   if (!cache_size_limit) {
     return E_FAIL;
   }
 
-  *limit = cache_size_limit.policy();
+  *limit = cache_size_limit->policy();
   return S_OK;
 }
 
@@ -1471,13 +1512,13 @@
     return E_INVALIDARG;
   }
 
-  PolicyStatus<int> cache_life_limit =
-      policy_service_->GetPackageCacheExpirationTimeDays();
+  auto cache_life_limit = PolicyStatusResult<int>::Get(base::BindRepeating(
+      &PolicyService::GetPackageCacheExpirationTimeDays, policy_service_));
   if (!cache_life_limit) {
     return E_FAIL;
   }
 
-  *days = cache_life_limit.policy();
+  *days = cache_life_limit->policy();
   return S_OK;
 }
 
@@ -1488,13 +1529,14 @@
     return E_INVALIDARG;
   }
 
-  PolicyStatus<int> install_policy =
-      policy_service_->GetPolicyForAppInstalls(base::WideToUTF8(app_id));
+  auto install_policy = PolicyStatusResult<int>::Get(
+      base::BindRepeating(&PolicyService::GetPolicyForAppInstalls,
+                          policy_service_, base::WideToUTF8(app_id)));
   if (!install_policy) {
     return E_FAIL;
   }
 
-  *policy = install_policy.policy();
+  *policy = install_policy->policy();
   return S_OK;
 }
 
@@ -1504,13 +1546,14 @@
     return E_INVALIDARG;
   }
 
-  PolicyStatus<int> update_policy =
-      policy_service_->GetPolicyForAppUpdates(base::WideToUTF8(app_id));
+  auto update_policy = PolicyStatusResult<int>::Get(
+      base::BindRepeating(&PolicyService::GetPolicyForAppUpdates,
+                          policy_service_, base::WideToUTF8(app_id)));
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-After-Free in Updater PolicyService via unsynchronized COM RPC calls

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 without the Chrome Security team.

Overview: A potential race condition in the Google Updater allows a vector of policy managers to be accessed concurrently by COM RPC threads and the main sequence. If the vector is cleared during a policy refresh while being copied by an RPC thread, a Use-After-Free occurs. A local, non-admin user could potentially exploit this to achieve privilege escalation to SYSTEM.

Affected files:

  • chrome/updater/policy/service.cc
  • chrome/updater/policy/service.h
  • chrome/updater/app/server/win/com_classes_legacy.cc
  • chrome/updater/configurator.cc
  • chrome/windows_services/service_program/service.cc

Estimated timestamp from git blame: 2025-11-13

Description

A race condition exists in the Google Updater’s PolicyService that can lead to a Use-After-Free (UAF) vulnerability. This issue occurs because the sequence-affine PolicyService is accessed concurrently from COM RPC threads and the main application sequence without sufficient synchronization.

The PolicyStatusSystemClass COM object, running as a NT AUTHORITY\SYSTEM service, implements the legacy IPolicyStatus interface. The COM security DACL explicitly grants access to the Interactive group (S-1-5-4), allowing any locally logged-in standard user to interface with it.

Because the server runs in a Multi-Threaded Apartment (MTA), calls to these interfaces are handled on an RPC thread pool. Several property getters in IPolicyStatus (such as get_effectivePolicyForAppUpdates, get_targetVersionPrefix, and get_isRollbackToTargetVersionAllowed in chrome/updater/app/server/win/com_classes_legacy.cc) call PolicyService methods directly on the RPC thread, bypassing the intended AppServerWin::PostRpcTask sequence marshaling.

Inside PolicyService::QueryAppPolicy (called by these getters), the code iterates over the list of policy managers by calling policy_managers_->managers(). This method returns std::vector<scoped_refptr<PolicyManagerInterface>> by value, invoking the std::vector copy constructor on the RPC thread.

Concurrently, an attacker can trigger IPolicyStatus2::refreshPolicies(). This fetches policies asynchronously and eventually executes PolicyService::FetchPoliciesDone on the main sequence. This function calls InitializeManagersVector(), which executes managers_.clear().

The Race Condition

There is no lock protecting the managers_ vector between the RPC thread’s copy constructor and the main sequence’s clear().

  1. managers_.clear() executes on the main sequence, destroying the scoped_refptr elements in the vector and dropping their reference counts to zero. This frees the underlying DMPolicyManager object.
  2. Concurrently, the RPC thread is executing the std::vector copy constructor. Because std::vector::clear() does not free the underlying buffer memory, the RPC thread reads the stale pointers from the buffer.
  3. The RPC thread constructs a new scoped_refptr using the stale pointer, incrementing the reference count field within the already freed DMPolicyManager memory block.
  4. The RPC thread continues its loop execution in QueryAppPolicy, invoking std::invoke with a member function pointer on the freed object. This results in a virtual method call on a freed object.

Because scoped_refptr::ptr_ is explicitly excluded from MiraclePtr protection (RAW_PTR_EXCLUSION), this UAF is not mitigated by BackupRefPtr.

Potential Exploitation Steps

Note: These are suggested steps; our tooling agent has not executed a working proof of concept.

  1. A standard, non-administrative local user logs into a managed Windows device.
  2. The attacker instantiates the PolicyStatusSystemClass COM object.
  3. The attacker spawns two concurrent threads.
  4. Thread 1 repeatedly calls IPolicyStatus2::refreshPolicies() to trigger policy fetches and subsequent managers_.clear() calls on the main sequence.
  5. Thread 2 repeatedly calls IPolicyStatus::get_effectivePolicyForAppUpdates(...) to trigger the unsynchronized vector copy and std::invoke virtual calls on the RPC thread.
  6. By grooming the heap, the attacker controls the reallocated memory and hijacks the virtual method call to achieve Local Privilege Escalation (LPE) to SYSTEM.

Suggested Fix

Update the legacy IPolicyStatus property getters in chrome/updater/app/server/win/com_classes_legacy.cc (e.g., get_effectivePolicyForAppUpdates) to use PolicyStatusResult<T>::Get to marshal the execution to the main sequence. This pattern is already safely implemented for the newer IPolicyStatus2 methods.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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