// Copyright 2014 The Crashpad Authors
//
// 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/child_port_handshake.h"

#include <errno.h>
#include <pthread.h>
#include <stdint.h>
#include <sys/event.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

#include <algorithm>
#include <iterator>
#include <utility>

#include "base/check_op.h"
#include "base/logging.h"
#include "base/mac/mach_logging.h"
#include "base/mac/scoped_mach_port.h"
#include "base/notreached.h"
#include "base/posix/eintr_wrapper.h"
#include "base/rand_util.h"
#include "base/strings/stringprintf.h"
#include "util/file/file_io.h"
#include "util/mach/bootstrap.h"
#include "util/mach/child_port.h"
#include "util/mach/child_port_server.h"
#include "util/mach/mach_extensions.h"
#include "util/mach/mach_message.h"
#include "util/mach/mach_message_server.h"
#include "util/misc/implicit_cast.h"
#include "util/misc/random_string.h"

namespace crashpad {
namespace {

class ChildPortHandshakeServer final : public ChildPortServer::Interface {
 public:
  ChildPortHandshakeServer();

  ChildPortHandshakeServer(const ChildPortHandshakeServer&) = delete;
  ChildPortHandshakeServer& operator=(const ChildPortHandshakeServer&) = delete;

  ~ChildPortHandshakeServer();

  mach_port_t RunServer(base::ScopedFD server_write_fd,
                        ChildPortHandshake::PortRightType port_right_type);

 private:
  // ChildPortServer::Interface:
  kern_return_t HandleChildPortCheckIn(child_port_server_t server,
                                       child_port_token_t token,
                                       mach_port_t port,
                                       mach_msg_type_name_t right_type,
                                       const mach_msg_trailer_t* trailer,
                                       bool* destroy_request) override;

