High chrome Authorization Error 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Updater
DescriptionIncorrect authorization in Updater
ComponentUpdater
Sub-componentMojo
Bug ClassAuthorization Error
Tracker564573066
Fix commitd003e09e9408 (chromium/src) +775/-78
CISA KEVNot listed
CreditedAria Akhavan - Haikhavan Security
Disclosed2026-10-13

Changed Functions

FunctionChangeNotes
if
components/named_mojo_ipc_server/named_mojo_message_pipe_server.cc
modified

Files Changed

  • chrome/updater/BUILD.gn
  • mojo/core/ipcz_driver/transport_test.cc
  • mojo/core/ipcz_driver/transport.cc
  • components/named_mojo_ipc_server/endpoint_options.h
  • chrome/updater/ipc/ipc_security_linux.cc
  • chrome/updater/ipc/update_service_proxy_mojo.cc
  • chrome/updater/ipc/ipc_security_mac.cc
  • mojo/core/ipcz_driver/transport.h
  • chrome/updater/ipc/ipc_security_win.cc
  • chrome/updater/ipc/ipc_security_win_unittest.cc
  • chrome/updater/app/server/update_service_stub.cc
  • chrome/updater/ipc/ipc_security.h
  • components/named_mojo_ipc_server/named_mojo_ipc_server_unittest.cc
  • components/named_mojo_ipc_server/named_mojo_message_pipe_server.cc
From d003e09e940872de8f3e564be750ea58e4e60987 Mon Sep 17 00:00:00 2001
From: S Ganesh <ganesh@chromium.org>
Date: Wed, 07 Oct 2026 15:33:33 -0700
Subject: [PATCH] [mojo]: Validate ipcz transport handles and sender trust

Sending handles between processes on Windows requires either the
sender or the receiver to duplicate the handle into its peer process
using DuplicateHandle(). Handles sent over ipcz are marked as kSender
(meaning the sender still owns the handle and the receiver is
responsible for duplicating it) or kRecipient (meaning the sender has
already duplicated the handle into the receiver) to coordinate this
handle transfer, since only one side should duplicate the handle.
Moreover, duplicating a handle into another process is a powerful
capability: if one side is more privileged, the convention is for the
more privileged side to duplicate handles.

When using a Mojo isolated connection, both sides are considered
brokers. Optionally, one side can mark the other as untrusted using
MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS - in that case, the
untrusted side should never be duplicating handles into its peer.
However, two places in ipcz_driver::Transport failed to enforce safety
invariants around handle duplication:

1. DecodeHandle() accepted HandleOwner::kRecipient handles from any
   kBroker peer (unless the receiving endpoint itself was marked
   elevated). Even though the untrusted sending broker could not
   actually duplicate handles into the receiving process, it could
   still guess numeric HANDLE values that happened to name pre-existing
   handles in the receiving process's handle table.
2. Transport::Deserialize() allowed even untrusted peers to name a
   transport's remote_process_.

Changes:
- DecodeHandle() no longer allows kRecipient handles from
  explicitly-untrusted broker peers - this is encapsulated in
  Transport::CanAcceptReceiverOwnedHandles().
- Transport::Deserialize() only allows remote processes to be
  specified if the sender is a non-broker and the receiver is a broker
  (as in the case of mojo::DirectReceiver) or if the peer is not
  marked as kUntrusted. This is encapsulated in
  Transport::CanAcceptRemoteProcessFromPeer().
- In the updater service, CreateServerEndpointOptions() previously
  set MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS for every caller of
  every install. Now that DecodeHandle() rejects kRecipient handles
  from untrusted peers, that declaration is made per caller, and only
  by a system install (where callers cross a privilege boundary): a
  new EndpointOptions::send_invitation_flags_callback lets
  NamedMojoMessagePipeServer choose the invitation flags for each
  connecting client, and the updater declares a caller untrusted if
  its token is below High integrity. Such a caller cannot open the
  SYSTEM server process and so never pre-duplicated legitimately. An
  elevated administrator or SYSTEM caller is not declared untrusted
  and keeps today's behavior. User installs, where both ends are
  equally privileged, declare nothing.
- To match the stricter checks in DecodeHandle(),
  UpdateServiceProxyMojoImpl uses MOJO_SEND_INVITATION_FLAG_ELEVATED
  when connecting to a system install on Windows. This keeps the more
  privileged side doing the duplication even for callers capable of
  opening the server process, and keeps such callers working should
  the server's classification ever be tightened.

