Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in UI
DescriptionInsufficient validation of untrusted input in UI
ComponentUI
Bug ClassLogic Error
Tracker504104263
Fix commit0e083d39ead6 (chromium/src) +106/-32
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
chrome/updater/ipc/update_service_dialer_posix.cc
modified
if
chrome/updater/ipc/update_service_dialer_win.cc
modified

Files Changed

  • chrome/enterprise_companion/enterprise_companion_client.cc
  • chrome/updater/ipc/update_service_dialer_posix.cc
  • chrome/updater/ipc/update_service_dialer_win.cc
  • chromeos/ash/experiences/arc/session/arc_session_impl.cc
  • components/named_mojo_ipc_server/named_mojo_ipc_server_client_util.cc
  • components/named_mojo_ipc_server/named_mojo_server_endpoint_connector_linux.cc
  • components/named_mojo_ipc_server/named_mojo_server_endpoint_connector_mac.cc
  • mojo/core/ipcz_driver/invitation.cc
From 0e083d39ead647192b1bbeccf443b3a4630f27f1 Mon Sep 17 00:00:00 2001
From: S Ganesh <ganesh@chromium.org>
Date: Wed, 22 Apr 2026 14:36:45 -0700
Subject: [PATCH] Harden Mojo IPCz and verify server privilege in updater and EC

This change addresses a Local Privilege Escalation vulnerability in the
updater and enterprise companion by making two security improvements:

1. In Mojo IPCz driver (invitation.cc), OpenRemoteProcess now
dynamically identifies the remote process ID by querying both client and
server PIDs, rather than relying on an assumption about the remote's
role. This prevents handle confusion where the caller queries its own
PID.

2. Added a `verify_server_privilege` option to NamedPlatformChannel on
Windows. When enabled, it verifies that the server process is running
at the same or higher integrity level as the client. This prevents
named pipe squatting attacks where a lower-privileged process
intercepts connections.

Fixed: 504104263
Change-Id: Iec25825708be810effb5b29b5308939a9b7a0f65
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7785274
Commit-Queue: S Ganesh <ganesh@chromium.org>
Reviewed-by: Long Cheng <lgcheng@google.com>
Reviewed-by: Noah Rose Ledesma <noahrose@google.com>
Reviewed-by: Will Harris <wfh@chromium.org>
Auto-Submit: S Ganesh <ganesh@chromium.org>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1619094}
---

diff --git a/chrome/enterprise_companion/enterprise_companion_client.cc b/chrome/enterprise_companion/enterprise_companion_client.cc
index c572cf2..1a1ccd1 100644
--- a/chrome/enterprise_companion/enterprise_companion_client.cc
+++ b/chrome/enterprise_companion/enterprise_companion_client.cc
@@ -103,8 +103,16 @@
     return;
   }
 
+#if BUILDFLAG(IS_WIN)
+  mojo::NamedPlatformChannel::Options options;
+  options.server_name = server_name;
+  options.verify_server_privilege = true;
+  mojo::PlatformChannelEndpoint endpoint =
+      named_mojo_ipc_server::ConnectToServer(options);
+#else
   mojo::PlatformChannelEndpoint endpoint =
       named_mojo_ipc_server::ConnectToServer(server_name);