  child_port_token_t token_;
  mach_port_t port_;
  mach_msg_type_name_t right_type_;
  bool checked_in_;
};

ChildPortHandshakeServer::ChildPortHandshakeServer()
    : token_(0),
      port_(MACH_PORT_NULL),
      right_type_(MACH_MSG_TYPE_PORT_NONE),
      checked_in_(false) {
}

ChildPortHandshakeServer::~ChildPortHandshakeServer() {
}

mach_port_t ChildPortHandshakeServer::RunServer(
    base::ScopedFD server_write_fd,
    ChildPortHandshake::PortRightType port_right_type) {
  DCHECK_EQ(port_, kMachPortNull);
  DCHECK(!checked_in_);
  DCHECK(server_write_fd.is_valid());

  // Initialize the token and share it with the client via the pipe.
  token_ = base::RandUint64();
  if (!LoggingWriteFile(server_write_fd.get(), &token_, sizeof(token_))) {
    LOG(WARNING) << "no client check-in";
    return MACH_PORT_NULL;
  }

  // Create a unique name for the bootstrap service mapping. Make it unguessable
  // to prevent outsiders from grabbing the name first, which would cause
  // bootstrap_check_in() to fail.
  uint64_t thread_id;
  errno = pthread_threadid_np(pthread_self(), &thread_id);
  PCHECK(errno == 0) << "pthread_threadid_np";
  std::string service_name = base::StringPrintf(
      "org.chromium.crashpad.child_port_handshake.%d.%llu.%s",
      getpid(),
      thread_id,
      RandomString().c_str());

  // Check the new service in with the bootstrap server, obtaining a receive
  // right for it.
  base::mac::ScopedMachReceiveRight server_port(BootstrapCheckIn(service_name));
  CHECK(server_port.is_valid());

  // Share the service name with the client via the pipe.
  uint32_t service_name_length = service_name.size();
  if (!LoggingWriteFile(server_write_fd.get(),
                        &service_name_length,
                        sizeof(service_name_length))) {
    LOG(WARNING) << "no client check-in";
    return MACH_PORT_NULL;
  }

  if (!LoggingWriteFile(
          server_write_fd.get(), service_name.c_str(), service_name_length)) {
    LOG(WARNING) << "no client check-in";
    return MACH_PORT_NULL;
  }

  // Prior to macOS 10.12, a kqueue cannot monitor a raw Mach receive right with
  // EVFILT_MACHPORT. It requires a port set. Compare 10.11.6
  // xnu-3248.60.10/osfmk/ipc/ipc_pset.c filt_machportattach(), which requires
  // MACH_PORT_RIGHT_PORT_SET, to 10.12.0 xnu-3789.1.32/osfmk/ipc/ipc_pset.c
  // filt_machportattach(), which also handles MACH_PORT_TYPE_RECEIVE. Create a
  // new port set and add the receive right to it.
  base::mac::ScopedMachPortSet server_port_set(
      NewMachPort(MACH_PORT_RIGHT_PORT_SET));
  CHECK(server_port_set.is_valid());

  kern_return_t kr = mach_port_insert_member(
      mach_task_self(), server_port.get(), server_port_set.get());
  MACH_CHECK(kr == KERN_SUCCESS, kr) << "mach_port_insert_member";

  // Set up a kqueue to monitor both the server’s receive right and the write
  // side of the pipe. Messages from the client will be received via the receive
  // right, and the pipe will show EOF if the client closes its read side
  // prematurely.
  base::ScopedFD kq(kqueue());
  PCHECK(kq != -1) << "kqueue";

  struct kevent changelist[2];
  EV_SET(&changelist[0],
         server_port_set.get(),
         EVFILT_MACHPORT,
         EV_ADD | EV_CLEAR,
         0,
         0,
         nullptr);
  EV_SET(&changelist[1],
         server_write_fd.get(),
         EVFILT_WRITE,
         EV_ADD | EV_CLEAR,
         0,
         0,
         nullptr);
  int rv = HANDLE_EINTR(
      kevent(kq.get(), changelist, std::size(changelist), nullptr, 0, nullptr));
  PCHECK(rv != -1) << "kevent";

  ChildPortServer child_port_server(this);

  bool blocking = true;
  DCHECK(!checked_in_);
  while (!checked_in_) {
    DCHECK_EQ(port_, kMachPortNull);

    // Get a kevent from the kqueue. Block while waiting for an event unless the
    // write pipe has arrived at EOF, in which case the kevent() should be
    // nonblocking. Although the client sends its check-in message before
    // closing the read side of the pipe, this organization allows the events to
    // be delivered out of order and the check-in message will still be
    // processed.
    struct kevent event;
    constexpr timespec nonblocking_timeout = {};
    const timespec* timeout = blocking ? nullptr : &nonblocking_timeout;
    rv = HANDLE_EINTR(kevent(kq.get(), nullptr, 0, &event, 1, timeout));
    PCHECK(rv != -1) << "kevent";

    if (rv == 0) {
      // Non-blocking kevent() with no events to return.
      DCHECK(!blocking);
      LOG(WARNING) << "no client check-in";
      return MACH_PORT_NULL;
    }

    DCHECK_EQ(rv, 1);

    if (event.flags & EV_ERROR) {
      // kevent() may have put its error here.
      errno = event.data;
      PLOG(FATAL) << "kevent";
    }

    switch (event.filter) {
      case EVFILT_MACHPORT: {
        // There’s something to receive on the port set.
        DCHECK_EQ(event.ident, server_port_set.get());

        // Run the message server in an inner loop instead of using
        // MachMessageServer::kPersistent. This allows the loop to exit as soon
        // as child_port_ is set, even if other messages are queued. This needs
        // to drain all messages, because the use of edge triggering (EV_CLEAR)
        // means that if more than one message is in the queue when kevent()
        // returns, no more notifications will be generated.
        while (!checked_in_) {
          // If a proper message is received from child_port_check_in(),
          // this will call HandleChildPortCheckIn().
          mach_msg_return_t mr =
              MachMessageServer::Run(&child_port_server,
                                     server_port_set.get(),
                                     MACH_MSG_OPTION_NONE,
                                     MachMessageServer::kOneShot,
                                     MachMessageServer::kReceiveLargeIgnore,
                                     kMachMessageTimeoutNonblocking);
          if (mr == MACH_RCV_TIMED_OUT) {
            break;
          } else if (mr != MACH_MSG_SUCCESS) {
            MACH_LOG(ERROR, mr) << "MachMessageServer::Run";
            return MACH_PORT_NULL;
          }
        }
        break;
      }

      case EVFILT_WRITE:
        // The write pipe is ready to be written to, or it’s at EOF. The former
        // case is uninteresting, but a notification for this may be presented
        // because the write pipe will be ready to be written to, at the latest,
        // when the client reads its messages from the read side of the same
        // pipe. Ignore that case. Multiple notifications for that situation
        // will not be generated because edge triggering (EV_CLEAR) is used
        // above.
        DCHECK_EQ(implicit_cast<int>(event.ident), server_write_fd.get());
        if (event.flags & EV_EOF) {
          // There are no readers attached to the write pipe. The client has
          // closed its side of the pipe. There can be one last shot at
          // receiving messages, in case the check-in message is delivered
          // out of order, after the EOF notification.
          blocking = false;
        }
        break;

      default:
        NOTREACHED();
        break;
    }
  }

  if (port_ == MACH_PORT_NULL) {
    return MACH_PORT_NULL;
  }

  bool mismatch = false;
  switch (port_right_type) {
    case ChildPortHandshake::PortRightType::kReceiveRight:
      if (right_type_ != MACH_MSG_TYPE_PORT_RECEIVE) {
        LOG(ERROR) << "expected receive right, observed " << right_type_;
        mismatch = true;
      }
      break;
    case ChildPortHandshake::PortRightType::kSendRight:
      if (right_type_ != MACH_MSG_TYPE_PORT_SEND &&
          right_type_ != MACH_MSG_TYPE_PORT_SEND_ONCE) {
        LOG(ERROR) << "expected send or send-once right, observed "
                   << right_type_;
        mismatch = true;
      }
      break;
  }

  if (mismatch) {
    MachMessageDestroyReceivedPort(port_, right_type_);
    port_ = MACH_PORT_NULL;
    return MACH_PORT_NULL;
  }

  mach_port_t port = MACH_PORT_NULL;
  std::swap(port_, port);
  return port;
}

kern_return_t ChildPortHandshakeServer::HandleChildPortCheckIn(
    child_port_server_t server,
    const child_port_token_t token,
    mach_port_t port,
    mach_msg_type_name_t right_type,
    const mach_msg_trailer_t* trailer,
    bool* destroy_request) {
  DCHECK_EQ(port_, kMachPortNull);
  DCHECK(!checked_in_);

  if (token != token_) {
    // If the token’s not correct, someone’s attempting to spoof the legitimate
    // client.
    LOG(WARNING) << "ignoring incorrect token";
    *destroy_request = true;
  } else {
    checked_in_ = true;

    if (right_type != MACH_MSG_TYPE_PORT_RECEIVE &&
        right_type != MACH_MSG_TYPE_PORT_SEND &&
        right_type != MACH_MSG_TYPE_PORT_SEND_ONCE) {
      // The message needs to carry a receive, send, or send-once right.
      LOG(ERROR) << "invalid right type " << right_type;
      *destroy_request = true;
    } else {
      // Communicate the child port and right type back to the RunServer().
      // *destroy_request is left at false, because RunServer() needs the right
      // to remain intact. It gives ownership of the right to its caller.
      port_ = port;
      right_type_ = right_type;
    }
  }

  // This is a MIG simpleroutine, there is no reply message.
  return MIG_NO_REPLY;
}

}  // namespace

ChildPortHandshake::ChildPortHandshake()
    : client_read_fd_(),
      server_write_fd_() {
  // Use socketpair() instead of pipe(). There is no way to suppress SIGPIPE on
  // pipes in Mac OS X 10.6, because the F_SETNOSIGPIPE fcntl() command was not
  // introduced until 10.7.
  int pipe_fds[2];
  PCHECK(socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, pipe_fds) == 0)
      << "socketpair";

