// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/apple/mach_port_rendezvous_mac.h"

#include <bsm/libbsm.h>
#include <mach/mig.h>
#include <servers/bootstrap.h>
#include <unistd.h>

#include <utility>

#include "base/apple/app_plist_size_buildflags.h"
#include "base/apple/foundation_util.h"
#include "base/apple/mach_logging.h"
#include "base/apple/scoped_dispatch_object.h"
#include "base/bits.h"
#include "base/compiler_specific.h"
#include "base/containers/buffer_iterator.h"
#include "base/environment.h"
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/mac/info_plist_data.h"
#include "base/mac/scoped_mach_msg_destroy.h"
#include "base/no_destructor.h"
#include "base/numerics/byte_conversions.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"

namespace base {

// Whether any peer process requirements should be validated.
BASE_FEATURE(kMachPortRendezvousValidatePeerRequirements,
             base::FEATURE_ENABLED_BY_DEFAULT);

// Whether a failure to validate a peer process against a requirement
// should result in aborting the rendezvous.
BASE_FEATURE(kMachPortRendezvousEnforcePeerRequirements,
             base::FEATURE_DISABLED_BY_DEFAULT);

namespace {

// The name to use in the bootstrap server, formatted with the BaseBundleID and
// PID of the server.
constexpr char kBootstrapNameFormat[] = "%s.MachPortRendezvousServer.%d";

// The size of the on-disk app-Info.plist with extra size added to account for
// size increases that occur during hydration. The extra size can safely be
// increased in the future.
constexpr size_t kMaxInfoPlistDataSize =
    BUILDFLAG(APP_INFO_PLIST_FILE_SIZE) + 1024;

// The state of the peer validation policy features is passed to child processes
// via this environment variable as Mach port rendezvous is performed before the
// feature list is initialized.
// TODO(crbug.com/362302761): Remove once enforcement is enabled by default.
constexpr char kPeerValidationPolicyEnvironmentVariable[] =
    "MACH_PORT_RENDEZVOUS_PEER_VALDATION";

MachPortRendezvousPeerValidationPolicy GetPeerValidationPolicy();

bool ShouldValidateProcessRequirements() {
  return GetPeerValidationPolicy() !=
         MachPortRendezvousPeerValidationPolicy::kNoValidation;
}

bool ShouldEnforceProcessRequirements() {
  return GetPeerValidationPolicy() ==
         MachPortRendezvousPeerValidationPolicy::kEnforce;
}

}  // namespace

struct MachPortRendezvousServerMac::ClientData {
  ClientData();
  ClientData(ClientData&&);
  ~ClientData();

  // A DISPATCH_SOURCE_TYPE_PROC / DISPATCH_PROC_EXIT dispatch source. When
  // the source is triggered, it calls OnClientExited().
  apple::ScopedDispatchObject<dispatch_source_t> exit_watcher;

  MachPortsForRendezvous ports;
  std::optional<mac::ProcessRequirement> requirement;
};

// static
MachPortRendezvousServerMac* MachPortRendezvousServerMac::GetInstance() {
  static auto* instance = new MachPortRendezvousServerMac();
  return instance;
}

// static
void MachPortRendezvousServerMac::AddFeatureStateToEnvironment(
    EnvironmentMap& environment) {
  environment.insert(
      {kPeerValidationPolicyEnvironmentVariable,
       NumberToString(static_cast<int>(GetPeerValidationPolicy()))});
}

MachPortRendezvousServerMac::ClientData&
MachPortRendezvousServerMac::ClientDataForPid(pid_t pid) {
  lock_.AssertAcquired();

  auto [it, inserted] = client_data_.try_emplace(pid);
  if (inserted) {
    apple::ScopedDispatchObject<dispatch_source_t> exit_watcher(
        dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC,
                               static_cast<uintptr_t>(pid), DISPATCH_PROC_EXIT,
                               dispatch_source_->Queue()));
    dispatch_source_set_event_handler(exit_watcher.get(), ^{
      OnClientExited(pid);
    });
    dispatch_resume(exit_watcher.get());
    it->second.exit_watcher = std::move(exit_watcher);
  }