+#endif
   if (endpoint.is_valid()) {
     std::move(callback).Run(std::move(endpoint));
     return;
@@ -132,7 +140,14 @@
       FROM_HERE, {base::MayBlock()},
       base::BindOnce(
           [](const mojo::NamedPlatformChannel::ServerName& server_name) {
+#if BUILDFLAG(IS_WIN)
+            mojo::NamedPlatformChannel::Options options;
+            options.server_name = server_name;
+            options.verify_server_privilege = true;
+            return named_mojo_ipc_server::ConnectToServer(options);
+#else
             return named_mojo_ipc_server::ConnectToServer(server_name);
+#endif
           },
           server_name)
           .Then(base::BindPostTaskToCurrentDefault(
diff --git a/chrome/updater/ipc/update_service_dialer_posix.cc b/chrome/updater/ipc/update_service_dialer_posix.cc
index 9724c9f4..b5d2e56 100644
--- a/chrome/updater/ipc/update_service_dialer_posix.cc
+++ b/chrome/updater/ipc/update_service_dialer_posix.cc
@@ -45,10 +45,11 @@
       return std::nullopt;
     }
 
-    return named_mojo_ipc_server::ConnectToServer(
-        {.server_name = is_internal_service
-                            ? GetUpdateServiceInternalServerName(scope)
-                            : GetUpdateServiceServerName(scope)});
+    mojo::NamedPlatformChannel::Options options;
+    options.server_name = is_internal_service
+                              ? GetUpdateServiceInternalServerName(scope)
+                              : GetUpdateServiceServerName(scope);
+    return named_mojo_ipc_server::ConnectToServer(options);
   }();
 
   if (!endpoint) {
diff --git a/chrome/updater/ipc/update_service_dialer_win.cc b/chrome/updater/ipc/update_service_dialer_win.cc
index 87a924bb..5c5f06f 100644
--- a/chrome/updater/ipc/update_service_dialer_win.cc
+++ b/chrome/updater/ipc/update_service_dialer_win.cc
@@ -84,12 +84,13 @@
     }
 
     server = result;
-    return named_mojo_ipc_server::ConnectToServer({
-        .server_name = is_internal_service
-                           ? GetUpdateServiceInternalServerName(scope)
-                           : GetUpdateServiceServerName(scope),
-        .allow_impersonation = true,
-    });
+    mojo::NamedPlatformChannel::Options options;
+    options.server_name = is_internal_service
+                              ? GetUpdateServiceInternalServerName(scope)
+                              : GetUpdateServiceServerName(scope);
+    options.allow_impersonation = true;
+    options.verify_server_privilege = true;
+    return named_mojo_ipc_server::ConnectToServer(options);
   }();
 
   if (tries >= 1 && !endpoint) {
diff --git a/chromeos/ash/experiences/arc/session/arc_session_impl.cc b/chromeos/ash/experiences/arc/session/arc_session_impl.cc
index fdd52826..99658e4a 100644
--- a/chromeos/ash/experiences/arc/session/arc_session_impl.cc
+++ b/chromeos/ash/experiences/arc/session/arc_session_impl.cc
@@ -271,7 +271,9 @@
     }
   }
 
-  auto endpoint = mojo::NamedPlatformChannel({socket_path.value()});
+  mojo::NamedPlatformChannel::Options options;
+  options.server_name = socket_path.value();
+  auto endpoint = mojo::NamedPlatformChannel(options);
   // TODO(cmtm): use NamedPlatformChannel to bootstrap mojo connection after
   // libchrome uprev in android.
   base::ScopedFD socket_fd =
diff --git a/components/named_mojo_ipc_server/named_mojo_ipc_server_client_util.cc b/components/named_mojo_ipc_server/named_mojo_ipc_server_client_util.cc
index d3c3173..f4bcbf6 100644
--- a/components/named_mojo_ipc_server/named_mojo_ipc_server_client_util.cc
+++ b/components/named_mojo_ipc_server/named_mojo_ipc_server_client_util.cc
@@ -57,7 +57,9 @@
 // static
 mojo::PlatformChannelEndpoint ConnectToServer(
     const mojo::NamedPlatformChannel::ServerName& server_name) {
-  return ConnectToServer({.server_name = server_name});
+  mojo::NamedPlatformChannel::Options options;
+  options.server_name = server_name;
+  return ConnectToServer(options);
 }
 
 }  // namespace named_mojo_ipc_server
diff --git a/components/named_mojo_ipc_server/named_mojo_server_endpoint_connector_linux.cc b/components/named_mojo_ipc_server/named_mojo_server_endpoint_connector_linux.cc
index 1cfa150..c5cf0b7 100644
--- a/components/named_mojo_ipc_server/named_mojo_server_endpoint_connector_linux.cc
+++ b/components/named_mojo_ipc_server/named_mojo_server_endpoint_connector_linux.cc
@@ -106,8 +106,10 @@
 bool NamedMojoServerEndpointConnectorLinux::TryStart() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 
+  mojo::NamedPlatformChannel::Options options;
+  options.server_name = options_.server_name;
   mojo::PlatformChannelServerEndpoint server_endpoint =
-      mojo::NamedPlatformChannel({options_.server_name}).TakeServerEndpoint();
+      mojo::NamedPlatformChannel(options).TakeServerEndpoint();
   if (!server_endpoint.is_valid()) {
     return false;
   }
