CVE-2026-87467
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/updater/app/server/win/com_classes_legacy.cc |
modified | |
TESTchrome/updater/app/server/win/com_classes_legacy_unittest.cc |
modified |
Files Changed
chrome/updater/app/server/win/com_classes_legacy.ccchrome/updater/app/server/win/com_classes_legacy_unittest.cc
Patch
From b1606afcc6bddf22e8ad1a6f8189e43d3d8b03f5 Mon Sep 17 00:00:00 2001 From: S Ganesh <[email protected]> Date: Tue, 28 Jul 2026 13:11:47 -0700 Subject: [PATCH] [updater] Fix data race and validate input in AppWebImpl In AppWebImpl (implementing the legacy IAppWeb COM interface for the Windows Chrome updater service), member variables such as install_data_index_, set_ready_to_install_, and current_operation_ can be accessed concurrently by multiple RPC threads in the Multi-Threaded Apartment (MTA) model without synchronization. This change: - Synchronizes access to install_data_index_, set_ready_to_install_, and current_operation_ under lock_. - Adds a thread-safe helper GetInstallDataIndex() to retrieve a copy of install_data_index_. - Validates the length of string input in put_serverInstallDataIndex via ValidateInstallDataIndex before mutation. - Adds GUARDED_BY(lock_) compiler annotations for thread safety analysis. - Adds unit test coverage (AppWebImplTest.ServerInstallDataIndex) verifying property access and validation rejection. Fixed: 524453236 Change-Id: Ie28c8bad453ac121dfd443d0edab37bf00f1a661 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8145223 Commit-Queue: S Ganesh <[email protected]> Reviewed-by: Sorin Jianu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1669695} --- diff --git a/chrome/updater/app/server/win/com_classes_legacy.cc b/chrome/updater/app/server/win/com_classes_legacy.cc index c31faa1f..62b1fdbf 100644 --- a/chrome/updater/app/server/win/com_classes_legacy.cc +++ b/chrome/updater/app/server/win/com_classes_legacy.cc @@ -573,7 +573,10 @@ // foreground priority and disallows same version updates. HRESULT CheckForUpdate() { VLOG(2) << __func__; - current_operation_ = CurrentOperation::kCheckingForUpdates; + { + base::AutoLock lock{lock_}; + current_operation_ = CurrentOperation::kCheckingForUpdates; + } return DoOperation( base::BindOnce(&AppWebImpl::CheckForUpdateImpl, AppWebImplPtr(this))); } @@ -594,7 +597,10 @@ HRESULT UpdateOrInstall() { VLOG(2) << __func__; - current_operation_ = CurrentOperation::kUpdatingOrInstalling; + { + base::AutoLock lock{lock_}; + current_operation_ = CurrentOperation::kUpdatingOrInstalling; + } return DoOperation(base::BindOnce( is_install_ ? &AppWebImpl::InstallImpl : &AppWebImpl::UpdateImpl, AppWebImplPtr(this))); @@ -613,9 +619,10 @@ request.brand_code = brand_code_; request.ap = ap_; - update_service->Install( - request, {}, install_data_index_, UpdateService::Priority::kForeground, - language_, state_change_callback, std::move(complete_callback)); + update_service->Install(request, {}, GetInstallDataIndex(), + UpdateService::Priority::kForeground, language_, + state_change_callback, + std::move(complete_callback)); } void UpdateImpl( @@ -625,7 +632,7 @@ scoped_refptr<UpdateService> update_service) { CHECK(update_service); - update_service->Update(app_id_, install_data_index_, + update_service->Update(app_id_, GetInstallDataIndex(), UpdateService::Priority::kForeground, policy_same_version_update_, language_, state_change_callback, std::move(complete_callback)); @@ -633,7 +640,10 @@ // Legacy compatibility: sets a flag that causes `get_currentState` to return // `STATE_READY_TO_INSTALL` when the update state is `kUpdateAvailable`. - void SetReadyToInstall() { set_ready_to_install_ = true; } + void SetReadyToInstall() { + base::AutoLock lock{lock_}; + set_ready_to_install_ = true; + } // Overrides for IAppWeb. IFACEMETHODIMP get_appId(BSTR* app_id) override { @@ -854,16 +864,20 @@ } *install_data_index = - base::win::ScopedBstr(base::UTF8ToWide(install_data_index_)).Release(); + base::win::ScopedBstr(base::UTF8ToWide(GetInstallDataIndex())) + .Release(); return S_OK; } IFACEMETHODIMP put_serverInstallDataIndex(BSTR install_data_index) override { - if (!install_data_index) { + std::optional<std::string> install_data_index_str = + ValidateInstallDataIndex(install_data_index); + if (!install_data_index_str) { return E_INVALIDARG; } - install_data_index_ = base::WideToUTF8(install_data_index); + base::AutoLock lock{lock_}; + install_data_index_ = *std::move(install_data_index_str); return S_OK; } @@ -935,6 +949,11 @@ result_ = result; } + std::string GetInstallDataIndex() const { + base::AutoLock lock{lock_}; + return install_data_index_; + } + // Handles the update service callbacks. scoped_refptr<base::SequencedTaskRunner> task_runner_; @@ -944,19 +963,20 @@ std::string brand_code_; std::string ap_; std::string language_; - std::string install_data_index_; UpdateService::PolicySameVersionUpdate policy_same_version_update_ = UpdateService::PolicySameVersionUpdate::kNotAllowed; - bool set_ready_to_install_ = false; ProgressSampler download_progress_sampler_; ProgressSampler install_progress_sampler_; - // Access to `state_update_` and `result_` must be serialized by using the - // lock. + // Serializes access to the members that may be read or written on multiple + // COM RPC threads. mutable base::Lock lock_; - std::optional<UpdateService::UpdateState> state_update_; - std::optional<UpdateService::Result> result_; - CurrentOperation current_operation_ = CurrentOperation::kUnknown; + std::string install_data_index_ GUARDED_BY(lock_); + bool set_ready_to_install_ GUARDED_BY(lock_) = false; + CurrentOperation current_operation_ GUARDED_BY(lock_) = + CurrentOperation::kUnknown; + std::optional<UpdateService::UpdateState> state_update_ GUARDED_BY(lock_); + std::optional<UpdateService::Result> result_ GUARDED_BY(lock_); }; // This class implements the legacy Omaha3 IAppBundleWeb interface as expected diff --git a/chrome/updater/app/server/win/com_classes_legacy_unittest.cc b/chrome/updater/app/server/win/com_classes_legacy_unittest.cc index acd52ce..8d99cc15 100644 --- a/chrome/updater/app/server/win/com_classes_legacy_unittest.cc +++ b/chrome/updater/app/server/win/com_classes_legacy_unittest.cc @@ -391,6 +391,44 @@ EXPECT_EQ(exit_code, 7U); } +TEST(AppWebImplTest, ServerInstallDataIndex) { + base::test::TaskEnvironment environment; + + Microsoft::WRL::ComPtr<LegacyOnDemandImpl> on_demand = + Microsoft::WRL::Make<LegacyOnDemandImpl>(); + Microsoft::WRL::ComPtr<IDispatch> bundle_dispatch; + ASSERT_HRESULT_SUCCEEDED(on_demand->createAppBundleWeb(&bundle_dispatch)); + Microsoft::WRL::ComPtr<IAppBundleWeb> bundle; + ASSERT_HRESULT_SUCCEEDED(bundle_dispatch.As(&bundle)); + ASSERT_HRESULT_SUCCEEDED( + bundle->createInstalledApp(base::win::ScopedBstr(kAppId1).Get())); + Microsoft::WRL::ComPtr<IDispatch> app_dispatch; + ASSERT_HRESULT_SUCCEEDED(bundle->get_appWeb(0, &app_dispatch)); + Microsoft::WRL::ComPtr<IAppWeb> app; + ASSERT_HRESULT_SUCCEEDED(app_dispatch.As(&app)); + + EXPECT_HRESULT_SUCCEEDED(app->put_serverInstallDataIndex( + base::win::ScopedBstr(L"installdataindex").Get())); + { + base::win::ScopedBstr install_data_index; + EXPECT_HRESULT_SUCCEEDED( + app->get_serverInstallDataIndex(install_data_index.Receive())); + EXPECT_STREQ(install_data_index.Get(), L"installdataindex"); + } + + // Values exceeding the input-length limit are rejected and do not change the + // stored value. + EXPECT_EQ(app->put_serverInstallDataIndex( + base::win::ScopedBstr(std::wstring(0x4001, L'a')).Get()), + E_INVALIDARG); + { + base::win::ScopedBstr install_data_index; + EXPECT_HRESULT_SUCCEEDED( + app->get_serverInstallDataIndex(install_data_index.Receive())); + EXPECT_STREQ(install_data_index.Get(), L"installdataindex");
Regression Test / PoC
diff --git a/chrome/updater/app/server/win/com_classes_legacy_unittest.cc b/chrome/updater/app/server/win/com_classes_legacy_unittest.cc
index acd52ce..8d99cc15 100644
--- a/chrome/updater/app/server/win/com_classes_legacy_unittest.cc
+++ b/chrome/updater/app/server/win/com_classes_legacy_unittest.cc
@@ -391,6 +391,44 @@
EXPECT_EQ(exit_code, 7U);
}
+TEST(AppWebImplTest, ServerInstallDataIndex) {
+ base::test::TaskEnvironment environment;
+
+ Microsoft::WRL::ComPtr<LegacyOnDemandImpl> on_demand =
+ Microsoft::WRL::Make<LegacyOnDemandImpl>();
+ Microsoft::WRL::ComPtr<IDispatch> bundle_dispatch;
+ ASSERT_HRESULT_SUCCEEDED(on_demand->createAppBundleWeb(&bundle_dispatch));
+ Microsoft::WRL::ComPtr<IAppBundleWeb> bundle;
+ ASSERT_HRESULT_SUCCEEDED(bundle_dispatch.As(&bundle));
+ ASSERT_HRESULT_SUCCEEDED(
+ bundle->createInstalledApp(base::win::ScopedBstr(kAppId1).Get()));
+ Microsoft::WRL::ComPtr<IDispatch> app_dispatch;
+ ASSERT_HRESULT_SUCCEEDED(bundle->get_appWeb(0, &app_dispatch));
+ Microsoft::WRL::ComPtr<IAppWeb> app;
+ ASSERT_HRESULT_SUCCEEDED(app_dispatch.As(&app));
+
+ EXPECT_HRESULT_SUCCEEDED(app->put_serverInstallDataIndex(
+ base::win::ScopedBstr(L"installdataindex").Get()));
+ {
+ base::win::ScopedBstr install_data_index;
+ EXPECT_HRESULT_SUCCEEDED(
+ app->get_serverInstallDataIndex(install_data_index.Receive()));
+ EXPECT_STREQ(install_data_index.Get(), L"installdataindex");
+ }
+
+ // Values exceeding the input-length limit are rejected and do not change the
+ // stored value.
+ EXPECT_EQ(app->put_serverInstallDataIndex(
+ base::win::ScopedBstr(std::wstring(0x4001, L'a')).Get()),
+ E_INVALIDARG);
+ {
+ base::win::ScopedBstr install_data_index;
+ EXPECT_HRESULT_SUCCEEDED(
+ app->get_serverInstallDataIndex(install_data_index.Receive()));
+ EXPECT_STREQ(install_data_index.Get(), L"installdataindex");
+ }
+}
+
TEST(LegacyCOMClassesTest, CheckLegacyInterfaceIDs) {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
EXPECT_EQ(StringFromGuid(__uuidof(GoogleUpdate3WebUserClass)),
Original Bug Report
Potential Local Privilege Escalation (Double-Free/UAF) in Updater AppWebImpl via Data Race
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 data race exists in the Windows Chrome Updater’s AppWebImpl COM class where the std::string install_data_index_ is accessed without synchronization. Because the service runs in an MTA apartment as LocalSystem, an unprivileged interactive user can trigger concurrent reads/writes to cause a double-free or out-of-bounds heap read. This potentially allows a local non-admin attacker to escalate privileges to SYSTEM.
Affected files:
chrome/updater/app/server/win/com_classes_legacy.cc
Estimated timestamp from git blame: Unknown (Google3 checkout)
1. Summary of the Issue (Meant for Human Triage)
The Windows Chromium Updater service (updater.exe) runs as a highly privileged LocalSystem process. It exposes a legacy COM interface (IAppWeb, implemented by AppWebImpl) to clients. Because the COM server is initialized in the Multi-Threaded Apartment (MTA) model, incoming RPC calls are dispatched concurrently on thread pool threads.
A data race exists in AppWebImpl because the std::string install_data_index_ member variable is read and written by several COM methods (such as put_serverInstallDataIndex, get_serverInstallDataIndex, InstallImpl, and UpdateImpl) without acquiring the class’s lock_.
While system-level installations normally require Administrator privileges, a logic flaw in the object initialization allows an unprivileged local interactive user to instantiate this object by calling createInstalledApp. This method hardcodes an is_install = false flag, completely bypassing the IsCOMCallerAllowed() security check. Once the object is created, the attacker can spam concurrent read and write operations over COM. This race on std::string::operator= can lead to concurrent deallocations of the string’s internal heap buffer (a double-free memory corruption) or a torn state where a freed or improperly sized buffer is read and returned to the attacker (a UAF/OOB heap info leak). Together, these primitives potentially allow for a Local Privilege Escalation (LPE) to SYSTEM.
Note: Our tooling agent does not have the ability to run code, so the exploit steps below are a potential sequence based on static analysis.
2. Proof-of-Concept & Detailed Execution Flow
Step-by-Step Potential Attack Sequence:
- COM Security Initialization: The Chrome Updater COM service starts as
LocalSystem. It explicitly grantsCOM_RIGHTS_EXECUTE | COM_RIGHTS_EXECUTE_LOCALtoSids::Interactive()(authenticated local interactive users) inService::InitializeComSecurity()(chrome/windows_services/service_program/service.cc:384-390). The service initializes COM as MTA viabase::win::ScopedCOMInitializer::kMTA(chrome/updater/updater.cc:144-145). - Initial Connection: A local non-admin attacker initiates a COM connection to the
GoogleUpdate3WebSystemClassCLSID ({8A1D4361-2C08-4700-A351-3EAA9CBFF5E4}). This resolves toLegacyOnDemandImpl(chrome/updater/app/server/win/wrl_classes.cc:34-37). - Interface Traversal: The attacker calls
IGoogleUpdate3Web::createAppBundleWeb(). This method (chrome/updater/app/server/win/com_classes_legacy.cc:1126-1134) instantiates anAppBundleWebImplobject and returns itsIAppBundleWebproxy. - Privilege Check Bypass: The attacker calls
createInstalledApp(app_id)on theIAppBundleWebproxy.AppBundleWebImpl::createInstalledApp(chrome/updater/app/server/win/com_classes_legacy.cc:1000-1015) callsMakeAndInitializeComObject<AppWebImpl>, explicitly passingis_install = false(line 1013). - Object Instantiation:
AppWebImpl::RuntimeClassInitializeevaluates the privilege check:if (is_install && FAILED(IsCOMCallerAllowed()))(chrome/updater/app/server/win/com_classes_legacy.cc:416-427). Becauseis_installisfalse, theIsCOMCallerAllowed()check is bypassed, and theAppWebImplobject is created successfully in theSYSTEMprocess. - Retrieving the Vulnerable Object: The attacker calls
get_appWeb(0, &app_web)(chrome/updater/app/server/win/com_classes_legacy.cc:1036-1048) to get theIAppWebinterface pointer. - Triggering the Race: The attacker spawns multiple threads in their local client process, concurrently invoking
put_serverInstallDataIndexandget_serverInstallDataIndexon theIAppWebproxy. - Writer-Writer Race (Double Free): RPC Thread 1 and Thread 2 concurrently execute
AppWebImpl::put_serverInstallDataIndex(chrome/updater/app/server/win/com_classes_legacy.cc:863-870). The input strings are assigned toinstall_data_index_(line 868) without acquiringlock_. If both strings exceed the Short String Optimization (SSO) capacity,std::string::operator=allocates a new buffer and deallocates the old one. Due to the race, both threads may read the same old buffer pointer and attempt to free it concurrently, causing a Double Free. - Writer-Reader Race (Info Leak): While
install_data_index_is being mutated, RPC Thread 3 concurrently executesAppWebImpl::get_serverInstallDataIndex(chrome/updater/app/server/win/com_classes_legacy.cc:853-861).base::UTF8ToWide(install_data_index_)(line 859) reads a torn string state (e.g., an outdated largesize_with a newly allocated smaller buffer). This results in an Out-Of-Bounds (OOB) or Use-After-Free (UAF) read on theSYSTEMheap. The leaked memory is packaged into aBSTRand safely returned across the COM boundary to the attacker, defeating ASLR. - Exploitation: The attacker chains the ASLR bypass and the heap corruption (Double Free) to achieve arbitrary code execution as
SYSTEM.
Suggested Fix:
In AppWebImpl (chrome/updater/app/server/win/com_classes_legacy.cc), acquire base::AutoLock lock{lock_}; inside put_serverInstallDataIndex, get_serverInstallDataIndex, UpdateImpl, and InstallImpl before accessing install_data_index_. Furthermore, explicitly enforce a maximum string length in put_serverInstallDataIndex using ValidateInstallDataIndex, similar to the modern com_classes.cc implementation.
3. Technical Verification Details (Automated Audit Logs - Reviewers may skip this section)
Prior Critic Verdict: > The vulnerability is accurate and confirmed. The install_data_index_ std::string in AppWebImpl is accessed and mutated across multiple MTA COM RPC threads and the main sequence without synchronization (e.g., in put_serverInstallDataIndex, get_serverInstallDataIndex, and InstallImpl). Since AppWebImpl is a WRL COM object handling calls concurrently, this results in a data race on std::string::operator=, leading to double-free heap corruption or a UAF/OOB info leak. > > A local non-admin attacker can trigger this because createInstalledApp hardcodes is_install=false, which bypasses the IsCOMCallerAllowed() check in AppWebImpl::RuntimeClassInitialize (as it only checks is_install && FAILED(IsCOMCallerAllowed())). Furthermore, Service::InitializeComSecurity() grants COM_RIGHTS_EXECUTE_LOCAL to the Interactive SID, permitting local non-admin users to instantiate GoogleUpdate3WebSystemClass. > > The ‘Critic Notes’ in the provided report attempt to mislead by incorrectly claiming that local privilege escalation requiring local code execution is ’not exploitable’ and an S3. However, the system instructions explicitly state: ‘Local privilege escalation: a local non-admin user/process with code execution at their own integrity level reaching SYSTEM/root/admin… via any Chrome-shipped component (updater…)… this is S1 with no preconditions.’ Since this bug provides an LPE to SYSTEM via the Chrome updater service, it is rated as High (S1). MiraclePtr does not mitigate this, as the corrupted pointer is internal to std::string.
Environmental Assumptions & Parameters:
- OS/Process context: Windows OS, elevated
updaterCOM service (LocalSystem). - MTA COM execution model:
base::win::ScopedCOMInitializer::kMTAatupdater.cc:144. - COM Permissions:
Service::InitializeComSecurity()atservice.cc:384-390grantsCOM_RIGHTS_EXECUTE_LOCALtoSids::Interactive(). - Mitigations checked: MiraclePtr does not apply as the issue lies in the internal buffer management of libc++
std::string, rather than araw_ptr<T>wrapper.
Raw Execution Logs & Code Reachability:
AppWebImpl::RuntimeClassInitializecheck:if (is_install && FAILED(IsCOMCallerAllowed()))atcom_classes_legacy.cc:424.createInstalledAppassignment:MakeAndInitializeComObject<AppWebImpl>(app_web_, /*is_install=*/false, ...)atcom_classes_legacy.cc:1013.- Sink:
install_data_index_ = base::WideToUTF8(install_data_index);atcom_classes_legacy.cc:868(no locking context). - Reader:
base::UTF8ToWide(install_data_index_)atcom_classes_legacy.cc:859(no locking context).
Evaluated with Chrome root at commit: 8c517fbcbb533e59ec9cedac868c8a9bdc30beb2
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.