  return it->second;
}

void MachPortRendezvousServerMac::RegisterPortsForPid(
    pid_t pid,
    const MachPortsForRendezvous& ports) {
  lock_.AssertAcquired();
  DCHECK_LT(ports.size(), internal::kMaximumRendezvousPorts);
  DCHECK(!ports.empty());

  ClientData& client = ClientDataForPid(pid);
  CHECK(client.ports.empty());
  client.ports = ports;
}

void MachPortRendezvousServerMac::SetProcessRequirementForPid(
    pid_t pid,
    mac::ProcessRequirement requirement) {
  lock_.AssertAcquired();

  ClientData& client = ClientDataForPid(pid);
  CHECK(!client.requirement.has_value());
  client.requirement = std::move(requirement);
}

MachPortRendezvousServerMac::ClientData::ClientData() = default;
MachPortRendezvousServerMac::ClientData::ClientData(ClientData&&) = default;

MachPortRendezvousServerMac::ClientData::~ClientData() {
  for (auto& pair : ports) {
    pair.second.Destroy();
  }
}

MachPortRendezvousServerMac::MachPortRendezvousServerMac() {
  std::string bootstrap_name =
      StringPrintf(kBootstrapNameFormat, apple::BaseBundleID(), getpid());
  kern_return_t kr = bootstrap_check_in(
      bootstrap_port, bootstrap_name.c_str(),
      apple::ScopedMachReceiveRight::Receiver(server_port_).get());
  BOOTSTRAP_CHECK(kr == KERN_SUCCESS, kr)
      << "bootstrap_check_in " << bootstrap_name;
  dispatch_source_ = std::make_unique<apple::DispatchSource>(
      bootstrap_name.c_str(), server_port_.get(), ^{
        HandleRequest();
      });
  dispatch_source_->Resume();
}

MachPortRendezvousServerMac::~MachPortRendezvousServerMac() = default;

void MachPortRendezvousServerMac::ClearClientDataForTesting() {
  client_data_.clear();
}

std::optional<MachPortsForRendezvous>
MachPortRendezvousServerMac::PortsForClient(audit_token_t audit_token) {
  pid_t pid = audit_token_to_pid(audit_token);
  std::optional<mac::ProcessRequirement> requirement;
  MachPortsForRendezvous ports_to_send;

  {
    AutoLock lock(lock_);
    auto it = client_data_.find(pid);
    if (it != client_data_.end()) {
      ports_to_send = std::move(it->second.ports);
      requirement = std::move(it->second.requirement);
      client_data_.erase(it);
    }
  }

  if (requirement.has_value() && ShouldValidateProcessRequirements()) {
    bool client_is_valid = requirement->ValidateProcess(audit_token);
    if (!client_is_valid && ShouldEnforceProcessRequirements()) {
      return std::nullopt;
    }
  }

  return ports_to_send;
}

void MachPortRendezvousServerMac::OnClientExited(pid_t pid) {
  AutoLock lock(lock_);
  client_data_.erase(pid);
}

bool MachPortRendezvousServerMac::IsValidAdditionalMessageId(
    mach_msg_id_t request) const {
  return request == internal::kMachRendezvousMsgIdRequestWithInfoPlistData;
}

std::vector<uint8_t> MachPortRendezvousServerMac::AdditionalDataForReply(
    mach_msg_id_t request) const {
  if (request == internal::kMachRendezvousMsgIdRequestWithInfoPlistData) {
    std::vector<uint8_t> info_plist_data =
        mac::OuterBundleCachedInfoPlistData();
    if (info_plist_data.size() > kMaxInfoPlistDataSize) {
      LOG(WARNING) << "Info.plist data too large to send to client.";
      return {};
    }
    return info_plist_data;
  }
  return {};
}