diff --git a/components/named_mojo_ipc_server/named_mojo_server_endpoint_connector_mac.cc b/components/named_mojo_ipc_server/named_mojo_server_endpoint_connector_mac.cc
index a6bb6519..1ed8928c 100644
--- a/components/named_mojo_ipc_server/named_mojo_server_endpoint_connector_mac.cc
+++ b/components/named_mojo_ipc_server/named_mojo_server_endpoint_connector_mac.cc
@@ -112,8 +112,10 @@
 bool NamedMojoServerEndpointConnectorMac::TryStart() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 
+  mojo::NamedPlatformChannel::Options options;
+  options.server_name = options_.server_name;
   mojo::PlatformChannelServerEndpoint server_endpoint =
-      mojo::NamedPlatformChannel({options_.server_name}).TakeServerEndpoint();
+      mojo::NamedPlatformChannel(options).TakeServerEndpoint();
   if (!server_endpoint.is_valid() ||
       !server_endpoint.platform_handle().is_valid_mach_receive()) {
     return false;
diff --git a/mojo/core/ipcz_driver/invitation.cc b/mojo/core/ipcz_driver/invitation.cc
index 9f808aef..69ec2887 100644
--- a/mojo/core/ipcz_driver/invitation.cc
+++ b/mojo/core/ipcz_driver/invitation.cc
@@ -109,31 +109,38 @@
 // given a handle to a connected named pipe. It may return an invalid process
 // object if either the handle does not refer to a named pipe or the handle
 // refers to a named pipe that is not connected.
-base::Process OpenRemoteProcess(const MojoInvitationTransportEndpoint& endpoint,
-                                bool remote_is_server) {
-  base::ProcessId remote_process_id = 0;
+base::Process OpenRemoteProcess(
+    const MojoInvitationTransportEndpoint& endpoint) {
   // Extract the handle to the connected named pipe from mojo invitation
   // transport endpoint.
   HANDLE handle =
       LongToHandle(static_cast<long>(endpoint.platform_handles[0].value));
-  auto get_remote_pid = remote_is_server ? &GetNamedPipeServerProcessId
-                                         : &GetNamedPipeClientProcessId;
-  // Try to get the remote client process id given the extracted handle via
-  // GetNamedPipe(Server|Client)ProcessId API.
-  if ((!get_remote_pid(handle, &remote_process_id) ||
-       remote_process_id == base::Process::Current().Pid())) {
-    DVLOG(2) << "Failed to get remote process id via the connected named pipe";
+
+  base::ProcessId client_pid = 0;
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential LPE via Named Pipe Squatting and Mojo IPCz Handle Confusion in Google Updater

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. Please see go/chrome-ai-generated-security-bugs-faq for more information.

Overview: The Google Updater on Windows connects to a predictable named pipe without verifying the server’s identity, allowing a local attacker to intercept the connection. Due to flaws in how Mojo IPCz handles isolated connections and queries process IDs, the attacker is trusted as a broker, bypassing handle decoding security checks. This allows the attacker to force the SYSTEM-privileged updater to close arbitrary handles.

Affected files:

  • chrome/enterprise_companion/enterprise_companion_client.cc
  • mojo/core/ipcz_driver/transport.cc
  • mojo/core/ipcz_driver/invitation.cc
  • mojo/public/cpp/platform/named_platform_channel_win.cc
  • chrome/updater/policy/policy_fetcher.cc

Estimated timestamp from git blame: 2025-12-18

Description

A potential local privilege escalation (LPE) vulnerability exists in the Google Updater on Windows. When the SYSTEM-privileged updater attempts to connect to the Chrome Enterprise Companion App (CECA), it does so over a named pipe with a predictable name and fails to verify the identity of the server. This allows a local, unprivileged attacker to squat the pipe and act as a malicious server.

Due to the way the updater establishes this connection (mojo::IsolatedConnection), Mojo IPCz classifies the connection as broker-to-broker. Furthermore, a logic error in OpenRemoteProcess causes the updater to query its own process ID instead of the attacker’s, leaving the remote process handle invalid. These conditions inadvertently bypass critical security checks in IPCz handle decoding, granting the attacker the ability to pass arbitrary handle values that the SYSTEM process will then close via base::win::ScopedHandle destruction. This provides a reliable Arbitrary CloseHandle primitive, which is a known vector for privilege escalation.

Technical Details

  1. Named Pipe Squatting: The updater connects to \\.\pipe\mojo.ChromeEnterpriseCompanionService via ConnectWithRetries(). The connection logic (mojo::NamedPlatformChannel::CreateClientEndpoint) uses SECURITY_ANONYMOUS to prevent impersonation but does not verify the identity (e.g., owner SID or process signature) of the server that created the pipe. Windows named pipes are first-come-first-served, so an unprivileged attacker can create this pipe before the legitimate CECA server does.

  2. Broker Trust via Isolated Connection: The updater wraps the connection in a mojo::IsolatedConnection. When this is passed down to IPCz via OutgoingInvitation::SendIsolated(), the MOJO_SEND_INVITATION_FLAG_ISOLATED flag causes Invitation::Send() to set the transport’s destination type to Transport::kBroker.

  3. Invalid Remote Process via Logic Flaw: Because IsolatedConnection does not explicitly provide the remote process, Invitation::Send() attempts to derive it by calling OpenRemoteProcess(*transport_endpoint, /* remote_is_server= */ false). Because remote_is_server is false, it calls GetNamedPipeClientProcessId. However, the updater is the client of this named pipe. It receives its own PID, detects the mismatch, and returns an invalid base::Process. Consequently, the transport is created with an invalid remote_process.

  4. Handle Verification Bypass: When the attacker sends an IPCz message, the handles are decoded in Transport::DecodeHandle(). The attacker can specify HandleOwner::kRecipient for a handle. The security check designed to prevent untrusted peers from sending kRecipient handles is:

    if (from_transport.destination_type() != Transport::kBroker &&
        !from_transport.is_peer_trusted() && !remote_process.is_current()) {
        return std::nullopt;
    }
    

    Because the destination type was set to Transport::kBroker (Step 2), this check is entirely bypassed.

  5. Arbitrary CloseHandle: Because the check is bypassed, DecodeHandle takes the attacker’s arbitrary 64-bit value, calls ::GetHandleInformation to verify the handle exists in the local (SYSTEM) process, and wraps it in a base::win::ScopedHandle (held by PlatformHandle). If the attacker intentionally crafts a malformed message so that Transport::DeserializeObject() fails, the PlatformHandle objects are destroyed. The destruction of the ScopedHandle invokes the Windows API ::CloseHandle() on the attacker-controlled value.

Potential Attack Steps

Note: Our tooling agent cannot run code, so these are suggested steps based on static analysis.

  1. As a non-admin user, create a named pipe at \\.\pipe\mojo.ChromeEnterpriseCompanionService.
  2. Wait for the scheduled task or invoke the COM method (IUpdater::FetchPolicies) to trigger the SYSTEM updater to connect to the pipe.
  3. Upon connection, send a serialized Mojo IPCz message (e.g., ConnectFromBrokerToBroker).
  4. Include handle data in the message with handle_owner = kRecipient and raw values corresponding to valid handles inside the SYSTEM process (e.g., small multiples of 4 like 0x4, 0x8, 0xC).
  5. Intentionally malform the rest of the message so that deserialization fails.
  6. The SYSTEM updater processes the message, wraps the handles, fails deserialization, and automatically closes the attacker-specified handles, disrupting the process or opening a window for LPE via handle re-use.

Suggested Fix

  1. Verify Server Identity: Modify named_mojo_ipc_server::ConnectToServer or mojo::NamedPlatformChannel::CreateClientEndpoint to verify the process ID, owner SID, or digital signature of the server process after successfully connecting to the named pipe.
  2. Fix PID Query Logic: Update OpenRemoteProcess in mojo/core/ipcz_driver/invitation.cc to ensure that when a client is attempting to verify a peer, it uses GetNamedPipeServerProcessId, and vice versa.
  3. Review Isolated Connection Trust: Re-evaluate whether IsolatedConnection should unconditionally set the destination_type to Transport::kBroker, especially when the remote process is unknown or unverified.

Evaluated with Chrome root at commit: 7353d249d9cacf9c7218e1d7b8a39cf39c72d646


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