CVE-2026-11115
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
PolicyStatusResultchrome/updater/app/server/win/com_classes_legacy.cc |
modified | |
ifchrome/updater/app/server/win/com_classes_legacy.cc |
modified | |
AppVersionWebImplchrome/updater/app/server/win/com_classes_legacy.cc |
modified |
Files Changed
chrome/updater/app/server/win/com_classes_legacy.cc
Patch
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)));
Original Bug Report
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.ccchrome/updater/policy/service.hchrome/updater/app/server/win/com_classes_legacy.ccchrome/updater/configurator.ccchrome/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().
managers_.clear()executes on the main sequence, destroying thescoped_refptrelements in the vector and dropping their reference counts to zero. This frees the underlyingDMPolicyManagerobject.- Concurrently, the RPC thread is executing the
std::vectorcopy constructor. Becausestd::vector::clear()does not free the underlying buffer memory, the RPC thread reads the stale pointers from the buffer. - The RPC thread constructs a new
scoped_refptrusing the stale pointer, incrementing the reference count field within the already freedDMPolicyManagermemory block. - The RPC thread continues its loop execution in
QueryAppPolicy, invokingstd::invokewith 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.
- A standard, non-administrative local user logs into a managed Windows device.
- The attacker instantiates the
PolicyStatusSystemClassCOM object. - The attacker spawns two concurrent threads.
- Thread 1 repeatedly calls
IPolicyStatus2::refreshPolicies()to trigger policy fetches and subsequentmanagers_.clear()calls on the main sequence. - Thread 2 repeatedly calls
IPolicyStatus::get_effectivePolicyForAppUpdates(...)to trigger the unsynchronized vector copy andstd::invokevirtual calls on the RPC thread. - 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.