CVE-2026-12018
Overview
Files Changed
components/named_mojo_ipc_server/named_mojo_message_pipe_server.ccmojo/public/cpp/system/invitation.ccmojo/public/cpp/system/invitation.hmojo/public/cpp/system/isolated_connection.ccmojo/public/cpp/system/isolated_connection.h
Patch
From 64fa90e7e17e6511a5b75ef44573555f26e1d24c Mon Sep 17 00:00:00 2001 From: Fred Shih <[email protected]> Date: Wed, 03 Jun 2026 12:40:10 -0700 Subject: [PATCH] Propagate invitation flag for isolated connections This is mainly in response to the bug... it does look like not propagating the flag in an isolated connection *seems* wrong and there doesn't seem to be any comment indicating why this omission is necessary. I didn't bother fixing *all* the plumbings, as it looks like this is a deprecated path, anyway. Bug: 516808201 Change-Id: Id64b7604d11c7fba1eb087944971265b71b90141 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7891887 Reviewed-by: Andrea Orru <[email protected]> Reviewed-by: Noah Rose Ledesma <[email protected]> Commit-Queue: Fred Shih <[email protected]> Cr-Commit-Position: refs/heads/main@{#1641119} --- diff --git a/components/named_mojo_ipc_server/named_mojo_message_pipe_server.cc b/components/named_mojo_ipc_server/named_mojo_message_pipe_server.cc index ea4133b8..36947b3 100644 --- a/components/named_mojo_ipc_server/named_mojo_message_pipe_server.cc +++ b/components/named_mojo_ipc_server/named_mojo_message_pipe_server.cc @@ -156,7 +156,8 @@ // Create isolated connection. auto connection = std::make_unique<mojo::IsolatedConnection>(); mojo::ScopedMessagePipeHandle message_pipe = - connection->Connect(std::move(endpoint), std::move(peer_process)); + connection->Connect(std::move(endpoint), std::move(peer_process), + options_.extra_send_invitation_flags); on_message_pipe_ready_.Run(std::move(message_pipe), std::move(info), result.context, std::move(connection)); return; diff --git a/mojo/public/cpp/system/invitation.cc b/mojo/public/cpp/system/invitation.cc index 217ec2d..5b97664 100644 --- a/mojo/public/cpp/system/invitation.cc +++ b/mojo/public/cpp/system/invitation.cc @@ -249,8 +249,10 @@ ScopedMessagePipeHandle OutgoingInvitation::SendIsolated( PlatformChannelEndpoint channel_endpoint, std::string_view connection_name, - base::ProcessHandle target_process) { + base::ProcessHandle target_process, + MojoSendInvitationFlags invitation_flags) { OutgoingInvitation invitation; + invitation.set_extra_flags(invitation_flags); ScopedMessagePipeHandle pipe = invitation.AttachMessagePipe(kIsolatedPipeName); SendInvitation(std::move(invitation.handle_), target_process, @@ -265,8 +267,10 @@ ScopedMessagePipeHandle OutgoingInvitation::SendIsolated( PlatformChannelServerEndpoint server_endpoint, std::string_view connection_name, - base::ProcessHandle target_process) { + base::ProcessHandle target_process, + MojoSendInvitationFlags invitation_flags) { OutgoingInvitation invitation; + invitation.set_extra_flags(invitation_flags); ScopedMessagePipeHandle pipe = invitation.AttachMessagePipe(kIsolatedPipeName); #if !BUILDFLAG(IS_FUCHSIA) && !BUILDFLAG(IS_IOS) diff --git a/mojo/public/cpp/system/invitation.h b/mojo/public/cpp/system/invitation.h index 2ba06320..6598a56 100644 --- a/mojo/public/cpp/system/invitation.h +++ b/mojo/public/cpp/system/invitation.h @@ -139,7 +139,9 @@ static ScopedMessagePipeHandle SendIsolated( PlatformChannelEndpoint channel_endpoint, std::string_view connection_name = {}, - base::ProcessHandle target_process = base::kNullProcessHandle); + base::ProcessHandle target_process = base::kNullProcessHandle, + MojoSendInvitationFlags invitation_flags = + MOJO_SEND_INVITATION_FLAG_NONE); // Similar to above but sends |invitation| via |server_endpoint|, which should // correspond to a |PlatformChannelServerEndpoint| taken from a @@ -150,7 +152,9 @@ static ScopedMessagePipeHandle SendIsolated( PlatformChannelServerEndpoint server_endpoint, std::string_view connection_name = {}, - base::ProcessHandle target_process = base::kNullProcessHandle); + base::ProcessHandle target_process = base::kNullProcessHandle, + MojoSendInvitationFlags invitation_flags = + MOJO_SEND_INVITATION_FLAG_NONE); private: MojoSendInvitationFlags extra_flags_ = MOJO_SEND_INVITATION_FLAG_NONE; diff --git a/mojo/public/cpp/system/isolated_connection.cc b/mojo/public/cpp/system/isolated_connection.cc index e03c783..6dcb4c96 100644 --- a/mojo/public/cpp/system/isolated_connection.cc +++ b/mojo/public/cpp/system/isolated_connection.cc @@ -40,8 +40,17 @@ ScopedMessagePipeHandle IsolatedConnection::Connect( PlatformChannelEndpoint endpoint, base::Process process) { + return Connect(std::move(endpoint), std::move(process), + MOJO_SEND_INVITATION_FLAG_NONE); +} + +ScopedMessagePipeHandle IsolatedConnection::Connect( + PlatformChannelEndpoint endpoint, + base::Process process, + MojoSendInvitationFlags invitation_flags) { return OutgoingInvitation::SendIsolated(std::move(endpoint), - token_.ToString(), process.Handle()); + token_.ToString(), process.Handle(), + invitation_flags); } ScopedMessagePipeHandle IsolatedConnection::Connect( diff --git a/mojo/public/cpp/system/isolated_connection.h b/mojo/public/cpp/system/isolated_connection.h index d670c2c88..199d432 100644 --- a/mojo/public/cpp/system/isolated_connection.h +++ b/mojo/public/cpp/system/isolated_connection.h @@ -7,6 +7,7 @@ #include "base/process/process.h" #include "base/unguessable_token.h" +#include "mojo/public/c/system/invitation.h" #include "mojo/public/cpp/platform/platform_channel_endpoint.h" #include "mojo/public/cpp/platform/platform_channel_server_endpoint.h" #include "mojo/public/cpp/system/message_pipe.h" @@ -56,6 +57,15 @@ ScopedMessagePipeHandle Connect(PlatformChannelEndpoint endpoint, base::Process process); + // Connects to a process at the other end of the channel. Returns a primordial + // message pipe that can be used for Mojo IPC. The connection + // will be connected to a corresponding peer pipe in the remote process. + // `process` identifies the remote process. Invitation flag sets additional + // invitation flags. + ScopedMessagePipeHandle Connect(PlatformChannelEndpoint endpoint, + base::Process process, + MojoSendInvitationFlags invitation_flags); + // Same as above but works with a server endpoint. The corresponding client // could use the above signature with NamedPlatformChannel::ConnectToServer. ScopedMessagePipeHandle Connect(PlatformChannelServerEndpoint endpoint);
Original Bug Report
Potential Mojo handle harvesting and DoS in Windows Chrome Updater via IsolatedConnection
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: When the system-scope Chrome Updater on Windows establishes an isolated connection with local clients, the ‘MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS’ flag is dropped during initialization. This causes the client transport to be treated as trusted, completely bypassing Mojo handle type protections during serialization and enabling local unprivileged clients to harvest or close arbitrary handles from the elevated updater process via the ‘RelayMessage’ protocol.
Affected files:
mojo/public/cpp/platform/platform_handle_security_util_win.ccmojo/core/ipcz_driver/transport.ccchrome/updater/ipc/ipc_security_win.cc
Estimated timestamp from git blame: 2025-05-08
Description
A potential Local Privilege Escalation (LPE) and Denial of Service (DoS) vulnerability exists in the Windows system-scope Chrome Updater process due to flag truncation when establishing Mojo ‘IsolatedConnection’ pipes with local clients.
While the updater configures its endpoint options to treat connections as untrusted by specifying ‘MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS’ in ‘chrome/updater/ipc/ipc_security_win.cc’, this flag is dropped when the server sets up an ‘IsolatedConnection’. Consequently, the transport is initialized as trusted (‘ProcessTrust::kTrusted’). An unprivileged local client can abuse the Mojo/Ipcz ‘RelayMessage’ protocol to trick the elevated updater broker into adopting arbitrary handles from its own handle table and duplicating them back to the client with identical permissions, while simultaneously closing them in the updater.
Root Cause Analysis
-
Truncation of Send Invitation Flags: In ‘chrome/updater/ipc/ipc_security_win.cc’ (line 25), the updater configures server options with the untrusted process flag. However, in ‘NamedMojoMessagePipeServer::OnConnectionAccepted’ (‘components/named_mojo_ipc_server/named_mojo_message_pipe_server.cc’), when ‘is_isolated’ is true, the server creates an isolated connection via ‘mojo::IsolatedConnection::Connect’. This function ignores the custom invitation flags and delegates to ‘OutgoingInvitation::SendIsolated’, which initializes an ‘OutgoingInvitation’ locally without carrying over the caller’s invitation flags. As a result, the ‘MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS’ flag is discarded.
-
Trust Level Bypass: Because the untrusted process flag is omitted, the Mojo/Ipcz driver layer in ‘mojo/core/ipcz_driver/invitation.cc’ initializes the connection’s trust level to ‘ProcessTrust::kTrusted’ instead of ‘kUntrusted’.
-
Mojo Handle Verification Bypass: Since the connection is marked ‘kTrusted’, any calls to ‘EncodeHandle’ inside ‘mojo/core/ipcz_driver/transport.cc’ skip handle safety checks entirely. The ‘MaybeCheckIfHandleIsUnsafe’ routine—which normally restricts handle transfers to safe types (such as ‘File’ or ‘Section’) and crashes on other types—is completely bypassed.
-
Confused Deputy via RelayMessage: Because ‘is_isolated’ is true, the transport destination type is initialized as ‘Transport::kBroker’. When decoding a handle with ‘handle_owner == HandleOwner::kRecipient’ in ‘DecodeHandle’ (‘mojo/core/ipcz_driver/transport.cc:203’), the safety check is bypassed since ‘destination_type’ is ‘kBroker’. This allows the updater to adopt any handle matching an active index in its own handle table.
An attacker can send a raw ‘msg::RelayMessage’ with a ‘HandleOwner::kRecipient’ driver object targeting a sensitive handle inside the updater process. The updater adopts the handle, then relays the message back to the attacker using ‘DuplicateHandle’ with ‘DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE’, resulting in the handle being transferred to the attacker and closed in the updater.
Potential Attack Scenario
Note: These are potential steps and findings derived from static analysis of the codebase. Our tooling does not currently have the capability to run code or execute a live proof of concept.
- A local low-privileged attacker connects to the system-scope updater’s named pipe (granted ‘GRGW’ to ‘Authenticated Users’ in the DACL).
- The attacker establishes an isolated Mojo connection and locates the index of a target handle (e.g., a write-access file handle or token handle) within the updater process via ‘NtQuerySystemInformation’.
- The attacker transmits a crafted ‘msg::RelayMessage’ containing a driver object with the target handle index and ‘handle_owner’ set to ‘HandleOwner::kRecipient’.
- The updater adopts the handle, serializes it back to the attacker as part of the relay dispatch, and executes ‘DuplicateHandle’ with the source close option into the attacker’s process.
- The attacker obtains the duplicated handle with original permissions, achieving handle harvesting (LPE) or causing the updater to crash (DoS) because its own copy of the handle was closed.
Suggested Fix
- Modify ‘mojo::IsolatedConnection::Connect’ (or add an overload) to accept and forward the extra invitation flags so that ‘MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS’ is properly preserved and enforced.
- In ‘NamedMojoMessagePipeServer::OnConnectionAccepted’, ensure that the extra invitation flags are plumbed into the isolated connection object.
- In ‘MaybeCheckIfHandleIsUnsafe’, return a status boolean rather than crashing the process via ‘NOTREACHED()’ on invalid handle types to prevent Denial of Service attacks on the updater.
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
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.