diff --git a/util/test/mac/mach_multiprocess.cc b/util/test/mac/mach_multiprocess.cc new file mode 100644 index 00000000..9d634f91 --- /dev/null +++ b/util/test/mac/mach_multiprocess.cc @@ -0,0 +1,356 @@ +// 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/test/mac/mach_multiprocess.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "base/auto_reset.h" +#include "base/files/scoped_file.h" +#include "base/logging.h" +#include "base/mac/scoped_mach_port.h" +#include "base/memory/scoped_ptr.h" +#include "base/rand_util.h" +#include "base/strings/stringprintf.h" +#include "gtest/gtest.h" +#include "util/mach/bootstrap.h" +#include "util/test/errors.h" +#include "util/test/mac/mach_errors.h" + +namespace { + +class ScopedNotReached { + public: + ScopedNotReached() {} + ~ScopedNotReached() { abort(); } + + private: + DISALLOW_COPY_AND_ASSIGN(ScopedNotReached); +}; + +// The “hello” message contains a send right to the child process’ task port. +struct SendHelloMessage : public mach_msg_base_t { + mach_msg_port_descriptor_t port_descriptor; +}; + +struct ReceiveHelloMessage : public SendHelloMessage { + mach_msg_audit_trailer_t audit_trailer; +}; + +} // namespace + +namespace crashpad { +namespace test { + +using namespace testing; + +namespace internal { + +struct MachMultiprocessInfo { + MachMultiprocessInfo() + : service_name(), + read_pipe(-1), + write_pipe(-1), + child_pid(0), + pipe_fd(-1), + local_port(MACH_PORT_NULL), + remote_port(MACH_PORT_NULL), + child_task(MACH_PORT_NULL) {} + + std::string service_name; + base::ScopedFD read_pipe; + base::ScopedFD write_pipe; + pid_t child_pid; // valid only in parent + int pipe_fd; // read_pipe in parent, write_pipe in child + base::mac::ScopedMachReceiveRight local_port; + base::mac::ScopedMachSendRight remote_port; + base::mac::ScopedMachSendRight child_task; // valid only in parent +}; + +} // namespace internal + +MachMultiprocess::MachMultiprocess() : info_(NULL) { +} + +void MachMultiprocess::Run() { + ASSERT_EQ(NULL, info_); + scoped_ptr info( + new internal::MachMultiprocessInfo); + base::AutoReset reset_info(&info_, + info.get()); + + int pipe_fds[2]; + int rv = pipe(pipe_fds); + ASSERT_EQ(0, rv) << ErrnoMessage("pipe"); + + info_->read_pipe.reset(pipe_fds[0]); + info_->write_pipe.reset(pipe_fds[1]); + + // Set up the parent port and register it with the bootstrap server before + // forking, so that it’s guaranteed to be there when the child attempts to + // look it up. + info_->service_name = "com.googlecode.crashpad.test.mach_multiprocess."; + for (int index = 0; index < 16; ++index) { + info_->service_name.append(1, base::RandInt('A', 'Z')); + } + + mach_port_t local_port; + kern_return_t kr = + BootstrapCheckIn(bootstrap_port, info_->service_name, &local_port); + ASSERT_EQ(BOOTSTRAP_SUCCESS, kr) + << BootstrapErrorMessage(kr, "bootstrap_check_in"); + info_->local_port.reset(local_port); + + pid_t pid = fork(); + ASSERT_GE(pid, 0) << ErrnoMessage("fork"); + + if (pid > 0) { + info_->child_pid = pid; + + RunParent(); + + // Waiting for the child happens here instead of in RunParent() because even + // if RunParent() returns early due to a gtest fatal assertion failure, the + // child should still be reaped. + + // This will make the parent hang up on the child as much as would be + // visible from the child’s perspective. The child’s side of the pipe will + // be broken, the child’s remote port will become a dead name, and an + // attempt by the child to look up the service will fail. If this weren’t + // done, the child might hang while waiting for a parent that has already + // triggered a fatal assertion failure to do something. + info.reset(); + info_ = NULL; + + int status; + pid_t wait_pid = waitpid(pid, &status, 0); + ASSERT_EQ(pid, wait_pid) << ErrnoMessage("waitpid"); + if (status != 0) { + std::string message; + if (WIFEXITED(status)) { + message = base::StringPrintf("Child exited with code %d", + WEXITSTATUS(status)); + } else if (WIFSIGNALED(status)) { + message = base::StringPrintf("Child terminated by signal %d (%s) %s", + WTERMSIG(status), + strsignal(WTERMSIG(status)), + WCOREDUMP(status) ? " (core dumped)" : ""); + } + ASSERT_EQ(0, status) << message; + } + } else { + RunChild(); + } +} + +MachMultiprocess::~MachMultiprocess() { +} + +pid_t MachMultiprocess::ChildPID() const { + EXPECT_NE(0, info_->child_pid); + return info_->child_pid; +} + +int MachMultiprocess::PipeFD() const { + EXPECT_NE(-1, info_->pipe_fd); + return info_->pipe_fd; +} + +mach_port_t MachMultiprocess::LocalPort() const { + EXPECT_NE(static_cast(MACH_PORT_NULL), info_->local_port); + return info_->local_port; +} + +mach_port_t MachMultiprocess::RemotePort() const { + EXPECT_NE(static_cast(MACH_PORT_NULL), info_->remote_port); + return info_->remote_port; +} + +mach_port_t MachMultiprocess::ChildTask() const { + EXPECT_NE(static_cast(MACH_PORT_NULL), info_->child_task); + return info_->child_task; +} + +void MachMultiprocess::RunParent() { + // The parent uses the read end of the pipe. + info_->write_pipe.reset(); + info_->pipe_fd = info_->read_pipe.get(); + + ReceiveHelloMessage message = {}; + + kern_return_t kr = + mach_msg(&message.header, + MACH_RCV_MSG | MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0) | + MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AUDIT), + 0, + sizeof(message), + info_->local_port, + MACH_MSG_TIMEOUT_NONE, + MACH_PORT_NULL); + ASSERT_EQ(MACH_MSG_SUCCESS, kr) << MachErrorMessage(kr, "mach_msg"); + + // Comb through the entire message, checking every field against its expected + // value. + EXPECT_EQ(MACH_MSGH_BITS(MACH_MSG_TYPE_MOVE_SEND, MACH_MSG_TYPE_MOVE_SEND) | + MACH_MSGH_BITS_COMPLEX, + message.header.msgh_bits); + ASSERT_EQ(sizeof(SendHelloMessage), message.header.msgh_size); + EXPECT_EQ(info_->local_port, message.header.msgh_local_port); + ASSERT_EQ(1u, message.body.msgh_descriptor_count); + EXPECT_EQ(static_cast(MACH_MSG_TYPE_MOVE_SEND), + message.port_descriptor.disposition); + ASSERT_EQ(static_cast(MACH_MSG_PORT_DESCRIPTOR), + message.port_descriptor.type); + ASSERT_EQ(static_cast(MACH_MSG_TRAILER_FORMAT_0), + message.audit_trailer.msgh_trailer_type); + ASSERT_EQ(sizeof(message.audit_trailer), + message.audit_trailer.msgh_trailer_size); + EXPECT_EQ(0u, message.audit_trailer.msgh_seqno); + + // Check the audit trailer’s values for sanity. This is a little bit of + // overkill, but because the service was registered with the bootstrap server + // and other processes will be able to look it up and send messages to it, + // these checks disambiguate genuine failures later on in the test from those + // that would occur if an errant process sends a message to this service. +#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_8 + uid_t audit_auid; + uid_t audit_euid; + gid_t audit_egid; + uid_t audit_ruid; + gid_t audit_rgid; + pid_t audit_pid; + au_asid_t audit_asid; + audit_token_to_au32(message.audit_trailer.msgh_audit, + &audit_auid, + &audit_euid, + &audit_egid, + &audit_ruid, + &audit_rgid, + &audit_pid, + &audit_asid, + NULL); +#else + uid_t audit_auid = audit_token_to_auid(message.audit_trailer.msgh_audit); + uid_t audit_euid = audit_token_to_euid(message.audit_trailer.msgh_audit); + gid_t audit_egid = audit_token_to_egid(message.audit_trailer.msgh_audit); + uid_t audit_ruid = audit_token_to_ruid(message.audit_trailer.msgh_audit); + gid_t audit_rgid = audit_token_to_rgid(message.audit_trailer.msgh_audit); + pid_t audit_pid = audit_token_to_pid(message.audit_trailer.msgh_audit); + au_asid_t audit_asid = audit_token_to_asid(message.audit_trailer.msgh_audit); +#endif + EXPECT_EQ(geteuid(), audit_euid); + EXPECT_EQ(getegid(), audit_egid); + EXPECT_EQ(getuid(), audit_ruid); + EXPECT_EQ(getgid(), audit_rgid); + ASSERT_EQ(ChildPID(), audit_pid); + + auditinfo_addr_t audit_info; + int rv = getaudit_addr(&audit_info, sizeof(audit_info)); + ASSERT_EQ(0, rv) << ErrnoMessage("getaudit_addr"); + EXPECT_EQ(audit_info.ai_auid, audit_auid); + EXPECT_EQ(audit_info.ai_asid, audit_asid); + + // Retrieve the remote port from the message header, and the child’s task port + // from the message body. + info_->remote_port.reset(message.header.msgh_remote_port); + info_->child_task.reset(message.port_descriptor.name); + + // Verify that the child’s task port is what it purports to be. + int mach_pid; + kr = pid_for_task(info_->child_task, &mach_pid); + ASSERT_EQ(KERN_SUCCESS, kr) << MachErrorMessage(kr, "pid_for_task"); + ASSERT_EQ(ChildPID(), mach_pid); + + Parent(); + + info_->remote_port.reset(); + info_->local_port.reset(); + + info_->pipe_fd = -1; + info_->read_pipe.reset(); +} + +void MachMultiprocess::RunChild() { + ScopedNotReached must_not_leave_this_scope; + + // local_port is not valid in the forked child process. + ignore_result(info_->local_port.release()); + + // The child uses the write end of the pipe. + info_->read_pipe.reset(); + info_->pipe_fd = info_->write_pipe.get(); + + mach_port_t local_port; + kern_return_t kr = mach_port_allocate( + mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &local_port); + ASSERT_EQ(KERN_SUCCESS, kr) << MachErrorMessage(kr, "mach_port_allocate"); + info_->local_port.reset(local_port); + + // The remote port can be obtained from the bootstrap server. + mach_port_t remote_port; + kr = bootstrap_look_up( + bootstrap_port, info_->service_name.c_str(), &remote_port); + ASSERT_EQ(BOOTSTRAP_SUCCESS, kr) + << BootstrapErrorMessage(kr, "bootstrap_look_up"); + info_->remote_port.reset(remote_port); + + // The “hello” message will provide the parent with its remote port, a send + // right to the child task’s local port receive right. It will also carry a + // send right to the child task’s task port. + SendHelloMessage message = {}; + message.header.msgh_bits = + MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MAKE_SEND) | + MACH_MSGH_BITS_COMPLEX; + message.header.msgh_size = sizeof(message); + message.header.msgh_remote_port = info_->remote_port; + message.header.msgh_local_port = info_->local_port; + message.body.msgh_descriptor_count = 1; + message.port_descriptor.name = mach_task_self(); + message.port_descriptor.disposition = MACH_MSG_TYPE_COPY_SEND; + message.port_descriptor.type = MACH_MSG_PORT_DESCRIPTOR; + + kr = mach_msg(&message.header, + MACH_SEND_MSG, + message.header.msgh_size, + 0, + MACH_PORT_NULL, + MACH_MSG_TIMEOUT_NONE, + MACH_PORT_NULL); + ASSERT_EQ(MACH_MSG_SUCCESS, kr) << MachErrorMessage(kr, "mach_msg"); + + Child(); + + info_->remote_port.reset(); + info_->local_port.reset(); + + info_->pipe_fd = -1; + info_->write_pipe.reset(); + + if (Test::HasFailure()) { + // Trigger the ScopedNotReached destructor. + return; + } + + exit(0); +} + +} // namespace test +} // namespace crashpad diff --git a/util/test/mac/mach_multiprocess.h b/util/test/mac/mach_multiprocess.h new file mode 100644 index 00000000..666b128f --- /dev/null +++ b/util/test/mac/mach_multiprocess.h @@ -0,0 +1,133 @@ +// 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_TEST_MAC_MACH_MULTIPROCESS_H_ +#define CRASHPAD_UTIL_TEST_MAC_MACH_MULTIPROCESS_H_ + +#include +#include + +#include "base/basictypes.h" + +namespace crashpad { +namespace test { + +namespace internal { +struct MachMultiprocessInfo; +} // namespace internal + +//! \brief Manages a Mach-aware multiprocess test. +//! +//! These tests are `fork()`-based. The parent process has access to the child +//! process’ task port. The parent and child processes are able to communicate +//! via Mach IPC, and the child process can also send messages to the parent +//! process via a POSIX pipe. +//! +//! Subclasses are expected to implement the parent and child by overriding the +//! appropriate methods. +class MachMultiprocess { + public: + MachMultiprocess(); + + //! \brief Runs the test. + //! + //! This method establishes the proper testing environment and calls + //! RunParent() in the parent process and RunChild() in the child process. + //! + //! This method uses gtest assertions to validate the testing environment. If + //! the testing environment cannot be set up properly, it is possible that + //! Parent() or Child() will not be called. In the parent process, this method + //! also waits for the child process to exit after Parent() returns, and + //! verifies that it exited cleanly with gtest assertions. + void Run(); + + protected: + ~MachMultiprocess(); + + //! \brief The subclass-provided parent routine. + //! + //! Test failures should be reported via gtest: `EXPECT_*()`, `ASSERT_*()`, + //! `FAIL()`, etc. + //! + //! This method must not use a `wait()`-family system call to wait for the + //! child process to exit, as this is handled by RunParent(). + //! + //! Subclasses must implement this method to define how the parent operates. + virtual void Parent() = 0; + + //! \brief The subclass-provided child routine. + //! + //! Test failures should be reported via gtest: `EXPECT_*()`, `ASSERT_*()`, + //! `FAIL()`, etc. + //! + //! Subclasses must implement this method to define how the child operates. + virtual void Child() = 0; + + //! \brief Returns the child process’ process ID. + //! + //! This method may only be called by the parent process. + pid_t ChildPID() const; + + //! \brief Returns the pipe’s file descriptor. + //! + //! This method may be called by either the parent or the child process. In + //! the parent process, the pipe is read-only, and in the child process, it is + //! write-only. + int PipeFD() const; + + //! \brief Returns a receive right for the local port. + //! + //! This method may be called by either the parent or the child process. It + //! returns a receive right, with a corresponding send right held in the + //! opposing process. + mach_port_t LocalPort() const; + + //! \brief Returns a send right for the remote port. + //! + //! This method may be called by either the parent or the child process. It + //! returns a send right, with the corresponding receive right held in the + //! opposing process. + mach_port_t RemotePort() const; + + //! \brief Returns a send right for the child’s task port. + //! + //! This method may only be called by the parent process. + mach_port_t ChildTask() const; + + private: + //! \brief Runs the parent side of the test. + //! + //! This method establishes the parent’s environment, performs the handshake + //! with the child, calls Parent(), and waits for the child process to exit. + //! Assuming that the environment can be set up correctly and the child exits + //! successfully, the test will pass. + void RunParent(); + + //! \brief Runs the child side of the test. + //! + //! This method establishes the child’s environment, performs the handshake + //! with the parent, calls Child(), and exits cleanly. However, if any failure + //! (via fatal or nonfatal gtest assertion) is detected, the child will exit + //! with a failure status. + void RunChild(); + + internal::MachMultiprocessInfo* info_; + + DISALLOW_COPY_AND_ASSIGN(MachMultiprocess); +}; + +} // namespace test +} // namespace crashpad + +#endif // CRASHPAD_UTIL_TEST_MAC_MACH_MULTIPROCESS_H_ diff --git a/util/test/mac/mach_multiprocess_test.cc b/util/test/mac/mach_multiprocess_test.cc new file mode 100644 index 00000000..b0429cdb --- /dev/null +++ b/util/test/mac/mach_multiprocess_test.cc @@ -0,0 +1,67 @@ +// 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/test/mac/mach_multiprocess.h" + +#include "base/basictypes.h" +#include "gtest/gtest.h" +#include "util/file/fd_io.h" +#include "util/test/errors.h" + +namespace { + +using namespace crashpad; +using namespace crashpad::test; + +class TestMachMultiprocess final : public MachMultiprocess { + public: + TestMachMultiprocess() : MachMultiprocess() {} + + ~TestMachMultiprocess() {} + + protected: + // The base class will have already exercised the Mach ports for IPC and the + // child task port. Just make sure that the pipe is set up correctly. + virtual void Parent() override { + int fd = PipeFD(); + + char c; + ssize_t rv = ReadFD(fd, &c, 1); + ASSERT_EQ(1, rv) << ErrnoMessage("read"); + EXPECT_EQ('M', c); + + // The child will close its end of the pipe and exit. Make sure that the + // parent sees EOF. + rv = ReadFD(fd, &c, 1); + ASSERT_EQ(0, rv) << ErrnoMessage("read"); + } + + virtual void Child() override { + int fd = PipeFD(); + + char c = 'M'; + ssize_t rv = WriteFD(fd, &c, 1); + ASSERT_EQ(1, rv) << ErrnoMessage("write"); + } + + private: + DISALLOW_COPY_AND_ASSIGN(TestMachMultiprocess); +}; + +TEST(MachMultiprocess, MachMultiprocess) { + TestMachMultiprocess mach_multiprocess; + mach_multiprocess.Run(); +} + +} // namespace diff --git a/util/util.gyp b/util/util.gyp index 57243e44..d385da39 100644 --- a/util/util.gyp +++ b/util/util.gyp @@ -63,17 +63,25 @@ 'type': 'static_library', 'dependencies': [ '../compat/compat.gyp:compat', + '../third_party/gtest/gtest.gyp:gtest', '../third_party/mini_chromium/mini_chromium/base/base.gyp:base', 'util', ], 'include_dirs': [ '..', ], + 'link_settings': { + 'libraries': [ + '$(SDKROOT)/usr/lib/libbsm.dylib', + ], + }, 'sources': [ 'test/errors.cc', 'test/errors.h', 'test/mac/mach_errors.cc', 'test/mac/mach_errors.h', + 'test/mac/mach_multiprocess.cc', + 'test/mac/mach_multiprocess.h', ], }, { @@ -104,6 +112,7 @@ 'numeric/in_range_cast_test.cc', 'posix/process_util_test.cc', 'stdlib/strlcpy_test.cc', + 'test/mac/mach_multiprocess_test.cc', ], }, ],