// static
MachPortRendezvousClient* MachPortRendezvousClient::GetInstance() {
  static MachPortRendezvousClientMac* client = []() -> auto* {
    auto* client = new MachPortRendezvousClientMac();
    if (!client->AcquirePorts()) {
      delete client;
      client = nullptr;
    }
    return client;
  }();
  return client;
}

// static
std::string MachPortRendezvousClientMac::GetBootstrapName() {
  return StringPrintf(kBootstrapNameFormat, apple::BaseBundleID(), getppid());
}

MachPortRendezvousClientMac::MachPortRendezvousClientMac()
    : server_requirement_(TakeServerCodeSigningRequirement()) {}

MachPortRendezvousClientMac::~MachPortRendezvousClientMac() = default;

bool MachPortRendezvousClientMac::AcquirePorts() {
  AutoLock lock(lock_);

  apple::ScopedMachSendRight server_port;
  std::string bootstrap_name = GetBootstrapName();
  kern_return_t kr = bootstrap_look_up(
      bootstrap_port, const_cast<char*>(bootstrap_name.c_str()),
      apple::ScopedMachSendRight::Receiver(server_port).get());
  if (kr != KERN_SUCCESS) {
    BOOTSTRAP_LOG(ERROR, kr) << "bootstrap_look_up " << bootstrap_name;
    return false;
  }

  mach_msg_id_t message_id = internal::kMachRendezvousMsgIdRequest;
  size_t additional_data_size = 0;
  if (NeedsInfoPlistData()) {
    message_id = internal::kMachRendezvousMsgIdRequestWithInfoPlistData;
    additional_data_size = kMaxInfoPlistDataSize;
  }
  return SendRequest(std::move(server_port), message_id, additional_data_size);
}

bool MachPortRendezvousClientMac::ValidateMessage(
    mach_msg_base_t* message,
    BufferIterator<uint8_t> iterator) {
  if (!server_requirement_.has_value() ||
      !ShouldValidateProcessRequirements()) {
    return true;
  }

  span<const uint8_t> info_plist_data;
  if (NeedsInfoPlistData()) {
    // Skip over the Mach ports to find the Info.plist data to use for
    // validation.
    iterator.Seek(iterator.position() +
                  message->body.msgh_descriptor_count *
                      (sizeof(mach_msg_port_descriptor_t) +
                       sizeof(MachPortsForRendezvous::key_type)));
    auto info_plist_length = iterator.CopyObject<uint64_t>();
    CHECK(info_plist_length.has_value());
    CHECK(*info_plist_length <= kMaxInfoPlistDataSize);
    info_plist_data = iterator.Span<uint8_t>(info_plist_length.value());
  }

  iterator.Seek(message->header.msgh_size);
  auto* trailer = iterator.Object<mach_msg_audit_trailer_t>();
  bool valid = server_requirement_->ValidateProcess(trailer->msgh_audit,
                                                    info_plist_data);
  if (ShouldEnforceProcessRequirements()) {
    return valid;
  }
  return true;
}

bool MachPortRendezvousClientMac::NeedsInfoPlistData() const {
  return ShouldValidateProcessRequirements() &&
         server_requirement_.has_value() &&
         server_requirement_->ShouldCheckDynamicValidityOnly();
}

namespace {

struct RequirementWithLock {
  Lock lock;
  std::optional<mac::ProcessRequirement> requirement;
};

RequirementWithLock& ServerCodeSigningRequirementWithLock() {
  static NoDestructor<RequirementWithLock> requirement_with_lock;
  return *requirement_with_lock;
}

}  // namespace

// static
void MachPortRendezvousClientMac::SetServerProcessRequirement(
    mac::ProcessRequirement requirement) {
  AutoLock lock(ServerCodeSigningRequirementWithLock().lock);
  ServerCodeSigningRequirementWithLock().requirement = requirement;
}

