// Copyright 2014 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 "sandbox/mac/launchd_interception_server.h"

#include <servers/bootstrap.h>
#include <stddef.h>
#include <stdint.h>

#include "base/logging.h"
#include "base/mac/mac_util.h"
#include "base/mac/mach_logging.h"
#include "sandbox/mac/bootstrap_sandbox.h"
#include "sandbox/mac/mach_message_server.h"
#include "sandbox/mac/os_compatibility.h"
#include "sandbox/mac/xpc_message_server.h"

namespace sandbox {

// The buffer size for all launchd messages. This comes from
// sizeof(union __RequestUnion__vproc_mig_job_subsystem) in launchd, and it
// is larger than the __ReplyUnion.
const mach_msg_size_t kBufferSize = 2096;

LaunchdInterceptionServer::LaunchdInterceptionServer(
    const BootstrapSandbox* sandbox)
    : sandbox_(sandbox),
      xpc_launchd_(false),
      sandbox_port_(MACH_PORT_NULL),
      compat_shim_(OSCompatibility::CreateForPlatform()) {
}

LaunchdInterceptionServer::~LaunchdInterceptionServer() {
  message_server_->Shutdown();
}

bool LaunchdInterceptionServer::Initialize(mach_port_t server_receive_right) {
  mach_port_t task = mach_task_self();
  kern_return_t kr;

  // Allocate the dummy sandbox port.
  mach_port_t port;
  if ((kr = mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &port)) !=
          KERN_SUCCESS) {
    MACH_LOG(ERROR, kr) << "Failed to allocate dummy sandbox port.";
    return false;
  }
  sandbox_port_.reset(port);
  if ((kr = mach_port_insert_right(task, sandbox_port_.get(),
          sandbox_port_.get(), MACH_MSG_TYPE_MAKE_SEND) != KERN_SUCCESS)) {
    MACH_LOG(ERROR, kr) << "Failed to allocate dummy sandbox port send right.";
    return false;
  }
  sandbox_send_port_.reset(sandbox_port_.get());

  if (base::mac::IsAtLeastOS10_10()) {
    message_server_.reset(new XPCMessageServer(this, server_receive_right));
    xpc_launchd_ = true;
  } else {
    message_server_.reset(
        new MachMessageServer(this, server_receive_right, kBufferSize));
  }
  return message_server_->Initialize();
}

void LaunchdInterceptionServer::DemuxMessage(IPCMessage request) {
  const uint64_t message_subsystem =
      compat_shim_->GetMessageSubsystem(request);
  const uint64_t message_id = compat_shim_->GetMessageID(request);
  VLOG(3) << "Incoming message #" << message_subsystem << "," << message_id;

  pid_t sender_pid = message_server_->GetMessageSenderPID(request);
  const BootstrapSandboxPolicy* policy =
      sandbox_->PolicyForProcess(sender_pid);
  if (policy == NULL) {
    // No sandbox policy is in place for the sender of this message, which
    // means it came from the unknown. Reject it.
    VLOG(3) << "Message from unknown pid " << sender_pid << " rejected.";
    message_server_->RejectMessage(request, MIG_REMOTE_ERROR);
    return;
  }

  if (compat_shim_->IsServiceLookUpRequest(request)) {
    // Filter messages sent via bootstrap_look_up to enforce the sandbox policy
    // over the bootstrap namespace.
    HandleLookUp(request, policy);
  } else if (compat_shim_->IsVprocSwapInteger(request)) {
    // Ensure that any vproc_swap_integer requests are safe.
    HandleSwapInteger(request);
  } else if (compat_shim_->IsXPCDomainManagement(request)) {
    // XPC domain management requests just require an ACK.
    message_server_->SendReply(message_server_->CreateReply(request));
  } else {
    // All other messages are not permitted.
    VLOG(1) << "Rejecting unhandled message #" << message_id;
    message_server_->RejectMessage(request, MIG_REMOTE_ERROR);
  }
}

