CVE-2026-10942
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/updater/ipc/update_service_dialer_posix.cc |
modified | |
ifchrome/updater/ipc/update_service_dialer_win.cc |
modified |
Files Changed
chrome/enterprise_companion/enterprise_companion_client.ccchrome/updater/ipc/update_service_dialer_posix.ccchrome/updater/ipc/update_service_dialer_win.ccchromeos/ash/experiences/arc/session/arc_session_impl.cccomponents/named_mojo_ipc_server/named_mojo_ipc_server_client_util.cccomponents/named_mojo_ipc_server/named_mojo_server_endpoint_connector_linux.cccomponents/named_mojo_ipc_server/named_mojo_server_endpoint_connector_mac.ccmojo/core/ipcz_driver/invitation.cc
Patch
From 0e083d39ead647192b1bbeccf443b3a4630f27f1 Mon Sep 17 00:00:00 2001 From: S Ganesh <[email protected]> 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 <[email protected]> Reviewed-by: Long Cheng <[email protected]> Reviewed-by: Noah Rose Ledesma <[email protected]> Reviewed-by: Will Harris <[email protected]> Auto-Submit: S Ganesh <[email protected]> Reviewed-by: Daniel Cheng <[email protected]> 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;
Original Bug Report
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.ccmojo/core/ipcz_driver/transport.ccmojo/core/ipcz_driver/invitation.ccmojo/public/cpp/platform/named_platform_channel_win.ccchrome/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
-
Named Pipe Squatting: The updater connects to
\\.\pipe\mojo.ChromeEnterpriseCompanionServiceviaConnectWithRetries(). The connection logic (mojo::NamedPlatformChannel::CreateClientEndpoint) usesSECURITY_ANONYMOUSto 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. -
Broker Trust via Isolated Connection: The updater wraps the connection in a
mojo::IsolatedConnection. When this is passed down to IPCz viaOutgoingInvitation::SendIsolated(), theMOJO_SEND_INVITATION_FLAG_ISOLATEDflag causesInvitation::Send()to set the transport’sdestinationtype toTransport::kBroker. -
Invalid Remote Process via Logic Flaw: Because
IsolatedConnectiondoes not explicitly provide the remote process,Invitation::Send()attempts to derive it by callingOpenRemoteProcess(*transport_endpoint, /* remote_is_server= */ false). Becauseremote_is_serveris false, it callsGetNamedPipeClientProcessId. However, the updater is the client of this named pipe. It receives its own PID, detects the mismatch, and returns an invalidbase::Process. Consequently, the transport is created with an invalidremote_process. -
Handle Verification Bypass: When the attacker sends an IPCz message, the handles are decoded in
Transport::DecodeHandle(). The attacker can specifyHandleOwner::kRecipientfor a handle. The security check designed to prevent untrusted peers from sendingkRecipienthandles 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. -
Arbitrary CloseHandle: Because the check is bypassed,
DecodeHandletakes the attacker’s arbitrary 64-bit value, calls::GetHandleInformationto verify the handle exists in the local (SYSTEM) process, and wraps it in abase::win::ScopedHandle(held byPlatformHandle). If the attacker intentionally crafts a malformed message so thatTransport::DeserializeObject()fails, thePlatformHandleobjects are destroyed. The destruction of theScopedHandleinvokes 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.
- As a non-admin user, create a named pipe at
\\.\pipe\mojo.ChromeEnterpriseCompanionService. - Wait for the scheduled task or invoke the COM method (
IUpdater::FetchPolicies) to trigger the SYSTEM updater to connect to the pipe. - Upon connection, send a serialized Mojo IPCz message (e.g.,
ConnectFromBrokerToBroker). - Include handle data in the message with
handle_owner = kRecipientand raw values corresponding to valid handles inside the SYSTEM process (e.g., small multiples of 4 like0x4,0x8,0xC). - Intentionally malform the rest of the message so that deserialization fails.
- 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
- Verify Server Identity: Modify
named_mojo_ipc_server::ConnectToServerormojo::NamedPlatformChannel::CreateClientEndpointto verify the process ID, owner SID, or digital signature of the server process after successfully connecting to the named pipe. - Fix PID Query Logic: Update
OpenRemoteProcessinmojo/core/ipcz_driver/invitation.ccto ensure that when a client is attempting to verify a peer, it usesGetNamedPipeServerProcessId, and vice versa. - Review Isolated Connection Trust: Re-evaluate whether
IsolatedConnectionshould unconditionally set thedestination_typetoTransport::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.