Compatibility: clients built before this change keep working against
a server built after it. A caller below High integrity cannot open the
SYSTEM server process and therefore already sends kSender, which the
server duplicates; a forged kRecipient handle from it is now refused.
An elevated caller is not declared untrusted, so its pre-duplicated
handles - including the link memory handle carried by the isolated
connection handshake itself - are accepted as before. This matters
because standalone installers bundle an older updater and run it
elevated against whichever version is active.

TAG=agy
CONV=f1f0f9e4-5cf6-48fb-aa22-494d1abe9ed1

Bug: 564573066, 565252396
Test: mojo_unittests, components_unittests, ipc_tests, updater_tests
Change-Id: I4236c7a0a9104b010300e79ac1b63d9bc0c8887e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8417663
Reviewed-by: Andrea Orru <andreaorru@chromium.org>
Commit-Queue: S Ganesh <ganesh@chromium.org>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Reviewed-by: Noah Rose Ledesma <noahrose@google.com>
Cr-Commit-Position: refs/heads/main@{#1713945}
---
diff --git a/chrome/updater/app/server/update_service_stub.cc b/chrome/updater/app/server/update_service_stub.cc
index 6d48808..b0cad93 100644
--- a/chrome/updater/app/server/update_service_stub.cc
+++ b/chrome/updater/app/server/update_service_stub.cc
@@ -405,7 +405,8 @@
     base::RepeatingClosure task_end_listener,
     base::RepeatingClosure endpoint_created_listener_for_testing)
     : filter_(std::make_unique<UpdateServiceStubUntrusted>(this)),