  client_read_fd_.reset(pipe_fds[0]);
  server_write_fd_.reset(pipe_fds[1]);

  // Simulate pipe() semantics by shutting down the “wrong” sides of the socket.
  PCHECK(shutdown(server_write_fd_.get(), SHUT_RD) == 0) << "shutdown SHUT_RD";
  PCHECK(shutdown(client_read_fd_.get(), SHUT_WR) == 0) << "shutdown SHUT_WR";

  // SIGPIPE is undesirable when writing to this pipe. Allow broken-pipe writes
  // to fail with EPIPE instead.
  constexpr int value = 1;
  PCHECK(setsockopt(server_write_fd_.get(),
                    SOL_SOCKET,
                    SO_NOSIGPIPE,
                    &value,
                    sizeof(value)) == 0) << "setsockopt";
}

ChildPortHandshake::~ChildPortHandshake() {
}

base::ScopedFD ChildPortHandshake::ClientReadFD() {
  DCHECK(client_read_fd_.is_valid());
  return std::move(client_read_fd_);
}

base::ScopedFD ChildPortHandshake::ServerWriteFD() {
  DCHECK(server_write_fd_.is_valid());
  return std::move(server_write_fd_);
}

mach_port_t ChildPortHandshake::RunServer(PortRightType port_right_type) {
  client_read_fd_.reset();
  return RunServerForFD(std::move(server_write_fd_), port_right_type);
}

