Chrome · Updater › Mojo
CVE-2026-110159
Authorization Error in Updater (Mojo)
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/named_mojo_ipc_server/named_mojo_message_pipe_server.cc |
modified |
Files Changed
chrome/updater/BUILD.gnmojo/core/ipcz_driver/transport_test.ccmojo/core/ipcz_driver/transport.cccomponents/named_mojo_ipc_server/endpoint_options.hchrome/updater/ipc/ipc_security_linux.ccchrome/updater/ipc/update_service_proxy_mojo.ccchrome/updater/ipc/ipc_security_mac.ccmojo/core/ipcz_driver/transport.hchrome/updater/ipc/ipc_security_win.ccchrome/updater/ipc/ipc_security_win_unittest.ccchrome/updater/app/server/update_service_stub.ccchrome/updater/ipc/ipc_security.hcomponents/named_mojo_ipc_server/named_mojo_ipc_server_unittest.cccomponents/named_mojo_ipc_server/named_mojo_message_pipe_server.cc
Patch
From d003e09e940872de8f3e564be750ea58e4e60987 Mon Sep 17 00:00:00 2001 From: S Ganesh <[email protected]> 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 <[email protected]> Commit-Queue: S Ganesh <[email protected]> Reviewed-by: Daniel Cheng <[email protected]> Reviewed-by: Noah Rose Ledesma <[email protected]> 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.
References
On This Page