// static
std::optional<mac::ProcessRequirement>
MachPortRendezvousClientMac::TakeServerCodeSigningRequirement() {
  AutoLock lock(ServerCodeSigningRequirementWithLock().lock);
  return std::move(ServerCodeSigningRequirementWithLock().requirement);
}

// static
MachPortRendezvousPeerValidationPolicy
MachPortRendezvousClientMac::PeerValidationPolicyForTesting() {
  return GetPeerValidationPolicy();
}

namespace {

// Helper function to avoid the compiler detecting that comparisons involving
// `default_state` are compile-time constants and declaring code as unreachable.
bool IsEnabledByDefault(const Feature& feature) {
  return feature.default_state == FEATURE_ENABLED_BY_DEFAULT;
}

MachPortRendezvousPeerValidationPolicy GetDefaultPeerValidationPolicy() {
  CHECK(!FeatureList::GetInstance());
  if (IsEnabledByDefault(kMachPortRendezvousEnforcePeerRequirements)) {
    return MachPortRendezvousPeerValidationPolicy::kEnforce;
  }
  if (IsEnabledByDefault(kMachPortRendezvousValidatePeerRequirements)) {
    return MachPortRendezvousPeerValidationPolicy::kValidateOnly;
  }
  return MachPortRendezvousPeerValidationPolicy::kNoValidation;
}

MachPortRendezvousPeerValidationPolicy
GetPeerValidationPolicyFromFeatureList() {
  if (base::FeatureList::IsEnabled(
          kMachPortRendezvousEnforcePeerRequirements)) {
    return MachPortRendezvousPeerValidationPolicy::kEnforce;
  }
  if (base::FeatureList::IsEnabled(
          kMachPortRendezvousValidatePeerRequirements)) {
    return MachPortRendezvousPeerValidationPolicy::kValidateOnly;
  }
  return MachPortRendezvousPeerValidationPolicy::kNoValidation;
}

MachPortRendezvousPeerValidationPolicy
GetPeerValidationPolicyFromEnvironment() {
  // The environment variable is set at launch and does not change. Compute the
  // policy once and cache it.
  static MachPortRendezvousPeerValidationPolicy policy = [] {
    std::unique_ptr<Environment> environment = Environment::Create();
    int policy_int = INT_MAX;
    std::optional<std::string> policy_str =
        environment->GetVar(kPeerValidationPolicyEnvironmentVariable);
    if (policy_str.has_value()) {
      if (!StringToInt(policy_str.value(), &policy_int)) {
        // StringToInt modifies the output value even on failure.
        policy_int = INT_MAX;
      }
    }

    switch (policy_int) {
      case std::to_underlying(
          MachPortRendezvousPeerValidationPolicy::kNoValidation):
        return MachPortRendezvousPeerValidationPolicy::kNoValidation;
      case std::to_underlying(
          MachPortRendezvousPeerValidationPolicy::kValidateOnly):
        return MachPortRendezvousPeerValidationPolicy::kValidateOnly;
      case std::to_underlying(MachPortRendezvousPeerValidationPolicy::kEnforce):
        return MachPortRendezvousPeerValidationPolicy::kEnforce;
      default:
        // An invalid policy or no policy was passed via the environment. Fall
        // back to the default values of the feature flags.
        return GetDefaultPeerValidationPolicy();
    }
  }();
  return policy;
}

MachPortRendezvousPeerValidationPolicy GetPeerValidationPolicy() {
  if (base::FeatureList::GetInstance()) {
    return GetPeerValidationPolicyFromFeatureList();
  }

  // In child processes, MachPortRendezvousClient is used during feature list
  // initialization so the validation policy is passed via an environment
  // variable.
  return GetPeerValidationPolicyFromEnvironment();
}

}  // namespace

}  // namespace base
