diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /ipc/chromium/src/chrome/common/ipc_channel_posix.cc | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ipc/chromium/src/chrome/common/ipc_channel_posix.cc')
-rw-r--r-- | ipc/chromium/src/chrome/common/ipc_channel_posix.cc | 1317 |
1 files changed, 1317 insertions, 0 deletions
diff --git a/ipc/chromium/src/chrome/common/ipc_channel_posix.cc b/ipc/chromium/src/chrome/common/ipc_channel_posix.cc new file mode 100644 index 0000000000..8b6fc3349a --- /dev/null +++ b/ipc/chromium/src/chrome/common/ipc_channel_posix.cc @@ -0,0 +1,1317 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +// Copyright (c) 2008 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/common/ipc_channel_posix.h" + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include "mozilla/Mutex.h" +#if defined(OS_MACOSX) +# include <mach/message.h> +# include <mach/port.h> +# include "mozilla/UniquePtrExtensions.h" +# include "chrome/common/mach_ipc_mac.h" +#endif +#if defined(OS_MACOSX) || defined(OS_NETBSD) +# include <sched.h> +#endif +#include <stddef.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/un.h> +#include <sys/uio.h> + +#include <string> +#include <map> + +#include "base/command_line.h" +#include "base/eintr_wrapper.h" +#include "base/logging.h" +#include "base/process_util.h" +#include "base/string_util.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/common/ipc_channel_utils.h" +#include "chrome/common/ipc_message_utils.h" +#include "mozilla/ipc/Endpoint.h" +#include "mozilla/ipc/ProtocolUtils.h" +#include "mozilla/Atomics.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" + +// Use OS specific iovec array limit where it's possible. +#if defined(IOV_MAX) +static const size_t kMaxIOVecSize = IOV_MAX; +#elif defined(ANDROID) +static const size_t kMaxIOVecSize = 256; +#else +static const size_t kMaxIOVecSize = 16; +#endif + +using namespace mozilla::ipc; + +namespace IPC { + +// IPC channels on Windows use named pipes (CreateNamedPipe()) with +// channel ids as the pipe names. Channels on POSIX use anonymous +// Unix domain sockets created via socketpair() as pipes. These don't +// quite line up. +// +// When creating a child subprocess, the parent side of the fork +// arranges it such that the initial control channel ends up on the +// magic file descriptor gClientChannelFd in the child. Future +// connections (file descriptors) can then be passed via that +// connection via sendmsg(). +// +// On Android, child processes are created as a service instead of +// forking the parent process. The Android Binder service is used to +// transport the IPC channel file descriptor to the child process. +// So rather than re-mapping the file descriptor to a known value, +// the received channel file descriptor is set by calling +// SetClientChannelFd before gecko has been initialized and started +// in the child process. + +//------------------------------------------------------------------------------ +namespace { + +// This is the file descriptor number that a client process expects to find its +// IPC socket. +static int gClientChannelFd = +#if defined(MOZ_WIDGET_ANDROID) + // On android the fd is set at the time of child creation. + -1 +#else + 3 +#endif // defined(MOZ_WIDGET_ANDROID) + ; + +//------------------------------------------------------------------------------ + +bool ErrorIsBrokenPipe(int err) { return err == EPIPE || err == ECONNRESET; } + +// Some Android ARM64 devices appear to have a bug where sendmsg +// sometimes returns 0xFFFFFFFF, which we're assuming is a -1 that was +// incorrectly truncated to 32-bit and then zero-extended. +// See bug 1660826 for details. +// +// This is a workaround to detect that value and replace it with -1 +// (and check that there really was an error), because the largest +// amount we'll ever write is Channel::kMaximumMessageSize (256MiB). +// +// The workaround is also enabled on x86_64 Android on debug builds, +// although the bug isn't known to manifest there, so that there will +// be some CI coverage of this code. + +static inline ssize_t corrected_sendmsg(int socket, + const struct msghdr* message, + int flags) { +#if defined(ANDROID) && \ + (defined(__aarch64__) || (defined(DEBUG) && defined(__x86_64__))) + static constexpr auto kBadValue = static_cast<ssize_t>(0xFFFFFFFF); + static_assert(kBadValue > 0); + +# ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + errno = 0; +# endif + ssize_t bytes_written = sendmsg(socket, message, flags); + if (bytes_written == kBadValue) { + MOZ_DIAGNOSTIC_ASSERT(errno != 0); + bytes_written = -1; + } + MOZ_DIAGNOSTIC_ASSERT(bytes_written < kBadValue); + return bytes_written; +#else + return sendmsg(socket, message, flags); +#endif +} + +} // namespace +//------------------------------------------------------------------------------ + +#if defined(MOZ_WIDGET_ANDROID) +void Channel::SetClientChannelFd(int fd) { gClientChannelFd = fd; } +#endif // defined(MOZ_WIDGET_ANDROID) + +Channel::ChannelImpl::ChannelImpl(const ChannelId& channel_id, Mode mode, + Listener* listener) + : chan_cap_("ChannelImpl::SendMutex", + MessageLoopForIO::current()->SerialEventTarget()) { + Init(mode, listener); + + if (!CreatePipe(mode)) { + CHROMIUM_LOG(WARNING) << "Unable to create pipe in " + << (mode == MODE_SERVER ? "server" : "client") + << " mode error(" << strerror(errno) << ")."; + return; + } + + EnqueueHelloMessage(); +} + +Channel::ChannelImpl::ChannelImpl(ChannelHandle pipe, Mode mode, + Listener* listener) + : chan_cap_("ChannelImpl::SendMutex", + MessageLoopForIO::current()->SerialEventTarget()) { + Init(mode, listener); + SetPipe(pipe.release()); + + EnqueueHelloMessage(); +} + +void Channel::ChannelImpl::SetPipe(int fd) { + chan_cap_.NoteExclusiveAccess(); + + pipe_ = fd; + pipe_buf_len_ = 0; + if (fd >= 0) { + int buf_len; + socklen_t optlen = sizeof(buf_len); + if (getsockopt(fd, SOL_SOCKET, SO_SNDBUF, &buf_len, &optlen) != 0) { + CHROMIUM_LOG(WARNING) + << "Unable to determine pipe buffer size: " << strerror(errno); + return; + } + CHECK(optlen == sizeof(buf_len)); + CHECK(buf_len > 0); + pipe_buf_len_ = static_cast<unsigned>(buf_len); + } +} + +bool Channel::ChannelImpl::PipeBufHasSpaceAfter(size_t already_written) { + // If the OS didn't tell us the buffer size for some reason, then + // don't apply this limitation on the amount we try to write. + return pipe_buf_len_ == 0 || + static_cast<size_t>(pipe_buf_len_) > already_written; +} + +void Channel::ChannelImpl::Init(Mode mode, Listener* listener) { + // Verify that we fit in a "quantum-spaced" jemalloc bucket. + static_assert(sizeof(*this) <= 512, "Exceeded expected size class"); + + MOZ_RELEASE_ASSERT(kControlBufferHeaderSize >= CMSG_SPACE(0)); + MOZ_RELEASE_ASSERT(kControlBufferSize >= + CMSG_SPACE(sizeof(int) * kControlBufferMaxFds)); + + chan_cap_.NoteExclusiveAccess(); + + mode_ = mode; + is_blocked_on_write_ = false; + partial_write_.reset(); + input_buf_offset_ = 0; + input_buf_ = mozilla::MakeUnique<char[]>(Channel::kReadBufferSize); + input_cmsg_buf_ = mozilla::MakeUnique<char[]>(kControlBufferSize); + SetPipe(-1); + client_pipe_ = -1; + listener_ = listener; + waiting_connect_ = true; +#if defined(OS_MACOSX) + last_pending_fd_id_ = 0; + other_task_ = nullptr; +#endif +} + +bool Channel::ChannelImpl::CreatePipe(Mode mode) { + chan_cap_.NoteExclusiveAccess(); + + DCHECK(pipe_ == -1); + + if (mode == MODE_SERVER) { + ChannelHandle server, client; + if (!Channel::CreateRawPipe(&server, &client)) { + return false; + } + + SetPipe(server.release()); + client_pipe_ = client.release(); + } else { + static mozilla::Atomic<bool> consumed(false); + CHECK(!consumed.exchange(true)) + << "child process main channel can be created only once"; + SetPipe(gClientChannelFd); + } + + return true; +} + +bool Channel::ChannelImpl::EnqueueHelloMessage() { + mozilla::UniquePtr<Message> msg( + new Message(MSG_ROUTING_NONE, HELLO_MESSAGE_TYPE)); + if (!msg->WriteInt(base::GetCurrentProcId())) { + CloseLocked(); + return false; + } + + OutputQueuePush(std::move(msg)); + return true; +} + +bool Channel::ChannelImpl::Connect() { + IOThread().AssertOnCurrentThread(); + mozilla::MutexAutoLock lock(SendMutex()); + return ConnectLocked(); +} + +bool Channel::ChannelImpl::ConnectLocked() { + chan_cap_.NoteExclusiveAccess(); + + if (pipe_ == -1) { + return false; + } + +#if defined(OS_MACOSX) + // If we're still waiting for our peer task to be provided, don't start + // listening yet. We'll start receiving messages once the task_t is set. + if (accept_mach_ports_ && privileged_ && !other_task_) { + MOZ_ASSERT(waiting_connect_); + return true; + } +#endif + + MessageLoopForIO::current()->WatchFileDescriptor( + pipe_, true, MessageLoopForIO::WATCH_READ, &read_watcher_, this); + waiting_connect_ = false; + + return ProcessOutgoingMessages(); +} + +bool Channel::ChannelImpl::ProcessIncomingMessages() { + chan_cap_.NoteOnIOThread(); + + struct msghdr msg = {0}; + struct iovec iov; + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = input_cmsg_buf_.get(); + + for (;;) { + msg.msg_controllen = kControlBufferSize; + + if (pipe_ == -1) return false; + + // In some cases the beginning of a message will be stored in input_buf_. We + // don't want to overwrite that, so we store the new data after it. + iov.iov_base = input_buf_.get() + input_buf_offset_; + iov.iov_len = Channel::kReadBufferSize - input_buf_offset_; + + // Read from pipe. + // recvmsg() returns 0 if the connection has closed or EAGAIN if no data + // is waiting on the pipe. + ssize_t bytes_read = HANDLE_EINTR(recvmsg(pipe_, &msg, MSG_DONTWAIT)); + + if (bytes_read < 0) { + if (errno == EAGAIN) { + return true; + } else { + if (!ErrorIsBrokenPipe(errno)) { + CHROMIUM_LOG(ERROR) + << "pipe error (fd " << pipe_ << "): " << strerror(errno); + } + return false; + } + } else if (bytes_read == 0) { + // The pipe has closed... + Close(); + return false; + } + DCHECK(bytes_read); + + // a pointer to an array of |num_wire_fds| file descriptors from the read + const int* wire_fds = NULL; + unsigned num_wire_fds = 0; + + // walk the list of control messages and, if we find an array of file + // descriptors, save a pointer to the array + + // This next if statement is to work around an OSX issue where + // CMSG_FIRSTHDR will return non-NULL in the case that controllen == 0. + // Here's a test case: + // + // int main() { + // struct msghdr msg; + // msg.msg_control = &msg; + // msg.msg_controllen = 0; + // if (CMSG_FIRSTHDR(&msg)) + // printf("Bug found!\n"); + // } + if (msg.msg_controllen > 0) { + // On OSX, CMSG_FIRSTHDR doesn't handle the case where controllen is 0 + // and will return a pointer into nowhere. + for (struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msg); cmsg; + cmsg = CMSG_NXTHDR(&msg, cmsg)) { + if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) { + const unsigned payload_len = cmsg->cmsg_len - CMSG_LEN(0); + DCHECK(payload_len % sizeof(int) == 0); + wire_fds = reinterpret_cast<int*>(CMSG_DATA(cmsg)); + num_wire_fds = payload_len / 4; + + if (msg.msg_flags & MSG_CTRUNC) { + CHROMIUM_LOG(ERROR) + << "SCM_RIGHTS message was truncated" + << " cmsg_len:" << cmsg->cmsg_len << " fd:" << pipe_; + for (unsigned i = 0; i < num_wire_fds; ++i) + IGNORE_EINTR(close(wire_fds[i])); + return false; + } + break; + } + } + } + + // Process messages from input buffer. + const char* p = input_buf_.get(); + const char* end = input_buf_.get() + input_buf_offset_ + bytes_read; + + // A pointer to an array of |num_fds| file descriptors which includes any + // fds that have spilled over from a previous read. + const int* fds; + unsigned num_fds; + unsigned fds_i = 0; // the index of the first unused descriptor + + if (input_overflow_fds_.empty()) { + fds = wire_fds; + num_fds = num_wire_fds; + } else { + // This code may look like a no-op in the case where + // num_wire_fds == 0, but in fact: + // + // 1. wire_fds will be nullptr, so passing it to memcpy is + // undefined behavior according to the C standard, even though + // the memcpy length is 0. + // + // 2. prev_size will be an out-of-bounds index for + // input_overflow_fds_; this is undefined behavior according to + // the C++ standard, even though the element only has its + // pointer taken and isn't accessed (and the corresponding + // operation on a C array would be defined). + // + // UBSan makes #1 a fatal error, and assertions in libstdc++ do + // the same for #2 if enabled. + if (num_wire_fds > 0) { + const size_t prev_size = input_overflow_fds_.size(); + input_overflow_fds_.resize(prev_size + num_wire_fds); + memcpy(&input_overflow_fds_[prev_size], wire_fds, + num_wire_fds * sizeof(int)); + } + fds = &input_overflow_fds_[0]; + num_fds = input_overflow_fds_.size(); + } + + // The data for the message we're currently reading consists of any data + // stored in incoming_message_ followed by data in input_buf_ (followed by + // other messages). + + // NOTE: We re-check `pipe_` after each message to make sure we weren't + // closed while calling `OnMessageReceived` or `OnChannelConnected`. + while (p < end && pipe_ != -1) { + // Try to figure out how big the message is. Size is 0 if we haven't read + // enough of the header to know the size. + uint32_t message_length = 0; + if (incoming_message_) { + message_length = incoming_message_->size(); + } else { + message_length = Message::MessageSize(p, end); + } + + if (!message_length) { + // We haven't seen the full message header. + MOZ_ASSERT(!incoming_message_); + + // Move everything we have to the start of the buffer. We'll finish + // reading this message when we get more data. For now we leave it in + // input_buf_. + memmove(input_buf_.get(), p, end - p); + input_buf_offset_ = end - p; + + break; + } + + input_buf_offset_ = 0; + + bool partial; + if (incoming_message_) { + // We already have some data for this message stored in + // incoming_message_. We want to append the new data there. + Message& m = *incoming_message_; + + // How much data from this message remains to be added to + // incoming_message_? + MOZ_DIAGNOSTIC_ASSERT(message_length > m.CurrentSize()); + uint32_t remaining = message_length - m.CurrentSize(); + + // How much data from this message is stored in input_buf_? + uint32_t in_buf = std::min(remaining, uint32_t(end - p)); + + m.InputBytes(p, in_buf); + p += in_buf; + + // Are we done reading this message? + partial = in_buf != remaining; + } else { + // How much data from this message is stored in input_buf_? + uint32_t in_buf = std::min(message_length, uint32_t(end - p)); + + incoming_message_ = mozilla::MakeUnique<Message>(p, in_buf); + p += in_buf; + + // Are we done reading this message? + partial = in_buf != message_length; + } + + if (partial) { + break; + } + + Message& m = *incoming_message_; + + if (m.header()->num_handles) { + // the message has file descriptors + const char* error = NULL; + if (m.header()->num_handles > num_fds - fds_i) { + // the message has been completely received, but we didn't get + // enough file descriptors. + error = "Message needs unreceived descriptors"; + } + + if (m.header()->num_handles > + IPC::Message::MAX_DESCRIPTORS_PER_MESSAGE) { + // There are too many descriptors in this message + error = "Message requires an excessive number of descriptors"; + } + + if (error) { + CHROMIUM_LOG(WARNING) + << error << " channel:" << this << " message-type:" << m.type() + << " header()->num_handles:" << m.header()->num_handles + << " num_fds:" << num_fds << " fds_i:" << fds_i; + // close the existing file descriptors so that we don't leak them + for (unsigned i = fds_i; i < num_fds; ++i) + IGNORE_EINTR(close(fds[i])); + input_overflow_fds_.clear(); + // abort the connection + return false; + } + +#if defined(OS_MACOSX) + // Send a message to the other side, indicating that we are now + // responsible for closing the descriptor. + auto fdAck = mozilla::MakeUnique<Message>(MSG_ROUTING_NONE, + RECEIVED_FDS_MESSAGE_TYPE); + DCHECK(m.fd_cookie() != 0); + fdAck->set_fd_cookie(m.fd_cookie()); + { + mozilla::MutexAutoLock lock(SendMutex()); + OutputQueuePush(std::move(fdAck)); + } +#endif + + nsTArray<mozilla::UniqueFileHandle> handles(m.header()->num_handles); + for (unsigned end_i = fds_i + m.header()->num_handles; fds_i < end_i; + ++fds_i) { + handles.AppendElement(mozilla::UniqueFileHandle(fds[fds_i])); + } + m.SetAttachedFileHandles(std::move(handles)); + } + + // Note: We set other_pid_ below when we receive a Hello message (which + // has no routing ID), but we only emit a profiler marker for messages + // with a routing ID, so there's no conflict here. + AddIPCProfilerMarker(m, other_pid_, MessageDirection::eReceiving, + MessagePhase::TransferEnd); + +#ifdef IPC_MESSAGE_DEBUG_EXTRA + DLOG(INFO) << "received message on channel @" << this << " with type " + << m.type(); +#endif + + if (m.routing_id() == MSG_ROUTING_NONE && + m.type() == HELLO_MESSAGE_TYPE) { + // The Hello message contains only the process id. + int32_t other_pid = MessageIterator(m).NextInt(); + SetOtherPid(other_pid); + listener_->OnChannelConnected(other_pid); +#if defined(OS_MACOSX) + } else if (m.routing_id() == MSG_ROUTING_NONE && + m.type() == RECEIVED_FDS_MESSAGE_TYPE) { + DCHECK(m.fd_cookie() != 0); + CloseDescriptors(m.fd_cookie()); +#endif + } else { + mozilla::LogIPCMessage::Run run(&m); +#if defined(OS_MACOSX) + if (!AcceptMachPorts(m)) { + return false; + } +#endif + listener_->OnMessageReceived(std::move(incoming_message_)); + } + + incoming_message_ = nullptr; + } + + input_overflow_fds_ = std::vector<int>(&fds[fds_i], &fds[num_fds]); + + // When the input data buffer is empty, the overflow fds should be too. If + // this is not the case, we probably have a rogue renderer which is trying + // to fill our descriptor table. + if (!incoming_message_ && input_buf_offset_ == 0 && + !input_overflow_fds_.empty()) { + // We close these descriptors in Close() + return false; + } + } +} + +bool Channel::ChannelImpl::ProcessOutgoingMessages() { + // NOTE: This method may be called on threads other than `IOThread()`. + chan_cap_.NoteSendMutex(); + + DCHECK(!waiting_connect_); // Why are we trying to send messages if there's + // no connection? + is_blocked_on_write_ = false; + + if (output_queue_.IsEmpty()) return true; + + if (pipe_ == -1) return false; + + // Write out all the messages we can till the write blocks or there are no + // more outgoing messages. + while (!output_queue_.IsEmpty()) { + Message* msg = output_queue_.FirstElement().get(); + + struct msghdr msgh = {0}; + + char cmsgBuf[kControlBufferSize]; + + if (partial_write_.isNothing()) { +#if defined(OS_MACOSX) + if (!TransferMachPorts(*msg)) { + return false; + } +#endif + + if (msg->attached_handles_.Length() > + IPC::Message::MAX_DESCRIPTORS_PER_MESSAGE) { + MOZ_DIAGNOSTIC_ASSERT(false, "Too many file descriptors!"); + CHROMIUM_LOG(FATAL) << "Too many file descriptors!"; + // This should not be reached. + return false; + } + + msg->header()->num_handles = msg->attached_handles_.Length(); +#if defined(OS_MACOSX) + if (!msg->attached_handles_.IsEmpty()) { + msg->set_fd_cookie(++last_pending_fd_id_); + } +#endif + + Pickle::BufferList::IterImpl iter(msg->Buffers()); + MOZ_DIAGNOSTIC_ASSERT(!iter.Done(), "empty message"); + partial_write_.emplace(PartialWrite{iter, msg->attached_handles_}); + + AddIPCProfilerMarker(*msg, other_pid_, MessageDirection::eSending, + MessagePhase::TransferStart); + } + + if (partial_write_->iter_.Done()) { + MOZ_DIAGNOSTIC_ASSERT(false, "partial_write_->iter_ should not be done"); + // report a send error to our caller, which will close the channel. + return false; + } + + // How much of this message have we written so far? + Pickle::BufferList::IterImpl iter = partial_write_->iter_; + auto handles = partial_write_->handles_; + + // Serialize attached file descriptors into the cmsg header. Only up to + // kControlBufferMaxFds can be serialized at once, so messages with more + // attachments must be sent over multiple `sendmsg` calls. + const size_t num_fds = std::min(handles.Length(), kControlBufferMaxFds); + size_t max_amt_to_write = iter.TotalBytesAvailable(msg->Buffers()); + if (num_fds > 0) { + msgh.msg_control = cmsgBuf; + msgh.msg_controllen = CMSG_LEN(sizeof(int) * num_fds); + struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msgh); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = msgh.msg_controllen; + for (size_t i = 0; i < num_fds; ++i) { + reinterpret_cast<int*>(CMSG_DATA(cmsg))[i] = handles[i].get(); + } + + // Avoid writing one byte per remaining handle in excess of + // kControlBufferMaxFds. Each handle written will consume a minimum of 4 + // bytes in the message (to store it's index), so we can depend on there + // being enough data to send every handle. + size_t remaining = handles.Length() - num_fds; + MOZ_ASSERT(max_amt_to_write > remaining, + "must be at least one byte in the message for each handle"); + max_amt_to_write -= remaining; + } + + // Store remaining segments to write into iovec. + // + // Don't add more than kMaxIOVecSize iovecs so that we avoid + // OS-dependent limits. Also, stop adding iovecs if we've already + // prepared to write at least the full buffer size. + struct iovec iov[kMaxIOVecSize]; + size_t iov_count = 0; + size_t amt_to_write = 0; + while (!iter.Done() && iov_count < kMaxIOVecSize && + PipeBufHasSpaceAfter(amt_to_write) && + amt_to_write < max_amt_to_write) { + char* data = iter.Data(); + size_t size = + std::min(iter.RemainingInSegment(), max_amt_to_write - amt_to_write); + + iov[iov_count].iov_base = data; + iov[iov_count].iov_len = size; + iov_count++; + amt_to_write += size; + iter.Advance(msg->Buffers(), size); + } + MOZ_ASSERT(amt_to_write <= max_amt_to_write); + MOZ_ASSERT(amt_to_write > 0); + + const bool intentional_short_write = !iter.Done(); + msgh.msg_iov = iov; + msgh.msg_iovlen = iov_count; + + ssize_t bytes_written = + HANDLE_EINTR(corrected_sendmsg(pipe_, &msgh, MSG_DONTWAIT)); + + if (bytes_written < 0) { + switch (errno) { + case EAGAIN: + // Not an error; the sendmsg would have blocked, so return to the + // event loop and try again later. + break; +#if defined(OS_MACOSX) || defined(OS_NETBSD) + // (Note: this comment is copied from https://crrev.com/86c3d9ef4fdf6; + // see also bug 1142693 comment #73.) + // + // On OS X if sendmsg() is trying to send fds between processes and + // there isn't enough room in the output buffer to send the fd + // structure over atomically then EMSGSIZE is returned. The same + // applies to NetBSD as well. + // + // EMSGSIZE presents a problem since the system APIs can only call us + // when there's room in the socket buffer and not when there is + // "enough" room. + // + // The current behavior is to return to the event loop when EMSGSIZE + // is received and hopefull service another FD. This is however still + // technically a busy wait since the event loop will call us right + // back until the receiver has read enough data to allow passing the + // FD over atomically. + case EMSGSIZE: + // Because this is likely to result in a busy-wait, we'll try to make + // it easier for the receiver to make progress, but only if we're on + // the I/O thread already. + if (IOThread().IsOnCurrentThread()) { + sched_yield(); + } + break; +#endif + default: + if (!ErrorIsBrokenPipe(errno)) { + CHROMIUM_LOG(ERROR) << "pipe error: " << strerror(errno); + } + return false; + } + } + + if (intentional_short_write || + static_cast<size_t>(bytes_written) != amt_to_write) { + // If write() fails with EAGAIN or EMSGSIZE then bytes_written will be -1. + if (bytes_written > 0) { + MOZ_DIAGNOSTIC_ASSERT(intentional_short_write || + static_cast<size_t>(bytes_written) < + amt_to_write); + partial_write_->iter_.AdvanceAcrossSegments(msg->Buffers(), + bytes_written); + partial_write_->handles_ = handles.From(num_fds); + // We should not hit the end of the buffer. + MOZ_DIAGNOSTIC_ASSERT(!partial_write_->iter_.Done()); + } + + is_blocked_on_write_ = true; + if (IOThread().IsOnCurrentThread()) { + // If we're on the I/O thread already, tell libevent to call us back + // when things are unblocked. + MessageLoopForIO::current()->WatchFileDescriptor( + pipe_, + false, // One shot + MessageLoopForIO::WATCH_WRITE, &write_watcher_, this); + } else { + // Otherwise, emulate being called back from libevent on the I/O thread, + // which will re-try the write, and then potentially start watching if + // still necessary. + IOThread().Dispatch(mozilla::NewRunnableMethod<int>( + "ChannelImpl::ContinueProcessOutgoing", this, + &ChannelImpl::OnFileCanWriteWithoutBlocking, -1)); + } + return true; + } else { + MOZ_ASSERT(partial_write_->handles_.Length() == num_fds, + "not all handles were sent"); + partial_write_.reset(); + +#if defined(OS_MACOSX) + if (!msg->attached_handles_.IsEmpty()) { + pending_fds_.push_back(PendingDescriptors{ + msg->fd_cookie(), std::move(msg->attached_handles_)}); + } +#else + if (bytes_written > 0) { + msg->attached_handles_.Clear(); + } +#endif + + // Message sent OK! + + AddIPCProfilerMarker(*msg, other_pid_, MessageDirection::eSending, + MessagePhase::TransferEnd); + +#ifdef IPC_MESSAGE_DEBUG_EXTRA + DLOG(INFO) << "sent message @" << msg << " on channel @" << this + << " with type " << msg->type(); +#endif + OutputQueuePop(); + // msg has been destroyed, so clear the dangling reference. + msg = nullptr; + } + } + return true; +} + +bool Channel::ChannelImpl::Send(mozilla::UniquePtr<Message> message) { + // NOTE: This method may be called on threads other than `IOThread()`. + mozilla::MutexAutoLock lock(SendMutex()); + chan_cap_.NoteSendMutex(); + +#ifdef IPC_MESSAGE_DEBUG_EXTRA + DLOG(INFO) << "sending message @" << message.get() << " on channel @" << this + << " with type " << message->type() << " (" + << output_queue_.Count() << " in queue)"; +#endif + + // If the channel has been closed, ProcessOutgoingMessages() is never going + // to pop anything off output_queue; output_queue will only get emptied when + // the channel is destructed. We might as well delete message now, instead + // of waiting for the channel to be destructed. + if (pipe_ == -1) { + if (mozilla::ipc::LoggingEnabled()) { + fprintf(stderr, + "Can't send message %s, because this channel is closed.\n", + message->name()); + } + return false; + } + + OutputQueuePush(std::move(message)); + if (!waiting_connect_) { + if (!is_blocked_on_write_) { + if (!ProcessOutgoingMessages()) return false; + } + } + + return true; +} + +void Channel::ChannelImpl::GetClientFileDescriptorMapping(int* src_fd, + int* dest_fd) const { + IOThread().AssertOnCurrentThread(); + DCHECK(mode_ == MODE_SERVER); + *src_fd = client_pipe_; + *dest_fd = gClientChannelFd; +} + +void Channel::ChannelImpl::CloseClientFileDescriptor() { + IOThread().AssertOnCurrentThread(); + if (client_pipe_ != -1) { + IGNORE_EINTR(close(client_pipe_)); + client_pipe_ = -1; + } +} + +// Called by libevent when we can read from th pipe without blocking. +void Channel::ChannelImpl::OnFileCanReadWithoutBlocking(int fd) { + IOThread().AssertOnCurrentThread(); + chan_cap_.NoteOnIOThread(); + + if (!waiting_connect_ && fd == pipe_ && pipe_ != -1) { + if (!ProcessIncomingMessages()) { + Close(); + listener_->OnChannelError(); + // The OnChannelError() call may delete this, so we need to exit now. + return; + } + } +} + +#if defined(OS_MACOSX) +void Channel::ChannelImpl::CloseDescriptors(uint32_t pending_fd_id) { + mozilla::MutexAutoLock lock(SendMutex()); + chan_cap_.NoteExclusiveAccess(); + + DCHECK(pending_fd_id != 0); + for (std::list<PendingDescriptors>::iterator i = pending_fds_.begin(); + i != pending_fds_.end(); i++) { + if ((*i).id == pending_fd_id) { + pending_fds_.erase(i); + return; + } + } + DCHECK(false) << "pending_fd_id not in our list!"; +} +#endif + +void Channel::ChannelImpl::OutputQueuePush(mozilla::UniquePtr<Message> msg) { + chan_cap_.NoteSendMutex(); + + mozilla::LogIPCMessage::LogDispatchWithPid(msg.get(), other_pid_); + + MOZ_DIAGNOSTIC_ASSERT(pipe_ != -1); + msg->AssertAsLargeAsHeader(); + output_queue_.Push(std::move(msg)); +} + +void Channel::ChannelImpl::OutputQueuePop() { + // Clear any reference to the front of output_queue_ before we destroy it. + partial_write_.reset(); + + mozilla::UniquePtr<Message> message = output_queue_.Pop(); +} + +// Called by libevent when we can write to the pipe without blocking. +void Channel::ChannelImpl::OnFileCanWriteWithoutBlocking(int fd) { + RefPtr<ChannelImpl> grip(this); + IOThread().AssertOnCurrentThread(); + mozilla::ReleasableMutexAutoLock lock(SendMutex()); + chan_cap_.NoteExclusiveAccess(); + if (pipe_ != -1 && !ProcessOutgoingMessages()) { + CloseLocked(); + lock.Unlock(); + listener_->OnChannelError(); + } +} + +void Channel::ChannelImpl::Close() { + IOThread().AssertOnCurrentThread(); + mozilla::MutexAutoLock lock(SendMutex()); + CloseLocked(); +} + +void Channel::ChannelImpl::CloseLocked() { + chan_cap_.NoteExclusiveAccess(); + + // Close can be called multiple times, so we need to make sure we're + // idempotent. + + // Unregister libevent for the FIFO and close it. + read_watcher_.StopWatchingFileDescriptor(); + write_watcher_.StopWatchingFileDescriptor(); + if (pipe_ != -1) { + IGNORE_EINTR(close(pipe_)); + SetPipe(-1); + } + if (client_pipe_ != -1) { + IGNORE_EINTR(close(client_pipe_)); + client_pipe_ = -1; + } + + while (!output_queue_.IsEmpty()) { + OutputQueuePop(); + } + + // Close any outstanding, received file descriptors + for (std::vector<int>::iterator i = input_overflow_fds_.begin(); + i != input_overflow_fds_.end(); ++i) { + IGNORE_EINTR(close(*i)); + } + input_overflow_fds_.clear(); + +#if defined(OS_MACOSX) + pending_fds_.clear(); + + other_task_ = nullptr; +#endif +} + +#if defined(OS_MACOSX) +void Channel::ChannelImpl::SetOtherMachTask(task_t task) { + IOThread().AssertOnCurrentThread(); + mozilla::MutexAutoLock lock(SendMutex()); + chan_cap_.NoteExclusiveAccess(); + + if (NS_WARN_IF(pipe_ == -1)) { + return; + } + + MOZ_ASSERT(accept_mach_ports_ && privileged_ && waiting_connect_); + other_task_ = mozilla::RetainMachSendRight(task); + // Now that `other_task_` is provided, we can continue connecting. + ConnectLocked(); +} + +void Channel::ChannelImpl::StartAcceptingMachPorts(Mode mode) { + IOThread().AssertOnCurrentThread(); + mozilla::MutexAutoLock lock(SendMutex()); + chan_cap_.NoteExclusiveAccess(); + + if (accept_mach_ports_) { + MOZ_ASSERT(privileged_ == (MODE_SERVER == mode)); + return; + } + accept_mach_ports_ = true; + privileged_ = MODE_SERVER == mode; +} + +//------------------------------------------------------------------------------ +// Mach port transferring logic +// +// It is currently not possible to directly transfer a mach send right between +// two content processes using SCM_RIGHTS, unlike how we can handle file +// descriptors. This means that mach ports need to be transferred through a +// separate mechanism. This file only implements support for transferring mach +// ports between a (potentially sandboxed) child process and the parent process. +// Support for transferring mach ports between other process pairs is handled by +// `NodeController`, which is responsible for relaying messages which carry +// handles via the parent process. +// +// The logic which we use for doing this is based on the following from +// Chromium, which pioneered this technique. As of this writing, chromium no +// longer uses this strategy, as all IPC messages are sent using mach ports on +// macOS. +// https://source.chromium.org/chromium/chromium/src/+/9f707e5e04598d8303fa99ca29eb507c839767d8:mojo/core/mach_port_relay.cc +// https://source.chromium.org/chromium/chromium/src/+/9f707e5e04598d8303fa99ca29eb507c839767d8:base/mac/mach_port_util.cc. +// +// As we only need to consider messages between the privileged (parent) and +// unprivileged (child) processes in this code, there are 2 relevant cases which +// we need to handle: +// +// # Unprivileged (child) to Privileged (parent) +// +// As the privileged process has access to the unprivileged process' `task_t`, +// it is possible to directly extract the mach port from the target process' +// address space, given its name, using `mach_port_extract_right`. +// +// To transfer the port, the unprivileged process will leak a reference to the +// send right, and include the port's name in the message footer. The privileged +// process will extract that port right (and drop the reference in the old +// process) using `mach_port_extract_right` with `MACH_MSG_TYPE_MOVE_SEND`. The +// call to `mach_port_extract_right` is handled by `BrokerExtractSendRight` +// +// # Privileged (parent) to Unprivileged (child) +// +// Unfortunately, the process of transferring a right into a target process is +// more complex. The only well-supported way to transfer a right into a process +// is by sending it with `mach_msg`, and receiving it on the other side [1]. +// +// To work around this, the privileged process uses `mach_port_allocate` to +// create a new receive right in the target process using its `task_t`, and +// `mach_port_extract_right` to extract a send-once right to that port. It then +// sends a message to the port with port we're intending to send as an +// attachment. This is handled by `BrokerTransferSendRight`, which returns the +// name of the newly created receive right in the target process to be sent in +// the message footer. +// +// In the unprivileged process, `mach_msg` is used to receive a single message +// from the receive right, which will have the actual port we were trying to +// transfer as an attachment. This is handled by the `MachReceivePortSendRight` +// function. +// +// [1] We cannot use `mach_port_insert_right` to transfer the right into the +// target process, as that method requires explicitly specifying the remote +// port's name, and we do not control the port name allocator. + +// Extract a send right from the given peer task. A reference to the remote +// right will be dropped. See comment above for details. +static mozilla::UniqueMachSendRight BrokerExtractSendRight( + task_t task, mach_port_name_t name) { + mach_port_t extractedRight = MACH_PORT_NULL; + mach_msg_type_name_t extractedRightType; + kern_return_t kr = + mach_port_extract_right(task, name, MACH_MSG_TYPE_MOVE_SEND, + &extractedRight, &extractedRightType); + if (kr != KERN_SUCCESS) { + CHROMIUM_LOG(ERROR) << "failed to extract port right from other process. " + << mach_error_string(kr); + return nullptr; + } + MOZ_ASSERT(extractedRightType == MACH_MSG_TYPE_PORT_SEND, + "We asked the OS for a send port"); + return mozilla::UniqueMachSendRight(extractedRight); +} + +// Transfer a send right to the given peer task. The name of a receive right in +// the remote process will be returned if successful. The sent port can be +// obtained from that port in the peer task using `MachReceivePortSendRight`. +// See comment above for details. +static mozilla::Maybe<mach_port_name_t> BrokerTransferSendRight( + task_t task, mozilla::UniqueMachSendRight port_to_send) { + mach_port_name_t endpoint; + kern_return_t kr = + mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &endpoint); + if (kr != KERN_SUCCESS) { + CHROMIUM_LOG(ERROR) + << "Unable to create receive right in TransferMachPorts. " + << mach_error_string(kr); + return mozilla::Nothing(); + } + + // Clean up the endpoint on error. + auto destroyEndpoint = + mozilla::MakeScopeExit([&] { mach_port_deallocate(task, endpoint); }); + + // Change its message queue limit so that it accepts one message. + mach_port_limits limits = {}; + limits.mpl_qlimit = 1; + kr = mach_port_set_attributes(task, endpoint, MACH_PORT_LIMITS_INFO, + reinterpret_cast<mach_port_info_t>(&limits), + MACH_PORT_LIMITS_INFO_COUNT); + if (kr != KERN_SUCCESS) { + CHROMIUM_LOG(ERROR) + << "Unable configure receive right in TransferMachPorts. " + << mach_error_string(kr); + return mozilla::Nothing(); + } + + // Get a send right. + mach_port_t send_once_right; + mach_msg_type_name_t send_right_type; + kr = mach_port_extract_right(task, endpoint, MACH_MSG_TYPE_MAKE_SEND_ONCE, + &send_once_right, &send_right_type); + if (kr != KERN_SUCCESS) { + CHROMIUM_LOG(ERROR) << "Unable extract send right in TransferMachPorts. " + << mach_error_string(kr); + return mozilla::Nothing(); + } + MOZ_ASSERT(MACH_MSG_TYPE_PORT_SEND_ONCE == send_right_type); + + kr = MachSendPortSendRight(send_once_right, port_to_send.get(), + mozilla::Some(0), MACH_MSG_TYPE_MOVE_SEND_ONCE); + if (kr != KERN_SUCCESS) { + // This right will be destroyed due to being a SEND_ONCE right if we + // succeed. + mach_port_deallocate(mach_task_self(), send_once_right); + CHROMIUM_LOG(ERROR) << "Unable to transfer right in TransferMachPorts. " + << mach_error_string(kr); + return mozilla::Nothing(); + } + + destroyEndpoint.release(); + return mozilla::Some(endpoint); +} + +// Process footer information attached to the message, and acquire owning +// references to any transferred mach ports. See comment above for details. +bool Channel::ChannelImpl::AcceptMachPorts(Message& msg) { + chan_cap_.NoteOnIOThread(); + + uint32_t num_send_rights = msg.header()->num_send_rights; + if (num_send_rights == 0) { + return true; + } + + if (!accept_mach_ports_) { + CHROMIUM_LOG(ERROR) << "invalid message: " << msg.name() + << ". channel is not configured to accept mach ports"; + return false; + } + + // Read in the payload from the footer, truncating the message. + nsTArray<uint32_t> payload; + payload.AppendElements(num_send_rights); + if (!msg.ReadFooter(payload.Elements(), num_send_rights * sizeof(uint32_t), + /* truncate */ true)) { + CHROMIUM_LOG(ERROR) << "failed to read mach port payload from message"; + return false; + } + msg.header()->num_send_rights = 0; + + // Read in the handles themselves, transferring ownership as required. + nsTArray<mozilla::UniqueMachSendRight> rights(num_send_rights); + for (uint32_t name : payload) { + mozilla::UniqueMachSendRight right; + if (privileged_) { + if (!other_task_) { + CHROMIUM_LOG(ERROR) << "other_task_ is invalid in AcceptMachPorts"; + return false; + } + right = BrokerExtractSendRight(other_task_.get(), name); + } else { + kern_return_t kr = MachReceivePortSendRight( + mozilla::UniqueMachReceiveRight(name), mozilla::Some(0), &right); + if (kr != KERN_SUCCESS) { + CHROMIUM_LOG(ERROR) + << "failed to receive mach send right. " << mach_error_string(kr); + return false; + } + } + if (!right) { + return false; + } + rights.AppendElement(std::move(right)); + } + + // We're done with the handle footer, truncate the message at that point. + msg.attached_send_rights_ = std::move(rights); + MOZ_ASSERT(msg.num_send_rights() == num_send_rights); + return true; +} + +// Transfer ownership of any attached mach ports to the peer task, and add the +// required information for AcceptMachPorts to the message footer. See comment +// above for details. +bool Channel::ChannelImpl::TransferMachPorts(Message& msg) { + uint32_t num_send_rights = msg.num_send_rights(); + if (num_send_rights == 0) { + return true; + } + + if (!accept_mach_ports_) { + CHROMIUM_LOG(ERROR) << "cannot send message: " << msg.name() + << ". channel is not configured to accept mach ports"; + return false; + } + +# ifdef DEBUG + uint32_t rights_offset = msg.header()->payload_size; +# endif + + nsTArray<uint32_t> payload(num_send_rights); + for (auto& port_to_send : msg.attached_send_rights_) { + if (privileged_) { + if (!other_task_) { + CHROMIUM_LOG(ERROR) << "other_task_ is invalid in TransferMachPorts"; + return false; + } + mozilla::Maybe<mach_port_name_t> endpoint = + BrokerTransferSendRight(other_task_.get(), std::move(port_to_send)); + if (!endpoint) { + return false; + } + payload.AppendElement(*endpoint); + } else { + payload.AppendElement(port_to_send.release()); + } + } + msg.attached_send_rights_.Clear(); + + msg.WriteFooter(payload.Elements(), payload.Length() * sizeof(uint32_t)); + msg.header()->num_send_rights = num_send_rights; + + MOZ_ASSERT(msg.header()->payload_size == + rights_offset + (sizeof(uint32_t) * num_send_rights), + "Unexpected number of bytes written for send rights footer?"); + return true; +} +#endif + +//------------------------------------------------------------------------------ +// Channel's methods simply call through to ChannelImpl. +Channel::Channel(const ChannelId& channel_id, Mode mode, Listener* listener) + : channel_impl_(new ChannelImpl(channel_id, mode, listener)) { + MOZ_COUNT_CTOR(IPC::Channel); +} + +Channel::Channel(ChannelHandle pipe, Mode mode, Listener* listener) + : channel_impl_(new ChannelImpl(std::move(pipe), mode, listener)) { + MOZ_COUNT_CTOR(IPC::Channel); +} + +Channel::~Channel() { MOZ_COUNT_DTOR(IPC::Channel); } + +bool Channel::Connect() { return channel_impl_->Connect(); } + +void Channel::Close() { channel_impl_->Close(); } + +Channel::Listener* Channel::set_listener(Listener* listener) { + return channel_impl_->set_listener(listener); +} + +bool Channel::Send(mozilla::UniquePtr<Message> message) { + return channel_impl_->Send(std::move(message)); +} + +void Channel::GetClientFileDescriptorMapping(int* src_fd, int* dest_fd) const { + return channel_impl_->GetClientFileDescriptorMapping(src_fd, dest_fd); +} + +void Channel::CloseClientFileDescriptor() { + channel_impl_->CloseClientFileDescriptor(); +} + +int32_t Channel::OtherPid() const { return channel_impl_->OtherPid(); } + +bool Channel::IsClosed() const { return channel_impl_->IsClosed(); } + +#if defined(OS_MACOSX) +void Channel::SetOtherMachTask(task_t task) { + channel_impl_->SetOtherMachTask(task); +} + +void Channel::StartAcceptingMachPorts(Mode mode) { + channel_impl_->StartAcceptingMachPorts(mode); +} +#endif + +// static +Channel::ChannelId Channel::GenerateVerifiedChannelID() { return {}; } + +// static +Channel::ChannelId Channel::ChannelIDForCurrentProcess() { return {}; } + +// static +bool Channel::CreateRawPipe(ChannelHandle* server, ChannelHandle* client) { + int fds[2]; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) < 0) { + mozilla::ipc::AnnotateCrashReportWithErrno( + CrashReporter::Annotation::IpcCreatePipeSocketPairErrno, errno); + return false; + } + + auto configureFd = [](int fd) -> bool { + // Mark the endpoints as non-blocking + if (fcntl(fd, F_SETFL, O_NONBLOCK) == -1) { + mozilla::ipc::AnnotateCrashReportWithErrno( + CrashReporter::Annotation::IpcCreatePipeFcntlErrno, errno); + return false; + } + + // Mark the pipes as FD_CLOEXEC + int flags = fcntl(fd, F_GETFD); + if (flags == -1) { + mozilla::ipc::AnnotateCrashReportWithErrno( + CrashReporter::Annotation::IpcCreatePipeCloExecErrno, errno); + return false; + } + flags |= FD_CLOEXEC; + if (fcntl(fd, F_SETFD, flags) == -1) { + mozilla::ipc::AnnotateCrashReportWithErrno( + CrashReporter::Annotation::IpcCreatePipeCloExecErrno, errno); + return false; + } + return true; + }; + + if (!configureFd(fds[0]) || !configureFd(fds[1])) { + IGNORE_EINTR(close(fds[0])); + IGNORE_EINTR(close(fds[1])); + return false; + } + + server->reset(fds[0]); + client->reset(fds[1]); + return true; +} + +} // namespace IPC |