Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace condition in Updater
DescriptionRace condition in Updater
ComponentUpdater
Bug ClassRace
Tracker524453236
Fix commitb1606afcc6bd (chromium/src) +75/-17
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

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

Files Changed

  • chrome/updater/app/server/win/com_classes_legacy.cc
  • chrome/updater/app/server/win/com_classes_legacy_unittest.cc
From b1606afcc6bddf22e8ad1a6f8189e43d3d8b03f5 Mon Sep 17 00:00:00 2001
From: S Ganesh <ganesh@chromium.org>
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 <ganesh@chromium.org>
Reviewed-by: Sorin Jianu <sorin@chromium.org>
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");
Loading diff…

Regression Test / PoC

shipped with the fix
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)),
Loading diff…

Original Bug Report

reported by aw...@chromium.org

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:

  1. COM Security Initialization: The Chrome Updater COM service starts as LocalSystem. It explicitly grants COM_RIGHTS_EXECUTE | COM_RIGHTS_EXECUTE_LOCAL to Sids::Interactive() (authenticated local interactive users) in Service::InitializeComSecurity() (chrome/windows_services/service_program/service.cc:384-390). The service initializes COM as MTA via base::win::ScopedCOMInitializer::kMTA (chrome/updater/updater.cc:144-145).
  2. Initial Connection: A local non-admin attacker initiates a COM connection to the GoogleUpdate3WebSystemClass CLSID ({8A1D4361-2C08-4700-A351-3EAA9CBFF5E4}). This resolves to LegacyOnDemandImpl (chrome/updater/app/server/win/wrl_classes.cc:34-37).
  3. Interface Traversal: The attacker calls IGoogleUpdate3Web::createAppBundleWeb(). This method (chrome/updater/app/server/win/com_classes_legacy.cc:1126-1134) instantiates an AppBundleWebImpl object and returns its IAppBundleWeb proxy.
  4. Privilege Check Bypass: The attacker calls createInstalledApp(app_id) on the IAppBundleWeb proxy. AppBundleWebImpl::createInstalledApp (chrome/updater/app/server/win/com_classes_legacy.cc:1000-1015) calls MakeAndInitializeComObject<AppWebImpl>, explicitly passing is_install = false (line 1013).
  5. Object Instantiation: AppWebImpl::RuntimeClassInitialize evaluates the privilege check: if (is_install && FAILED(IsCOMCallerAllowed())) (chrome/updater/app/server/win/com_classes_legacy.cc:416-427). Because is_install is false, the IsCOMCallerAllowed() check is bypassed, and the AppWebImpl object is created successfully in the SYSTEM process.
  6. 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 the IAppWeb interface pointer.
  7. Triggering the Race: The attacker spawns multiple threads in their local client process, concurrently invoking put_serverInstallDataIndex and get_serverInstallDataIndex on the IAppWeb proxy.
  8. 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 to install_data_index_ (line 868) without acquiring lock_. 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.
  9. Writer-Reader Race (Info Leak): While install_data_index_ is being mutated, RPC Thread 3 concurrently executes AppWebImpl::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 large size_ with a newly allocated smaller buffer). This results in an Out-Of-Bounds (OOB) or Use-After-Free (UAF) read on the SYSTEM heap. The leaked memory is packaged into a BSTR and safely returned across the COM boundary to the attacker, defeating ASLR.
  10. 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 updater COM service (LocalSystem).
  • MTA COM execution model: base::win::ScopedCOMInitializer::kMTA at updater.cc:144.
  • COM Permissions: Service::InitializeComSecurity() at service.cc:384-390 grants COM_RIGHTS_EXECUTE_LOCAL to Sids::Interactive().
  • Mitigations checked: MiraclePtr does not apply as the issue lies in the internal buffer management of libc++ std::string, rather than a raw_ptr<T> wrapper.

Raw Execution Logs & Code Reachability:

  • AppWebImpl::RuntimeClassInitialize check: if (is_install && FAILED(IsCOMCallerAllowed())) at com_classes_legacy.cc:424.
  • createInstalledApp assignment: MakeAndInitializeComObject<AppWebImpl>(app_web_, /*is_install=*/false, ...) at com_classes_legacy.cc:1013.
  • Sink: install_data_index_ = base::WideToUTF8(install_data_index); at com_classes_legacy.cc:868 (no locking context).
  • Reader: base::UTF8ToWide(install_data_index_) at com_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.

View on issue tracker