From 67082c93b922eda317b325c1fc4b8bfe09103dd2 Mon Sep 17 00:00:00 2001 From: Mark Mentovai Date: Mon, 8 Sep 2014 21:06:34 -0400 Subject: [PATCH] Add MachMessageServer and its test. MachMessageServer is much like mach_msg_server() and mach_msg_server_once(), but with a C++ interface and with a number of deficiencies corrected. TEST=util_test MachMessageServer.* R=rsesek@chromium.org Review URL: https://codereview.chromium.org/544393002 --- util/mach/mach_message_server.cc | 282 ++++++++++++++ util/mach/mach_message_server.h | 154 ++++++++ util/mach/mach_message_server_test.cc | 517 ++++++++++++++++++++++++++ util/util.gyp | 3 + 4 files changed, 956 insertions(+) create mode 100644 util/mach/mach_message_server.cc create mode 100644 util/mach/mach_message_server.h create mode 100644 util/mach/mach_message_server_test.cc diff --git a/util/mach/mach_message_server.cc b/util/mach/mach_message_server.cc new file mode 100644 index 00000000..dfaaa974 --- /dev/null +++ b/util/mach/mach_message_server.cc @@ -0,0 +1,282 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "util/mach/mach_message_server.h" + +#include + +#include + +#include "base/mac/mach_logging.h" +#include "base/mac/scoped_mach_vm.h" + +namespace crashpad { + +namespace { + +mach_timebase_info_data_t* TimebaseInternal() { + mach_timebase_info_data_t* timebase_info = new mach_timebase_info_data_t; + kern_return_t kr = mach_timebase_info(timebase_info); + MACH_CHECK(kr == KERN_SUCCESS, kr) << "mach_timebase_info"; + return timebase_info; +} + +mach_timebase_info_data_t* Timebase() { + static mach_timebase_info_data_t* timebase_info = TimebaseInternal(); + return timebase_info; +} + +uint64_t NanosecondTicks() { + uint64_t absolute_time = mach_absolute_time(); + mach_timebase_info_data_t* timebase_info = Timebase(); + return absolute_time * timebase_info->numer / timebase_info->denom; +} + +const int kNanosecondsPerMillisecond = 1E6; + +// TimerRunning determines whether |deadline| has passed. If |deadline| is in +// the future, |*remaining_ms| is set to the number of milliseconds remaining, +// which will always be a positive value, and this function returns true. If +// |deadline| is zero (indicating that no timer is in effect), |*remaining_ms| +// is set to zero and this function returns true. Otherwise, this function +// returns false. |deadline| is specified on the same time base as is returned +// by NanosecondTicks(). +bool TimerRunning(uint64_t deadline, mach_msg_timeout_t* remaining_ms) { + if (!deadline) { + *remaining_ms = MACH_MSG_TIMEOUT_NONE; + return true; + } + + uint64_t now = NanosecondTicks(); + + if (now >= deadline) { + return false; + } + + uint64_t remaining = deadline - now; + + // Round to the nearest millisecond, taking care not to overflow. + const int kHalfMillisecondInNanoseconds = kNanosecondsPerMillisecond / 2; + mach_msg_timeout_t remaining_mach; + if (remaining <= + std::numeric_limits::max() - kHalfMillisecondInNanoseconds) { + remaining_mach = (remaining + kHalfMillisecondInNanoseconds) / + kNanosecondsPerMillisecond; + } else { + remaining_mach = remaining / kNanosecondsPerMillisecond; + } + + if (remaining_mach == MACH_MSG_TIMEOUT_NONE) { + return false; + } + + *remaining_ms = remaining_mach; + return true; +} + +} // namespace + +// This implementation is based on 10.9.4 +// xnu-2422.110.17/libsyscall/mach/mach_msg.c mach_msg_server_once(), but +// adapted to local style using scopers, replacing the server callback function +// and |max_size| parameter with a C++ interface, and with the addition of the +// the |persistent| parameter allowing this function to serve as a stand-in for +// mach_msg_server(), the |nonblocking| parameter to control blocking directly, +// and the |timeout_ms| parameter allowing this function to not block +// indefinitely. +// +// static +mach_msg_return_t MachMessageServer::Run(Interface* interface, + mach_port_t receive_port, + mach_msg_options_t options, + Persistent persistent, + Nonblocking nonblocking, + mach_msg_timeout_t timeout_ms) { + options &= ~(MACH_RCV_MSG | MACH_SEND_MSG); + + mach_msg_options_t timeout_options = MACH_RCV_TIMEOUT | MACH_SEND_TIMEOUT | + MACH_RCV_INTERRUPT | MACH_SEND_INTERRUPT; + + uint64_t deadline; + if (nonblocking == kNonblocking) { + options |= timeout_options; + deadline = 0; + } else if (timeout_ms != MACH_MSG_TIMEOUT_NONE) { + options |= timeout_options; + deadline = NanosecondTicks() + + static_cast(timeout_ms) * kNanosecondsPerMillisecond; + } else { + options &= ~timeout_options; + deadline = 0; + } + + mach_msg_size_t trailer_alloc = REQUESTED_TRAILER_SIZE(options); + mach_msg_size_t max_request_size = interface->MachMessageServerRequestSize(); + mach_msg_size_t request_alloc = round_page(max_request_size + trailer_alloc); + mach_msg_size_t request_size = (options & MACH_RCV_LARGE) + ? request_alloc + : max_request_size + trailer_alloc; + + mach_msg_size_t max_reply_size = interface->MachMessageServerReplySize(); + mach_msg_size_t reply_alloc = round_page( + (options & MACH_SEND_TRAILER) ? (max_reply_size + MAX_TRAILER_SIZE) + : max_reply_size); + + mach_port_t self = mach_task_self(); + + kern_return_t kr; + + do { + mach_msg_size_t this_request_alloc = request_alloc; + mach_msg_size_t this_request_size = request_size; + + base::mac::ScopedMachVM request_scoper; + mach_msg_header_t* request_header = NULL; + + while (!request_scoper.address()) { + vm_address_t request_addr; + kr = vm_allocate(self, + &request_addr, + this_request_alloc, + VM_FLAGS_ANYWHERE | VM_MAKE_TAG(VM_MEMORY_MACH_MSG)); + if (kr != KERN_SUCCESS) { + return kr; + } + base::mac::ScopedMachVM trial_request_scoper(request_addr, + this_request_alloc); + request_header = reinterpret_cast(request_addr); + + do { + // If |options| contains MACH_RCV_INTERRUPT, retry mach_msg() in a loop + // when it returns MACH_RCV_INTERRUPTED to recompute |remaining_ms| + // rather than allowing mach_msg() to retry using the original timeout + // value. See 10.9.4 xnu-2422.110.17/libsyscall/mach/mach_msg.c + // mach_msg(). + mach_msg_timeout_t remaining_ms; + if (!TimerRunning(deadline, &remaining_ms)) { + return MACH_RCV_TIMED_OUT; + } + + kr = mach_msg(request_header, + options | MACH_RCV_MSG, + 0, + this_request_size, + receive_port, + remaining_ms, + MACH_PORT_NULL); + } while (kr == MACH_RCV_INTERRUPTED); + + if (kr == MACH_MSG_SUCCESS) { + request_scoper.swap(trial_request_scoper); + } else if (kr == MACH_RCV_TOO_LARGE && options & MACH_RCV_LARGE) { + this_request_size = + round_page(request_header->msgh_size + trailer_alloc); + this_request_alloc = this_request_size; + } else { + return kr; + } + } + + vm_address_t reply_addr; + kr = vm_allocate(self, + &reply_addr, + reply_alloc, + VM_FLAGS_ANYWHERE | VM_MAKE_TAG(VM_MEMORY_MACH_MSG)); + if (kr != KERN_SUCCESS) { + return kr; + } + + base::mac::ScopedMachVM reply_scoper(reply_addr, reply_alloc); + + mach_msg_header_t* reply_header = + reinterpret_cast(reply_addr); + bool destroy_complex_request = false; + interface->MachMessageServerFunction( + request_header, reply_header, &destroy_complex_request); + + if (!(reply_header->msgh_bits & MACH_MSGH_BITS_COMPLEX)) { + mig_reply_error_t* reply_mig = + reinterpret_cast(reply_header); + if (reply_mig->RetCode == MIG_NO_REPLY) { + reply_header->msgh_remote_port = MACH_PORT_NULL; + } else if (reply_mig->RetCode != KERN_SUCCESS && + request_header->msgh_bits & MACH_MSGH_BITS_COMPLEX) { + destroy_complex_request = true; + } + } + + if (destroy_complex_request && + request_header->msgh_bits & MACH_MSGH_BITS_COMPLEX) { + request_header->msgh_remote_port = MACH_PORT_NULL; + mach_msg_destroy(request_header); + } + + if (reply_header->msgh_remote_port != MACH_PORT_NULL) { + // If the reply port right is a send-once right, the send won’t block even + // if the remote side isn’t waiting for a message. No timeout is used, + // which keeps the communication on the kernel’s fast path. If the reply + // port right is a send right, MACH_SEND_TIMEOUT is used to avoid blocking + // indefinitely. This duplicates the logic in 10.9.4 + // xnu-2422.110.17/libsyscall/mach/mach_msg.c mach_msg_server_once(). + mach_msg_option_t send_options = + options | MACH_SEND_MSG | + (MACH_MSGH_BITS_REMOTE(reply_header->msgh_bits) == + MACH_MSG_TYPE_MOVE_SEND_ONCE + ? 0 + : MACH_SEND_TIMEOUT); + + bool running; + do { + // If |options| contains MACH_SEND_INTERRUPT, retry mach_msg() in a loop + // when it returns MACH_SEND_INTERRUPTED to recompute |remaining_ms| + // rather than allowing mach_msg() to retry using the original timeout + // value. See 10.9.4 xnu-2422.110.17/libsyscall/mach/mach_msg.c + // mach_msg(). + mach_msg_timeout_t remaining_ms; + running = TimerRunning(deadline, &remaining_ms); + if (!running) { + // Don’t return just yet. If the timer ran out in between the time the + // request was received and now, at least try to send the response. + remaining_ms = 0; + } + + kr = mach_msg(reply_header, + send_options, + reply_header->msgh_size, + 0, + MACH_PORT_NULL, + remaining_ms, + MACH_PORT_NULL); + } while (kr == MACH_SEND_INTERRUPTED); + + if (kr != KERN_SUCCESS) { + if (kr == MACH_SEND_INVALID_DEST || kr == MACH_SEND_TIMED_OUT) { + mach_msg_destroy(reply_header); + } + return kr; + } + + if (!running) { + // The reply message was sent successfuly, so act as though the deadline + // was reached before or during the subsequent receive operation when in + // persistent mode, and just return success when not in persistent mode. + return (persistent == kPersistent) ? MACH_RCV_TIMED_OUT : kr; + } + } + } while (persistent == kPersistent); + + return kr; +} + +} // namespace crashpad diff --git a/util/mach/mach_message_server.h b/util/mach/mach_message_server.h new file mode 100644 index 00000000..c77fb324 --- /dev/null +++ b/util/mach/mach_message_server.h @@ -0,0 +1,154 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef CRASHPAD_UTIL_MACH_MACH_MESSAGE_SERVER_H_ +#define CRASHPAD_UTIL_MACH_MACH_MESSAGE_SERVER_H_ + +#include + +#include "base/basictypes.h" + +namespace crashpad { + +//! \brief Runs a Mach message server to handle a Mach RPC request for MIG +//! servers. +//! +//! The principal entry point to this interface is the static Run() method. +class MachMessageServer { + public: + //! \brief A Mach RPC callback interface, called by Run(). + class Interface { + public: + //! \brief Handles a Mach RPC request. + //! + //! This method is a stand-in for a MIG-generated Mach RPC server “demux” + //! function such as `exc_server()` and `mach_exc_server()`. Implementations + //! may call such a function directly. This method is expected to behave + //! exactly as these functions behave. + //! + //! \param[in] in The request message, received as a Mach message. + //! \param[out] out The reply message. The caller allocates storage, and the + //! callee is expected to populate the reply message appropriately. + //! After returning, the caller will send this reply as a Mach message + //! via the message’s reply port. + //! \param[out] destroy_complex_request `true` if a complex request message + //! is to be destroyed even when handled successfully, `false` + //! otherwise. The traditional behavior is `false`. In this case, the + //! caller only destroys the request message in \a in when the reply + //! message in \a out is not complex and when it indicates a return code + //! other than `KERN_SUCCESS` or `MIG_NO_REPLY`. The assumption is that + //! the rights or out-of-line data carried in a complex message may be + //! retained by the server in this situation, and that it is the + //! responsibility of the server to release these resources as needed. + //! However, in many cases, these resources are not needed beyond the + //! duration of a request-reply transaction, and in such cases, it is + //! less error-prone to always have the caller, + //! MachMessageServer::Run(), destroy complex request messages. To + //! choose this behavior, this parameter should be set to `true`. + //! + //! \return `true` on success and `false` on failure, although the caller + //! ignores the return value. However, the return code to be included in + //! the reply message should be set as `mig_reply_error_t::RetCode`. The + //! non-`void` return value is used for increased compatibility with + //! MIG-generated functions. + virtual bool MachMessageServerFunction(mach_msg_header_t* in, + mach_msg_header_t* out, + bool* destroy_complex_request) = 0; + + //! \return The expected or maximum size, in bytes, of a request message to + //! be received as the \a in parameter of MachMessageServerFunction(). + virtual mach_msg_size_t MachMessageServerRequestSize() = 0; + + //! \return The maximum size, in bytes, of a reply message to be sent via + //! the \a out parameter of MachMessageServerFunction(). This value does + //! not need to include the size of any trailer to be sent with the + //! message. + virtual mach_msg_size_t MachMessageServerReplySize() = 0; + + protected: + ~Interface() {} + }; + + //! \brief Informs Run() whether to handle a single request-reply transaction + //! or to run in a loop. + enum Persistent { + //! \brief Handle a single request-reply transaction and then return. + kOneShot = false, + + //! \brief Run in a loop, potentially handling multiple request-reply + //! transactions. + kPersistent, + }; + + //! \brief Informs Run() whether or not to block while waiting for requests. + enum Nonblocking { + //! \brief Wait for a request message if none is queued. + kBlocking = false, + + //! \brief Return as soon as there no request messages queued. This may + //! result in an immediate return without handling any requests. + kNonblocking, + }; + + //! \brief Runs a Mach message server to handle a Mach RPC request for MIG + //! servers. + //! + //! This function listens for a request message and passes it to a callback + //! interface. A reponse is collected from that interface, and is sent back as + //! a reply. + //! + //! This function is similar to `mach_msg_server()` and + //! `mach_msg_server_once()`. + //! + //! \param[in] interface The MachMessageServerInterface that is responsible + //! for handling the message. Interface::MachMessageServerRequestSize() is + //! used as the receive size for the request message, and + //! Interface::MachMessageServerReplySize() is used as the + //! maximum size of the reply message. If \a options contains + //! `MACH_RCV_LARGE`, this function will retry a receive operation that + //! returns `MACH_RCV_TOO_LARGE` with an appropriately-sized buffer. + //! MachMessageServerInterface::MachMessageServerFunction() is called to + //! handle the request and populate the reply. + //! \param[in] receive_port The port on which to receive the request message. + //! \param[in] options Options suitable for mach_msg. + //! \param[in] persistent Chooses between one-shot and persistent operation. + //! \param[in] nonblocking Chooses between blocking and nonblocking operation. + //! \param[in] timeout_ms When \a nonblocking is `false`, the the maximum + //! duration that this entire function will run, in milliseconds, or + //! `MACH_MSG_TIMEOUT_NONE` to specify no timeout (infinite waiting). When + //! \a nonblocking is `true`, this parameter has no effect. When \a + //! persistent is `true`, the timeout applies to the overall duration of + //! this function, not to any individual `mach_msg()` call. + //! + //! \return On success, `KERN_SUCCESS` (when \a persistent is `false`) or + //! `MACH_RCV_TIMED_OUT` (when \a persistent and \a nonblocking are both + //! `true`, or when \a persistent is `true`, \a nonblocking is `false`, + //! and \a timeout is not `MACH_MSG_TIMEOUT_NONE`. This function has no + //! successful return value when \a persistent is `true`, \a nonblocking + //! is `false`, and \a timeout is `MACH_MSG_TIMEOUT_NONE`. On failure, + //! returns a value identifying the nature of the error. + static mach_msg_return_t Run(Interface* interface, + mach_port_t receive_port, + mach_msg_options_t options, + Persistent persistent, + Nonblocking nonblocking, + mach_msg_timeout_t timeout_ms); + + private: + DISALLOW_IMPLICIT_CONSTRUCTORS(MachMessageServer); +}; + +} // namespace crashpad + +#endif // CRASHPAD_UTIL_MACH_MACH_MESSAGE_SERVER_H_ diff --git a/util/mach/mach_message_server_test.cc b/util/mach/mach_message_server_test.cc new file mode 100644 index 00000000..dcbbae38 --- /dev/null +++ b/util/mach/mach_message_server_test.cc @@ -0,0 +1,517 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "util/mach/mach_message_server.h" + +#include +#include + +#include "base/basictypes.h" +#include "base/mac/scoped_mach_port.h" +#include "gtest/gtest.h" +#include "util/file/fd_io.h" +#include "util/test/errors.h" +#include "util/test/mac/mach_errors.h" +#include "util/test/mac/mach_multiprocess.h" + +namespace { + +using namespace crashpad; +using namespace crashpad::test; + +class TestMachMessageServer : public MachMessageServer::Interface, + public MachMultiprocess { + public: + struct Options { + // The type of reply port that the client should put in its request message. + enum ReplyPortType { + // The normal reply port is the client’s local port, to which it holds + // a receive right. This allows the server to respond directly to the + // client. The client will expect a reply. + kReplyPortNormal, + + // Use MACH_PORT_NULL as the reply port, which the server should detect + // avoid attempting to send a message to, and return success. The client + // will not expect a reply. + kReplyPortNull, + + // Make the server see the reply port as a dead name by setting the reply + // port to a receive right and then destroying that right before the + // server processes the request. The server should return + // MACH_SEND_INVALID_DEST, and the client will not expect a reply. + kReplyPortDead, + }; + + Options() + : expect_server_interface_method_called(true), + parent_wait_for_child_pipe(false), + server_persistent(MachMessageServer::kOneShot), + server_nonblocking(MachMessageServer::kBlocking), + server_timeout_ms(MACH_MSG_TIMEOUT_NONE), + server_mig_retcode(KERN_SUCCESS), + expect_server_result(KERN_SUCCESS), + client_send_request_count(1), + client_reply_port_type(kReplyPortNormal), + child_send_all_requests_before_receiving_any_replies(false) { + } + + // true if MachMessageServerFunction() is expected to be called. + bool expect_server_interface_method_called; + + // true if the parent should wait for the child to write a byte to the pipe + // as a signal that the child is ready for the parent to begin its side of + // the test. This is used for nonblocking tests, which require that there + // be something in the server’s queue before attempting a nonblocking + // receive if the receive is to be successful. + bool parent_wait_for_child_pipe; + + // Whether the server should run in one-shot or persistent mode. + MachMessageServer::Persistent server_persistent; + + // Whether the server should run in blocking or nonblocking mode. + MachMessageServer::Nonblocking server_nonblocking; + + // The server’s timeout. + mach_msg_timeout_t server_timeout_ms; + + // The return code that the server returns to the client via the + // mig_reply_error_t::RetCode field. A client would normally see this as + // a Mach RPC return value. + kern_return_t server_mig_retcode; + + // The expected return value from MachMessageServer::Run(). + kern_return_t expect_server_result; + + // The number of requests that the client should send to the server. + size_t client_send_request_count; + + // The type of reply port that the client should provide in its request’s + // mach_msg_header_t::msgh_local_port, which will appear to the server as + // mach_msg_header_t::msgh_remote_port. + ReplyPortType client_reply_port_type; + + // true if the client should send all requests before attempting to receive + // any replies from the server. This is used for the persistent nonblocking + // test, which requires the client to fill the server’s queue before the + // server can attempt processing it. + bool child_send_all_requests_before_receiving_any_replies; + }; + + explicit TestMachMessageServer(const Options& options) + : MachMessageServer::Interface(), + MachMultiprocess(), + options_(options) { + } + + // Runs the test. + void Test() { + EXPECT_EQ(requests_, replies_); + uint32_t start = requests_; + + Run(); + + EXPECT_EQ(requests_, replies_); + EXPECT_EQ(options_.client_send_request_count, requests_ - start); + } + + // MachMessageServerInterface: + + virtual bool MachMessageServerFunction( + mach_msg_header_t* in, + mach_msg_header_t* out, + bool* destroy_complex_request) override { + *destroy_complex_request = true; + + EXPECT_TRUE(options_.expect_server_interface_method_called); + if (!options_.expect_server_interface_method_called) { + return false; + } + + struct ReceiveRequestMessage : public RequestMessage { + mach_msg_trailer_t trailer; + }; + + const ReceiveRequestMessage* request = + reinterpret_cast(in); + EXPECT_EQ(static_cast( + MACH_MSGH_BITS(MACH_MSG_TYPE_MOVE_SEND, MACH_MSG_TYPE_MOVE_SEND)), + request->header.msgh_bits); + EXPECT_EQ(sizeof(RequestMessage), request->header.msgh_size); + if (options_.client_reply_port_type == Options::kReplyPortNormal) { + EXPECT_EQ(RemotePort(), request->header.msgh_remote_port); + } + EXPECT_EQ(LocalPort(), request->header.msgh_local_port); + EXPECT_EQ(kRequestMessageId, request->header.msgh_id); + EXPECT_EQ(0, memcmp(&request->ndr, &NDR_record, sizeof(NDR_record))); + EXPECT_EQ(requests_, request->number); + EXPECT_EQ(static_cast(MACH_MSG_TRAILER_FORMAT_0), + request->trailer.msgh_trailer_type); + EXPECT_EQ(MACH_MSG_TRAILER_MINIMUM_SIZE, + request->trailer.msgh_trailer_size); + + ++requests_; + + ReplyMessage* reply = reinterpret_cast(out); + reply->Head.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0); + reply->Head.msgh_size = sizeof(*reply); + reply->Head.msgh_remote_port = request->header.msgh_remote_port; + reply->Head.msgh_local_port = MACH_PORT_NULL; + reply->Head.msgh_id = kReplyMessageId; + reply->NDR = NDR_record; + reply->RetCode = options_.server_mig_retcode; + reply->number = replies_++; + + return true; + } + + virtual mach_msg_size_t MachMessageServerRequestSize() override { + return sizeof(RequestMessage); + } + + virtual mach_msg_size_t MachMessageServerReplySize() override { + return sizeof(ReplyMessage); + } + + private: + struct RequestMessage { + mach_msg_header_t header; + NDR_record_t ndr; + uint32_t number; + }; + + struct ReplyMessage : public mig_reply_error_t { + uint32_t number; + }; + + // MachMultiprocess: + + virtual void MachMultiprocessParent() override { + if (options_.parent_wait_for_child_pipe) { + // Wait until the child is done sending what it’s going to send. + char c; + ssize_t rv = ReadFD(ReadPipeFD(), &c, 1); + EXPECT_EQ(1, rv) << ErrnoMessage("read"); + EXPECT_EQ('\0', c); + } + + kern_return_t kr; + ASSERT_EQ(options_.expect_server_result, + (kr = MachMessageServer::Run(this, + LocalPort(), + MACH_MSG_OPTION_NONE, + options_.server_persistent, + options_.server_nonblocking, + options_.server_timeout_ms))) + << MachErrorMessage(kr, "MachMessageServer"); + } + + virtual void MachMultiprocessChild() override { + for (size_t index = 0; + index < options_.client_send_request_count; + ++index) { + if (options_.child_send_all_requests_before_receiving_any_replies) { + // For this test, all of the messages need to go into the queue before + // the parent is allowed to start processing them. Don’t attempt to + // process replies before all of the requests are sent, because the + // server won’t have sent any replies until all of the requests are in + // its queue. + ChildSendRequest(); + } else { + ChildSendRequestAndWaitForReply(); + } + if (testing::Test::HasFatalFailure()) { + return; + } + } + + if (options_.parent_wait_for_child_pipe && + options_.child_send_all_requests_before_receiving_any_replies) { + // Now that all of the requests have been sent, let the parent know that + // it’s safe to begin processing them, and then wait for the replies. + ChildNotifyParentViaPipe(); + if (testing::Test::HasFatalFailure()) { + return; + } + + for (size_t index = 0; + index < options_.client_send_request_count; + ++index) { + ChildWaitForReply(); + if (testing::Test::HasFatalFailure()) { + return; + } + } + } + } + + // In the child process, sends a request message to the server. + void ChildSendRequest() { + // local_receive_port_owner will the receive right that is created in this + // scope and intended to be destroyed when leaving this scope, after it has + // been carried in a Mach message. + base::mac::ScopedMachReceiveRight local_receive_port_owner; + + RequestMessage request = {}; + request.header.msgh_bits = + MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MAKE_SEND); + request.header.msgh_size = sizeof(request); + request.header.msgh_remote_port = RemotePort(); + kern_return_t kr; + switch (options_.client_reply_port_type) { + case Options::kReplyPortNormal: + request.header.msgh_local_port = LocalPort(); + break; + case Options::kReplyPortNull: + request.header.msgh_local_port = MACH_PORT_NULL; + break; + case Options::kReplyPortDead: { + // Use a newly-allocated receive right that will be destroyed when this + // method returns. A send right will be made from this receive right and + // carried in the request message to the server. By the time the server + // looks at the right, it will have become a dead name. + kr = mach_port_allocate(mach_task_self(), + MACH_PORT_RIGHT_RECEIVE, + &request.header.msgh_local_port); + ASSERT_EQ(KERN_SUCCESS, kr) + << MachErrorMessage(kr, "mach_port_allocate"); + local_receive_port_owner.reset(request.header.msgh_local_port); + break; + } + } + request.header.msgh_id = kRequestMessageId; + request.number = requests_++; + request.ndr = NDR_record; + + kr = mach_msg(&request.header, + MACH_SEND_MSG | MACH_SEND_TIMEOUT, + request.header.msgh_size, + 0, + MACH_PORT_NULL, + MACH_MSG_TIMEOUT_NONE, + MACH_PORT_NULL); + ASSERT_EQ(MACH_MSG_SUCCESS, kr) << MachErrorMessage(kr, "mach_msg"); + } + + // In the child process, waits for a reply message from the server. + void ChildWaitForReply() { + if (options_.client_reply_port_type != Options::kReplyPortNormal) { + // The client shouldn’t expect a reply when it didn’t send a good reply + // port with its request. + return; + } + + struct ReceiveReplyMessage : public ReplyMessage { + mach_msg_trailer_t trailer; + }; + + ReceiveReplyMessage reply = {}; + kern_return_t kr = mach_msg(&reply.Head, + MACH_RCV_MSG, + 0, + sizeof(reply), + LocalPort(), + MACH_MSG_TIMEOUT_NONE, + MACH_PORT_NULL); + ASSERT_EQ(MACH_MSG_SUCCESS, kr) << MachErrorMessage(kr, "mach_msg"); + + ASSERT_EQ(static_cast( + MACH_MSGH_BITS(0, MACH_MSG_TYPE_MOVE_SEND)), reply.Head.msgh_bits); + ASSERT_EQ(sizeof(ReplyMessage), reply.Head.msgh_size); + ASSERT_EQ(static_cast(MACH_PORT_NULL), + reply.Head.msgh_remote_port); + ASSERT_EQ(LocalPort(), reply.Head.msgh_local_port); + ASSERT_EQ(kReplyMessageId, reply.Head.msgh_id); + ASSERT_EQ(0, memcmp(&reply.NDR, &NDR_record, sizeof(NDR_record))); + ASSERT_EQ(options_.server_mig_retcode, reply.RetCode); + ASSERT_EQ(replies_, reply.number); + ASSERT_EQ(static_cast(MACH_MSG_TRAILER_FORMAT_0), + reply.trailer.msgh_trailer_type); + ASSERT_EQ(MACH_MSG_TRAILER_MINIMUM_SIZE, reply.trailer.msgh_trailer_size); + + ++replies_; + } + + // For test types where the child needs to notify the server in the parent + // that the child is ready, this method will send a byte via the POSIX pipe. + // The parent will be waiting in a read() on this pipe, and will proceed to + // running MachMessageServer() once it’s received. + void ChildNotifyParentViaPipe() { + char c = '\0'; + ssize_t rv = WriteFD(WritePipeFD(), &c, 1); + ASSERT_EQ(1, rv) << ErrnoMessage("write"); + } + + // In the child process, sends a request message to the server and then + // receives a reply message. + void ChildSendRequestAndWaitForReply() { + ChildSendRequest(); + if (testing::Test::HasFatalFailure()) { + return; + } + + if (options_.parent_wait_for_child_pipe && + !options_.child_send_all_requests_before_receiving_any_replies) { + // The parent is waiting to read a byte to indicate that the message has + // been placed in the queue. + ChildNotifyParentViaPipe(); + if (testing::Test::HasFatalFailure()) { + return; + } + } + + ChildWaitForReply(); + } + + const Options& options_; + + static uint32_t requests_; + static uint32_t replies_; + + static const mach_msg_id_t kRequestMessageId = 16237; + static const mach_msg_id_t kReplyMessageId = kRequestMessageId + 100; + + DISALLOW_COPY_AND_ASSIGN(TestMachMessageServer); +}; + +uint32_t TestMachMessageServer::requests_; +uint32_t TestMachMessageServer::replies_; +const mach_msg_id_t TestMachMessageServer::kRequestMessageId; +const mach_msg_id_t TestMachMessageServer::kReplyMessageId; + +TEST(MachMessageServer, Basic) { + // The client sends one message to the server, which will wait indefinitely in + // blocking mode for it. + TestMachMessageServer::Options options; + TestMachMessageServer test_mach_message_server(options); + test_mach_message_server.Test(); +} + +TEST(MachMessageServer, NonblockingNoMessage) { + // The server waits in nonblocking mode and the client sends nothing, so the + // server should return immediately without processing any message. + TestMachMessageServer::Options options; + options.expect_server_interface_method_called = false; + options.server_nonblocking = MachMessageServer::kNonblocking; + options.expect_server_result = MACH_RCV_TIMED_OUT; + options.client_send_request_count = 0; + TestMachMessageServer test_mach_message_server(options); + test_mach_message_server.Test(); +} + +TEST(MachMessageServer, TimeoutNoMessage) { + // The server waits in blocking mode for one message, but with a timeout. The + // client sends no message, so the server returns after the timeout. + TestMachMessageServer::Options options; + options.expect_server_interface_method_called = false; + options.server_timeout_ms = 10; + options.expect_server_result = MACH_RCV_TIMED_OUT; + options.client_send_request_count = 0; + TestMachMessageServer test_mach_message_server(options); + test_mach_message_server.Test(); +} + +TEST(MachMessageServer, Nonblocking) { + // The client sends one message to the server and then signals the server that + // it’s safe to start waiting for it in nonblocking mode. The message is in + // the server’s queue, so it’s able to receive it when it begins listening in + // nonblocking mode. + TestMachMessageServer::Options options; + options.parent_wait_for_child_pipe = true; + options.server_nonblocking = MachMessageServer::kNonblocking; + TestMachMessageServer test_mach_message_server(options); + test_mach_message_server.Test(); +} + +TEST(MachMessageServer, Timeout) { + // The client sends one message to the server, which will wait in blocking + // mode for it up to a specific timeout. + TestMachMessageServer::Options options; + options.server_timeout_ms = 10; + TestMachMessageServer test_mach_message_server(options); + test_mach_message_server.Test(); +} + +TEST(MachMessageServer, PersistentTenMessages) { + // The server waits for as many messages as it can receive in blocking mode + // with a timeout. The client sends several messages, and the server processes + // them all. + TestMachMessageServer::Options options; + options.server_persistent = MachMessageServer::kPersistent; + options.server_timeout_ms = 10; + options.expect_server_result = MACH_RCV_TIMED_OUT; + options.client_send_request_count = 10; + TestMachMessageServer test_mach_message_server(options); + test_mach_message_server.Test(); +} + +TEST(MachMessageServer, PersistentNonblockingFourMessages) { + // The client sends several messages to the server and then signals the server + // that it’s safe to start waiting for them in nonblocking mode. The server + // then listens for them in nonblocking persistent mode, and receives all of + // them because they’ve been queued up. The client doesn’t wait for the + // replies until after it’s put all of its requests into the server’s queue. + // + // This test is sensitive to the length of the IPC queue limit. Mach ports + // normally have a queue length limit of MACH_PORT_QLIMIT_DEFAULT (which is + // MACH_PORT_QLIMIT_BASIC, or 5). The number of messages sent for this test + // must be below this, because the server does not begin dequeueing request + // messages until the client has finished sending them. + TestMachMessageServer::Options options; + options.parent_wait_for_child_pipe = true; + options.server_persistent = MachMessageServer::kPersistent; + options.server_nonblocking = MachMessageServer::kNonblocking; + options.expect_server_result = MACH_RCV_TIMED_OUT; + options.client_send_request_count = 4; + options.child_send_all_requests_before_receiving_any_replies = true; + TestMachMessageServer test_mach_message_server(options); + test_mach_message_server.Test(); +} + +TEST(MachMessageServer, ReturnCodeInvalidArgument) { + // This tests that the mig_reply_error_t::RetCode field is properly returned + // to the client. + TestMachMessageServer::Options options; + TestMachMessageServer test_mach_message_server(options); + options.server_mig_retcode = KERN_INVALID_ARGUMENT; + test_mach_message_server.Test(); +} + +TEST(MachMessageServer, ReplyPortNull) { + // The client sets its reply port to MACH_PORT_NULL. The server should see + // this and avoid sending a message to the null port. No reply message is + // sent and the server returns success. + TestMachMessageServer::Options options; + TestMachMessageServer test_mach_message_server(options); + options.client_reply_port_type = + TestMachMessageServer::Options::kReplyPortNull; + test_mach_message_server.Test(); +} + +TEST(MachMessageServer, ReplyPortDead) { + // The client allocates a new port and uses it as the reply port in its + // request message, and then deallocates its receive right to that port. It + // then signals the server to process the request message. The server’s view + // of the port is that it is a dead name. The server function will return + // MACH_SEND_INVALID_DEST because it’s not possible to send a message to a + // dead name. + TestMachMessageServer::Options options; + TestMachMessageServer test_mach_message_server(options); + options.parent_wait_for_child_pipe = true; + options.expect_server_result = MACH_SEND_INVALID_DEST; + options.client_reply_port_type = + TestMachMessageServer::Options::kReplyPortDead; + test_mach_message_server.Test(); +} + +} // namespace diff --git a/util/util.gyp b/util/util.gyp index aa906000..b38e1fb7 100644 --- a/util/util.gyp +++ b/util/util.gyp @@ -61,6 +61,8 @@ 'mac/process_types/traits.h', 'mach/bootstrap.cc', 'mach/bootstrap.h', + 'mach/mach_message_server.cc', + 'mach/mach_message_server.h', 'mach/task_memory.cc', 'mach/task_memory.h', 'misc/initialization_state.h', @@ -181,6 +183,7 @@ 'mac/process_types_test.cc', 'mac/service_management_test.mm', 'mach/bootstrap_test.cc', + 'mach/mach_message_server_test.cc', 'mach/task_memory_test.cc', 'misc/initialization_state_dcheck_test.cc', 'misc/initialization_state_test.cc',