void LaunchdInterceptionServer::HandleLookUp(
    IPCMessage request,
    const BootstrapSandboxPolicy* policy) {
  const std::string request_service_name(
      compat_shim_->GetServiceLookupName(request));
  VLOG(2) << "Incoming look_up2 request for " << request_service_name;

  // Find the Rule for this service. If a named rule is not found, use the
  // default specified by the policy.
  const BootstrapSandboxPolicy::NamedRules::const_iterator it =
      policy->rules.find(request_service_name);
  Rule rule(policy->default_rule);
  if (it != policy->rules.end())
    rule = it->second;

  if (rule.result == POLICY_ALLOW) {
    // This service is explicitly allowed, so this message will not be
    // intercepted by the sandbox.
    VLOG(1) << "Permitting and forwarding look_up2: " << request_service_name;
    ForwardMessage(request);
  } else if (rule.result == POLICY_DENY_ERROR) {
    // The child is not permitted to look up this service. Send a MIG error
    // reply to the client. Returning a NULL or unserviced port for a look up
    // can cause clients to crash or hang.
    VLOG(1) << "Denying look_up2 with MIG error: " << request_service_name;
    message_server_->RejectMessage(request, BOOTSTRAP_UNKNOWN_SERVICE);
  } else if (rule.result == POLICY_DENY_DUMMY_PORT ||
             rule.result == POLICY_SUBSTITUTE_PORT) {
    // The policy result is to deny access to the real service port, replying
    // with a sandboxed port in its stead. Use either the dummy sandbox_port_
    // or the one specified in the policy.
    VLOG(1) << "Intercepting look_up2 with a sandboxed service port: "
            << request_service_name;

    mach_port_t result_port;
    if (rule.result == POLICY_DENY_DUMMY_PORT)
      result_port = sandbox_port_.get();
    else
      result_port = rule.substitute_port;

    IPCMessage reply = message_server_->CreateReply(request);
    compat_shim_->WriteServiceLookUpReply(reply, result_port);
    // If the message was sent successfully, clear the result_port out of the
    // message so that it is not destroyed at the end of ReceiveMessage. The
    // above-inserted right has been moved out of the process, and destroying
    // the message will unref yet another right.
    if (message_server_->SendReply(reply))
      compat_shim_->WriteServiceLookUpReply(reply, MACH_PORT_NULL);
  } else {
    NOTREACHED();
  }
}

void LaunchdInterceptionServer::HandleSwapInteger(IPCMessage request) {
  // Only allow getting information out of launchd. Do not allow setting
  // values. Two commonly observed values that are retrieved are
  // VPROC_GSK_MGR_PID and VPROC_GSK_TRANSACTIONS_ENABLED.
  if (compat_shim_->IsSwapIntegerReadOnly(request)) {
    VLOG(2) << "Forwarding vproc swap_integer message.";
    ForwardMessage(request);
  } else {
    VLOG(2) << "Rejecting non-read-only swap_integer message.";
    message_server_->RejectMessage(request, BOOTSTRAP_NOT_PRIVILEGED);
  }
}
void LaunchdInterceptionServer::ForwardMessage(IPCMessage request) {
  // If launchd is using XPC, then when the request is forwarded, it must
  // contain a valid domain port. Because the client processes are sandboxed,
  // they have not had their launchd domains uncorked (and launchd will
  // reject the message as being from an invalid client). Instead, provide the
  // original bootstrap as the domain port, so launchd services the request
  // as if it were coming from the sandbox host process (this).
  if (xpc_launchd_) {
    // xpc_dictionary_set_mach_send increments the send right count.
    xpc_dictionary_set_mach_send(request.xpc, "domain-port",
                                 sandbox_->real_bootstrap_port());
  }

  message_server_->ForwardMessage(request, sandbox_->real_bootstrap_port());
}

}  // namespace sandbox
