Add MachMultiprocess and its test.

TEST=util_test MachMultiprocess.MachMultiprocess
R=rsesek@chromium.org

Review URL: https://codereview.chromium.org/482483002
This commit is contained in:
Mark Mentovai 2014-08-20 13:58:42 -04:00
parent 280c4345c5
commit bbe4d85935
4 changed files with 565 additions and 0 deletions

View File

@ -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 <AvailabilityMacros.h>
#include <bsm/libbsm.h>
#include <servers/bootstrap.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string>
#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<internal::MachMultiprocessInfo> info(
new internal::MachMultiprocessInfo);
base::AutoReset<internal::MachMultiprocessInfo*> 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 its 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 childs perspective. The childs side of the pipe will
// be broken, the childs remote port will become a dead name, and an
// attempt by the child to look up the service will fail. If this werent
// 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_t>(MACH_PORT_NULL), info_->local_port);
return info_->local_port;
}
mach_port_t MachMultiprocess::RemotePort() const {
EXPECT_NE(static_cast<mach_port_t>(MACH_PORT_NULL), info_->remote_port);
return info_->remote_port;
}
mach_port_t MachMultiprocess::ChildTask() const {
EXPECT_NE(static_cast<mach_port_t>(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_name_t>(MACH_MSG_TYPE_MOVE_SEND),
message.port_descriptor.disposition);
ASSERT_EQ(static_cast<mach_msg_descriptor_type_t>(MACH_MSG_PORT_DESCRIPTOR),
message.port_descriptor.type);
ASSERT_EQ(static_cast<mach_msg_trailer_type_t>(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 trailers 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 childs 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 childs 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 tasks local port receive right. It will also carry a
// send right to the child tasks 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

View File

@ -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 <mach/mach.h>
#include <unistd.h>
#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 pipes 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 childs 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 parents 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 childs 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_

View File

@ -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

View File

@ -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',
],
},
],