-      server_(CreateServerEndpointOptions(GetUpdateServiceServerName(scope)),
+      server_(CreateServerEndpointOptions(scope,
+                                          GetUpdateServiceServerName(scope)),
               MakeImplProvider(this, filter_.get())),
       impl_(impl),
       task_start_listener_(task_start_listener),
diff --git a/chrome/updater/ipc/ipc_security.h b/chrome/updater/ipc/ipc_security.h
index 16808c5e..c993ca51 100644
--- a/chrome/updater/ipc/ipc_security.h
+++ b/chrome/updater/ipc/ipc_security.h
@@ -9,6 +9,11 @@
 #include "chrome/updater/updater_scope.h"
 #include "mojo/public/cpp/platform/named_platform_channel.h"
 
+#if BUILDFLAG(IS_WIN)
+#include "base/win/windows_types.h"
+#include "mojo/public/c/system/invitation.h"
+#endif
+
 namespace named_mojo_ipc_server {
 struct ConnectionInfo;
 struct EndpointOptions;
@@ -20,8 +25,19 @@
 bool IsConnectionTrusted(
     const named_mojo_ipc_server::ConnectionInfo& connector);
 
-// Creates the options for instantiating the `NamedMojoIpcServer`.
+// Creates the options for instantiating the `NamedMojoIpcServer` serving
+// `scope`. How the endpoint is secured is derived from `scope`, which callers
+// must also derive `server_name` from, so that a pipe's name and its
+// protection cannot disagree.
+//
+// In production these never diverge: the only caller is `UpdateServiceStub`,
+// whose scope is `App::updater_scope()`, which is initialized from
+// `GetUpdaterScope()` - the same value the parameterless `IsSystemInstall()`
+// reads. Passing it explicitly keeps the decision an input rather than ambient
+// process state, matches `CreateProtectedServerEndpointOptions()` below, and
+// lets tests cover both scopes without mutating the process command line.
 named_mojo_ipc_server::EndpointOptions CreateServerEndpointOptions(
+    UpdaterScope scope,
     const mojo::NamedPlatformChannel::ServerName& server_name);
 
 #if BUILDFLAG(IS_WIN)
@@ -30,6 +46,20 @@
 named_mojo_ipc_server::EndpointOptions CreateProtectedServerEndpointOptions(
     UpdaterScope scope,
     const mojo::NamedPlatformChannel::ServerName& server_name);
+
+// The extra invitation flags a system install's server uses for a caller,
+// chosen by the caller's token integrity level: a caller at High integrity or
+// above (an elevated administrator or SYSTEM) gets none, any other caller is
+// declared untrusted. `MAXDWORD` denotes a token with no integrity level, or
+// one that could not be read, and is treated as untrusted.
+MojoSendInvitationFlags SendInvitationFlagsForIntegrityLevel(
+    DWORD integrity_level);
+
+// `SendInvitationFlagsForIntegrityLevel()` applied to the token of
+// `info.process`, which must have been opened by the server (see
+// `EndpointOptions::include_peer_process_info`).
+MojoSendInvitationFlags SendInvitationFlagsForCaller(
+    const named_mojo_ipc_server::ConnectionInfo& info);
 #endif
 
 }  // namespace updater
diff --git a/chrome/updater/ipc/ipc_security_linux.cc b/chrome/updater/ipc/ipc_security_linux.cc
index 0e88fa8d..d270fb9a 100644
--- a/chrome/updater/ipc/ipc_security_linux.cc
+++ b/chrome/updater/ipc/ipc_security_linux.cc
@@ -20,6 +20,7 @@
 }
 
 named_mojo_ipc_server::EndpointOptions CreateServerEndpointOptions(
+    [[maybe_unused]] UpdaterScope scope,
     const mojo::NamedPlatformChannel::ServerName& server_name) {
   named_mojo_ipc_server::EndpointOptions options;
   options.server_name = server_name;
diff --git a/chrome/updater/ipc/ipc_security_mac.cc b/chrome/updater/ipc/ipc_security_mac.cc
index ee9c991..239c8dd 100644
--- a/chrome/updater/ipc/ipc_security_mac.cc
+++ b/chrome/updater/ipc/ipc_security_mac.cc
@@ -21,6 +21,7 @@
 }
 
 named_mojo_ipc_server::EndpointOptions CreateServerEndpointOptions(
+    [[maybe_unused]] UpdaterScope scope,
     const mojo::NamedPlatformChannel::ServerName& server_name) {
   return {server_name,
           named_mojo_ipc_server::EndpointOptions::kUseIsolatedConnection,
diff --git a/chrome/updater/ipc/ipc_security_win.cc b/chrome/updater/ipc/ipc_security_win.cc
index 45f0ce3..1d52a03 100644
--- a/chrome/updater/ipc/ipc_security_win.cc
+++ b/chrome/updater/ipc/ipc_security_win.cc
@@ -4,7 +4,14 @@
 
 #include "chrome/updater/ipc/ipc_security.h"
 
+#include <windows.h>
+
+#include <optional>
+
 #include "base/check.h"
+#include "base/functional/bind.h"
+#include "base/process/process.h"
+#include "base/win/access_token.h"
 #include "chrome/updater/get_updater_scope.h"
 #include "components/named_mojo_ipc_server/connection_info.h"
 #include "components/named_mojo_ipc_server/endpoint_options.h"
@@ -23,14 +30,61 @@
   return !IsSystemInstall();
 }
 
+MojoSendInvitationFlags SendInvitationFlagsForIntegrityLevel(
+    DWORD integrity_level) {
+  // A caller at High integrity or above is an elevated administrator or
+  // SYSTEM, and so at least as privileged as this process; an administrator
+  // to SYSTEM transition is not a security boundary. Anything below that,
+  // or a token with no integrity level, is declared untrusted.
+  return integrity_level != MAXDWORD &&
+                 integrity_level >= SECURITY_MANDATORY_HIGH_RID
+             ? MOJO_SEND_INVITATION_FLAG_NONE
+             : MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS;
+}
+
+MojoSendInvitationFlags SendInvitationFlagsForCaller(
+    const named_mojo_ipc_server::ConnectionInfo& info) {
+  // Fail closed: a caller whose token cannot be read is declared untrusted.
+  const std::optional<base::win::AccessToken> token =
+      info.process.IsValid()
+          ? base::win::AccessToken::FromProcess(info.process.Handle())
+          : std::nullopt;
+  return SendInvitationFlagsForIntegrityLevel(token ? token->IntegrityLevel()
+                                                    : MAXDWORD);
+}
+
 named_mojo_ipc_server::EndpointOptions CreateServerEndpointOptions(
+    UpdaterScope scope,
     const mojo::NamedPlatformChannel::ServerName& server_name) {
   named_mojo_ipc_server::EndpointOptions options{
       server_name,
-      named_mojo_ipc_server::EndpointOptions::kUseIsolatedConnection,
-      MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS};
+      named_mojo_ipc_server::EndpointOptions::kUseIsolatedConnection};
 
-  if (IsSystemInstall()) {
+  if (IsSystemInstall(scope)) {
+    // This server runs as SYSTEM while the DACL below lets any authenticated
+    // user connect. A caller below High integrity is strictly less privileged
+    // than this process, and is declared untrusted so that the connection
+    // refuses objects whose safety depends on the caller's good behavior,
+    // notably handles it claims to have already duplicated into this process.
+    // See `Transport::CanAcceptReceiverOwnedHandles()`. Such a caller cannot
+    // open this process and so never pre-duplicates legitimately; nothing it
+    // could do before is lost.
+    //
+    // The declaration is made per caller rather than for the endpoint so that
+    // elevated callers - administrators and SYSTEM - keep today's behavior.
+    // They can open this process and pre-duplicate, and callers built before
+    // this change do so, including during the connection handshake itself;
+    // declaring them untrusted would refuse the connection outright. Elevated
+    // callers built after this change declare the server elevated instead and
+    // send handles for this process to duplicate; see
+    // `UpdateServiceProxyMojoImpl::OnConnected()`.
+    //
+    // A user install has no such boundary - server and callers run as the same
+    // user - so no caller is declared untrusted there.
+    options.include_peer_process_info = true;
+    options.send_invitation_flags_callback =
+        base::BindRepeating(&SendInvitationFlagsForCaller);
+
     // A DACL to grant:
     // GA = Generic All
     // access to:
@@ -51,7 +105,7 @@
     const mojo::NamedPlatformChannel::ServerName& server_name) {
   CHECK(IsSystemInstall(scope));
   named_mojo_ipc_server::EndpointOptions options =
-      CreateServerEndpointOptions(server_name);
+      CreateServerEndpointOptions(scope, server_name);
   options.pipe_name_type =
       mojo::NamedPlatformChannel::PipeNameType::kAdminProtected;
 
diff --git a/chrome/updater/ipc/update_service_proxy_mojo.cc b/chrome/updater/ipc/update_service_proxy_mojo.cc
index e2aea55..1950d42e 100644
--- a/chrome/updater/ipc/update_service_proxy_mojo.cc
+++ b/chrome/updater/ipc/update_service_proxy_mojo.cc
@@ -20,6 +20,7 @@
 #include "base/logging.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/memory/weak_ptr.h"
+#include "base/process/process.h"
 #include "base/sequence_checker.h"
 #include "base/task/bind_post_task.h"
 #include "base/task/sequenced_task_runner.h"
@@ -41,6 +42,7 @@
 #include "chrome/updater/util/posix_util.h"
 #include "components/named_mojo_ipc_server/named_mojo_ipc_server_client_util.h"
 #include "components/policy/core/common/policy_types.h"
+#include "mojo/public/c/system/invitation.h"
 #include "mojo/public/cpp/bindings/callback_helpers.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "mojo/public/cpp/bindings/pending_remote.h"
@@ -406,13 +408,45 @@
     return;
   }
 
+  // A system install's server is at least as privileged as this process - it
+  // runs as SYSTEM while callers may be any authenticated user. Saying so here
+  // keeps handles this process sends owned by this process, for the server to
+  // duplicate out of it, rather than pre-duplicated into the server. If this
+  // process cannot open the server (it runs below High integrity) that is
+  // what happens anyway. If it can (it is elevated, or SYSTEM) the server
+  // would still accept pre-duplicated handles from it, since the server only
+  // declares callers below High integrity untrusted; the flag keeps the more
+  // privileged side doing the duplication regardless, and keeps this client
+  // working should the server's classification ever be tightened. A user
+  // install's server runs as this same user, so no flag is passed there. See
+  // `CreateServerEndpointOptions()` in chrome/updater/ipc/ipc_security_win.cc
+  // and `Transport::CanAcceptReceiverOwnedHandles()`.
+  //
+  // This also grants trust in the other direction: the flag sets
+  // `is_peer_trusted` on this end, so this process will accept pre-duplicated
+  // handles and broker-destined transports from whatever is on the far end of
+  // the pipe. That is only safe because `ConnectToUpdateService()` in
+  // update_service_dialer_win.cc connects with `verify_server_privilege` and
+  // rejects the endpoint unless `IsServerElevated()` holds for system scope.
+  // Do not relax that check without revisiting this flag.
+  //
+  // Handle ownership is a Windows-only concept; elsewhere the flag is rejected.
+  MojoSendInvitationFlags invitation_flags = MOJO_SEND_INVITATION_FLAG_NONE;
+#if BUILDFLAG(IS_WIN)
+  if (IsSystemInstall(scope_)) {
+    invitation_flags = MOJO_SEND_INVITATION_FLAG_ELEVATED;
+  }
+#endif  // BUILDFLAG(IS_WIN)
+
   auto connection = std::make_unique<mojo::IsolatedConnection>();
   // Connect `remote_` to the RPC server by fusing its message pipe to the one
   // created by `IsolatedConnection::Connect`.
   if (!mojo::FusePipes(
           std::move(pending_receiver),
           mojo::PendingRemote<mojom::UpdateService>(
-              connection->Connect(std::move(endpoint.value())), 0))) {
+              connection->Connect(std::move(endpoint.value()), base::Process(),
+                                  invitation_flags),
+              0))) {
     LOG(ERROR) << "Failed to fuse Mojo pipes for RPC.";
     remote_.reset();
     return;
diff --git a/components/named_mojo_ipc_server/endpoint_options.h b/components/named_mojo_ipc_server/endpoint_options.h
index b6f44eb..17d6428 100644
--- a/components/named_mojo_ipc_server/endpoint_options.h
+++ b/components/named_mojo_ipc_server/endpoint_options.h
@@ -10,6 +10,7 @@
 #include <string>
 #include <variant>
 
+#include "base/functional/callback.h"
 #include "build/build_config.h"
 #include "mojo/public/c/system/invitation.h"
 #include "mojo/public/cpp/platform/named_platform_channel.h"
@@ -20,6 +21,8 @@
 
 namespace named_mojo_ipc_server {
 
+struct ConnectionInfo;
+
 // Options used by NamedMojoIpcServer to start the server endpoint.
 struct EndpointOptions {
   using MessagePipeId = std::variant<std::monostate, uint64_t, std::string>;
@@ -59,6 +62,15 @@
   MojoSendInvitationFlags extra_send_invitation_flags =
       MOJO_SEND_INVITATION_FLAG_NONE;
 
+  // If set, called for each client that has passed validation to choose the
+  // extra flags for the invitation sent to that client, in place of
+  // `extra_send_invitation_flags`. This lets a server declare only some of its
+  // callers untrusted (MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS), based on
+  // who they are. Runs on the server's sequence.
+  using SendInvitationFlagsCallback =
+      base::RepeatingCallback<MojoSendInvitationFlags(const ConnectionInfo&)>;
+  SendInvitationFlagsCallback send_invitation_flags_callback;
+
 #if BUILDFLAG(IS_WIN)
   // If non-empty, a security descriptor to use when creating the pipe. If
   // empty, a default security descriptor will be used.
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 3feb210..7010b2e 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
@@ -145,12 +145,16 @@
   }
 #undef INVALID_PROCESS_LOG
 
+  const MojoSendInvitationFlags send_invitation_flags =
+      options_.send_invitation_flags_callback
+          ? options_.send_invitation_flags_callback.Run(*info)
+          : options_.extra_send_invitation_flags;
+
   if (is_isolated) {
     // Create isolated connection.
     auto connection = std::make_unique<mojo::IsolatedConnection>();
-    mojo::ScopedMessagePipeHandle message_pipe =
-        connection->Connect(std::move(endpoint), std::move(peer_process),
-                            options_.extra_send_invitation_flags);
+    mojo::ScopedMessagePipeHandle message_pipe = connection->Connect(
+        std::move(endpoint), std::move(peer_process), send_invitation_flags);
     on_message_pipe_ready_.Run(std::move(message_pipe), std::move(info),
                                result.context, std::move(connection));
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/updater/ipc/ipc_security_win_unittest.cc b/chrome/updater/ipc/ipc_security_win_unittest.cc
new file mode 100644
index 0000000..f7d8038
--- /dev/null
+++ b/chrome/updater/ipc/ipc_security_win_unittest.cc
@@ -0,0 +1,124 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/updater/ipc/ipc_security.h"
+
+#include <windows.h>
+
+#include <optional>
+
+#include "base/process/process.h"
+#include "base/process/process_handle.h"
+#include "base/win/access_token.h"
+#include "chrome/updater/updater_scope.h"
+#include "components/named_mojo_ipc_server/connection_info.h"
+#include "components/named_mojo_ipc_server/endpoint_options.h"
+#include "mojo/public/c/system/invitation.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace updater {
+
+namespace {
+
+constexpr wchar_t kTestServerName[] = L"IpcSecurityWinTestServerName";
+
+}  // namespace
+
+// Only a system install's server classifies its callers: it runs as SYSTEM and
+// its pipe is open to any authenticated user. A user install's server and its
+// callers run as the same user at the same integrity level, so there is
+// nothing to defend and no caller is declared untrusted.
+TEST(IpcSecurityWinTest, CallerClassificationIsSystemOnly) {
+  const named_mojo_ipc_server::EndpointOptions user_options =
+      CreateServerEndpointOptions(UpdaterScope::kUser, kTestServerName);
+  EXPECT_FALSE(user_options.send_invitation_flags_callback);
+  EXPECT_FALSE(user_options.include_peer_process_info);
+  EXPECT_EQ(user_options.extra_send_invitation_flags,
+            MOJO_SEND_INVITATION_FLAG_NONE);
+  EXPECT_TRUE(user_options.security_descriptor.empty());
+
+  const named_mojo_ipc_server::EndpointOptions system_options =
+      CreateServerEndpointOptions(UpdaterScope::kSystem, kTestServerName);
+  EXPECT_TRUE(system_options.send_invitation_flags_callback);
+  // The callback reads the caller's token, so it needs the caller's process.
+  EXPECT_TRUE(system_options.include_peer_process_info);
+  EXPECT_FALSE(system_options.security_descriptor.empty());
+}
+
+// The endpoint's security is a function of the scope it was asked for, not of
+// the scope this process happens to be running as. Production always passes
+// the process's own scope, so this pins the coupling rather than guarding a
+// reachable mismatch: it fails if the implementation goes back to reading the
+// process-global `IsSystemInstall()`, since this test process is user scope.
+TEST(IpcSecurityWinTest, OptionsFollowRequestedScopeNotProcessScope) {
+  EXPECT_FALSE(CreateServerEndpointOptions(UpdaterScope::kUser, kTestServerName)
+                   .send_invitation_flags_callback);
+  EXPECT_TRUE(
+      CreateServerEndpointOptions(UpdaterScope::kSystem, kTestServerName)
+          .send_invitation_flags_callback);
+}
+
+// The protected endpoint is system-only by construction, so it classifies
+// callers too. Its DACL admits only administrators and SYSTEM, so in practice
+// every caller is trusted.
+TEST(IpcSecurityWinTest, ProtectedEndpointClassifiesCallers) {
+  const named_mojo_ipc_server::EndpointOptions options =
+      CreateProtectedServerEndpointOptions(UpdaterScope::kSystem,
+                                           kTestServerName);
+  EXPECT_TRUE(options.send_invitation_flags_callback);
+  EXPECT_TRUE(options.include_peer_process_info);
+}
+
+// A caller at High integrity or above (an elevated administrator, or SYSTEM)
+// is at least as privileged as the server and keeps the behavior it had before
+// callers were classified: it may pre-duplicate handles into the server. This
+// is what keeps elevated callers built before that change - which do
+// pre-duplicate, including during the connection handshake - able to connect.
+// Everything below High integrity cannot open the SYSTEM server and so never
+// pre-duplicates legitimately; it is declared untrusted so that the server
+// refuses handle values it merely claims to have duplicated.
+TEST(IpcSecurityWinTest, IntegrityLevelClassification) {
+  EXPECT_EQ(SendInvitationFlagsForIntegrityLevel(SECURITY_MANDATORY_SYSTEM_RID),
+            MOJO_SEND_INVITATION_FLAG_NONE);
+  EXPECT_EQ(SendInvitationFlagsForIntegrityLevel(SECURITY_MANDATORY_HIGH_RID),
+            MOJO_SEND_INVITATION_FLAG_NONE);
+  EXPECT_EQ(SendInvitationFlagsForIntegrityLevel(SECURITY_MANDATORY_MEDIUM_RID),
+            MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS);
+  EXPECT_EQ(SendInvitationFlagsForIntegrityLevel(SECURITY_MANDATORY_LOW_RID),
+            MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS);
+  EXPECT_EQ(
+      SendInvitationFlagsForIntegrityLevel(SECURITY_MANDATORY_UNTRUSTED_RID),
+      MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS);
+  // No integrity level at all.
+  EXPECT_EQ(SendInvitationFlagsForIntegrityLevel(MAXDWORD),
+            MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS);
+}
+
+// The per-caller classification reads the token of the process the server
+// opened. This process stands in for a caller; whatever its integrity level
+// is, the classification must agree with reading it directly.
+TEST(IpcSecurityWinTest, CallerClassificationReadsCallerToken) {
+  named_mojo_ipc_server::ConnectionInfo info;
+  info.pid = base::GetCurrentProcId();
+  info.process = base::Process::OpenWithAccess(
+      info.pid, PROCESS_QUERY_LIMITED_INFORMATION);
+  ASSERT_TRUE(info.process.IsValid());
+
+  const std::optional<base::win::AccessToken> token =
+      base::win::AccessToken::FromCurrentProcess();
+  ASSERT_TRUE(token);
+  EXPECT_EQ(SendInvitationFlagsForCaller(info),
+            SendInvitationFlagsForIntegrityLevel(token->IntegrityLevel()));
+}
+
+// A caller whose process the server could not open is declared untrusted
+// rather than trusted by default.
+TEST(IpcSecurityWinTest, CallerWithoutProcessIsUntrusted) {
+  named_mojo_ipc_server::ConnectionInfo info;
+  ASSERT_FALSE(info.process.IsValid());
+  EXPECT_EQ(SendInvitationFlagsForCaller(info),
+            MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS);
+}
+
+}  // namespace updater
diff --git a/components/named_mojo_ipc_server/named_mojo_ipc_server_unittest.cc b/components/named_mojo_ipc_server/named_mojo_ipc_server_unittest.cc
index b973f83..4afcba8 100644
--- a/components/named_mojo_ipc_server/named_mojo_ipc_server_unittest.cc
+++ b/components/named_mojo_ipc_server/named_mojo_ipc_server_unittest.cc
@@ -11,6 +11,7 @@
 #include <memory>
 #include <string>
 #include <string_view>
+#include <vector>
 
 #include "base/command_line.h"
 #include "base/functional/bind.h"
@@ -408,6 +409,26 @@
   WaitForProcessExit(child_process);
 }
 
+TEST_P(NamedMojoIpcServerTest, SendInvitationFlagsCallback_CalledPerClient) {
+  ipc_server_.reset();
+  std::vector<base::ProcessId> callback_pids;
+  EndpointOptions options;
+  options.send_invitation_flags_callback = base::BindLambdaForTesting(
+      [&](const ConnectionInfo& info) -> MojoSendInvitationFlags {
+        callback_pids.push_back(info.pid);
+        return MOJO_SEND_INVITATION_FLAG_NONE;
+      });
+  CreateIpcServer(std::move(options));
+  ipc_server_->StartServer();
+  WaitForServerEndpointCreated();
+
+  base::Process child_process = LaunchClientProcess();
+  base::ProcessId child_pid = child_process.Pid();
+  EXPECT_EQ(0, WaitForProcessExit(child_process));
+  EXPECT_EQ(child_pid, last_echo_string_peer_pid_);
+  EXPECT_THAT(callback_pids, testing::ElementsAre(child_pid));
+}
+
 #if BUILDFLAG(IS_LINUX)
 
 TEST_P(NamedMojoIpcServerTest,
diff --git a/mojo/core/ipcz_driver/transport_test.cc b/mojo/core/ipcz_driver/transport_test.cc
index 72c7484..14cf1d08 100644
--- a/mojo/core/ipcz_driver/transport_test.cc
+++ b/mojo/core/ipcz_driver/transport_test.cc
@@ -22,6 +22,8 @@
 #include "base/memory/scoped_refptr.h"
 #include "base/memory/unsafe_shared_memory_region.h"
 #include "base/path_service.h"
+#include "base/process/process.h"
+#include "base/process/process_handle.h"
 #include "base/strings/string_view_util.h"
 #include "base/synchronization/condition_variable.h"
 #include "base/synchronization/lock.h"
@@ -41,6 +43,10 @@
 #include "mojo/public/cpp/platform/platform_handle.h"
 #include "mojo/public/cpp/system/platform_handle.h"
 
+#if BUILDFLAG(IS_WIN)
+#include "base/win/scoped_handle.h"
+#endif
+
 namespace mojo::core::ipcz_driver {
 namespace {
 
@@ -91,15 +97,20 @@
   }
 
   // Retrieves a PlatformChannel endpoint from `pipe` and returns a newly
-  // constructed Transport over it.
-  static scoped_refptr<Transport> ReceiveTransport(MojoHandle pipe) {
+  // constructed Transport over it. By default the new Transport is a non-broker
+  // endpoint connected to a broker.
+  static scoped_refptr<Transport> ReceiveTransport(
+      MojoHandle pipe,
+      Transport::EndpointTypes endpoint_types =
+          {.source = Transport::kNonBroker, .destination = Transport::kBroker},
+      Transport::ProcessTrust process_trust = Transport::ProcessTrust{}) {
     MojoHandle transport_for_client;
     ReadMessageWithHandles(pipe, &transport_for_client, 1);
     PlatformHandle handle =
         UnwrapPlatformHandle(ScopedHandle(Handle(transport_for_client)));
-    return Transport::Create(
-        {.source = Transport::kNonBroker, .destination = Transport::kBroker},
-        PlatformChannelEndpoint(std::move(handle)));
+    return Transport::Create(endpoint_types,
+                             PlatformChannelEndpoint(std::move(handle)),
+                             base::Process(), process_trust);
   }
 
   static TestMessage SerializeObjectFor(Transport& transmitter,
@@ -826,6 +837,31 @@
       });
 }
 
+// Substitutes `local_handle` for the first handle encoded in `message` and
+// verifies that `transport` refuses to deserialize the result. `message` must
+// have been encoded with HandleOwner::kRecipient. On return `local_handle` must
+// still be owned solely by the caller.
+void ExpectRecipientOwnedHandleRejected(Transport& transport,
+                                        TestMessage message,
+                                        HANDLE local_handle) {
+  const uint64_t value =
+      static_cast<uint64_t>(reinterpret_cast<uintptr_t>(local_handle));
+  base::span(message.bytes)
+      .subspan(Transport::FirstHandleOffsetForTesting())
+      .first<sizeof(uint64_t)>()
+      .copy_from(base::byte_span_from_ref(value));
+
+  scoped_refptr<ObjectBase> object;
+  const IpczResult result = transport.DeserializeObject(
+      base::span(message.bytes), base::span(message.handles), object);
+  EXPECT_EQ(result, IPCZ_RESULT_INVALID_ARGUMENT);
+  EXPECT_FALSE(object);
+
+  // The handle must not have been adopted (and therefore closed) by the
+  // deserializer.
+  EXPECT_TRUE(::SetEvent(local_handle));
+}
+
 DEFINE_TEST_CLIENT_TEST_WITH_PIPE(RecipientHandleFromUntrustedBrokerClient,
                                   MojoIpczTransportTest,
                                   h) {
@@ -836,31 +872,15 @@
   scoped_refptr<Transport> transport = ReceiveTransport(h);
   transport->set_is_elevated(true);
   transport->set_is_trusted_by_peer(true);
+  ASSERT_FALSE(transport->CanAcceptReceiverOwnedHandles());
 
   TransportListener listener(*transport);
 
   // Hold a local handle which the deserializer must not adopt.
-  HANDLE event = ::CreateEvent(nullptr, FALSE, FALSE, nullptr);
-  ASSERT_NE(event, nullptr);
-
-  TestMessage message = listener.WaitForNextMessage();
-  // Substitute the local handle for the one encoded by the host. The host
-  // encoded the message with handles owned by the recipient.
-  uint64_t value = static_cast<uint64_t>(reinterpret_cast<uintptr_t>(event));
-  base::span(message.bytes)
-      .subspan(Transport::FirstHandleOffsetForTesting())
-      .first<sizeof(uint64_t)>()
-      .copy_from(base::byte_span_from_ref(value));
-
-  scoped_refptr<ObjectBase> object;
-  const IpczResult result = transport->DeserializeObject(
-      base::span(message.bytes), base::span(message.handles), object);
-  EXPECT_EQ(result, IPCZ_RESULT_INVALID_ARGUMENT);
-  EXPECT_FALSE(object);
-
-  // The local handle must still be live and owned solely by this scope.
-  EXPECT_TRUE(::SetEvent(event));
-  ::CloseHandle(event);
+  base::win::ScopedHandle event(::CreateEvent(nullptr, FALSE, FALSE, nullptr));
+  ASSERT_TRUE(event.is_valid());
+  ExpectRecipientOwnedHandleRejected(*transport, listener.WaitForNextMessage(),
+                                     event.get());
 
   TestMessage("done").Transmit(*transport);
   EXPECT_EQ(MOJO_RESULT_OK, MojoClose(h));
@@ -888,6 +908,147 @@
       });
 }
 
+DEFINE_TEST_CLIENT_TEST_WITH_PIPE(
+    RecipientHandleFromUntrustedBrokerToBrokerClient,
+    MojoIpczTransportTest,
+    h) {
+  // This client is a broker connected to another broker over what is modeled
+  // on mojo::IsolatedConnection, and it has declared the peer's process
+  // untrustworthy - as the updater does for its public endpoint, which any
+  // authenticated user may connect to. Being a broker confers no implicit trust
... (truncated)
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.