bool ChildPortHandshake::RunClient(mach_port_t port,
                                   mach_msg_type_name_t right_type) {
  server_write_fd_.reset();
  return RunClientForFD(std::move(client_read_fd_), port, right_type);
}

// static
mach_port_t ChildPortHandshake::RunServerForFD(base::ScopedFD server_write_fd,
                                               PortRightType port_right_type) {
  ChildPortHandshakeServer server;
  return server.RunServer(std::move(server_write_fd), port_right_type);
}

// static
bool ChildPortHandshake::RunClientForFD(base::ScopedFD client_read_fd,
                                        mach_port_t port,
                                        mach_msg_type_name_t right_type) {
  DCHECK(client_read_fd.is_valid());

  // Read the token and the service name from the read side of the pipe.
  child_port_token_t token;
  std::string service_name;
  if (!RunClientInternal_ReadPipe(
          client_read_fd.get(), &token, &service_name)) {
    return false;
  }

  // Look up the server and check in with it by providing the token and port.
  return RunClientInternal_SendCheckIn(service_name, token, port, right_type);
}

// static
bool ChildPortHandshake::RunClientInternal_ReadPipe(int client_read_fd,
                                                    child_port_token_t* token,
                                                    std::string* service_name) {
  // Read the token from the pipe.
  if (!LoggingReadFileExactly(client_read_fd, token, sizeof(*token))) {
    return false;
  }

  // Read the service name from the pipe.
  uint32_t service_name_length;
  if (!LoggingReadFileExactly(
          client_read_fd, &service_name_length, sizeof(service_name_length))) {
    return false;
  }

  service_name->resize(service_name_length);
  if (!service_name->empty() &&
      !LoggingReadFileExactly(
          client_read_fd, &(*service_name)[0], service_name_length)) {
    return false;
  }

  return true;
}

// static
bool ChildPortHandshake::RunClientInternal_SendCheckIn(
    const std::string& service_name,
    child_port_token_t token,
    mach_port_t port,
    mach_msg_type_name_t right_type) {
  // Get a send right to the server by looking up the service with the bootstrap
  // server by name.
  base::mac::ScopedMachSendRight server_port(BootstrapLookUp(service_name));
  if (server_port == kMachPortNull) {
    return false;
  }

  // Check in with the server.
  kern_return_t kr = child_port_check_in(
      server_port.get(), token, port, right_type);
  if (kr != KERN_SUCCESS) {
    MACH_LOG(ERROR, kr) << "child_port_check_in";
    return false;
  }

  return true;
}

}  // namespace crashpad
