chromium/chrome/browser/mac/code_sign_clone_manager.mm

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

#include "chrome/browser/mac/code_sign_clone_manager.h"

#import <Foundation/Foundation.h>
#include <errno.h>
#include <fcntl.h>
#include <paths.h>
#include <stdint.h>
#include <sys/attr.h>
#include <sys/clonefile.h>
#include <sys/fcntl.h>
#include <sys/ioccom.h>
#include <sys/stat.h>
#include <sys/syslimits.h>
#include <unistd.h>

#include <iomanip>
#include <string>
#include <string_view>
#include <vector>

#include "base/apple/foundation_util.h"
#include "base/containers/fixed_flat_set.h"
#include "base/feature_list.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/mac/mac_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/path_service.h"
#include "base/posix/eintr_wrapper.h"
#include "base/process/launch.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_switches.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/common/content_paths.h"
#include "content/public/common/content_switches.h"

//
// Sourced from Libc-1592.100.35 (macOS 14.5)
// https://github.com/apple-oss-distributions/Libc/blob/Libc-1592.100.35/gen/confstr.c#L78
//
// `DIRHELPER_USER_LOCAL_TRANSLOCATION` is not public and its name has been made
// up here. Support for the value `DIRHELPER_USER_LOCAL_TRANSLOCATION`
// represents was added in macOS 11.
//
typedef enum {
  DIRHELPER_USER_LOCAL = 0,            // "0/"
  DIRHELPER_USER_LOCAL_TEMP,           // "T/"
  DIRHELPER_USER_LOCAL_CACHE,          // "C/"
  DIRHELPER_USER_LOCAL_TRANSLOCATION,  // "X/"
  DIRHELPER_USER_LOCAL_LAST = DIRHELPER_USER_LOCAL_TRANSLOCATION
} dirhelper_which_t;

//
// Sourced from Libsystem-1345.120.2 (macOS 14.5)
// https://github.com/apple-oss-distributions/Libsystem/blob/Libsystem-1345.120.2/init.c#L125
//
// Tested on macOS 10.15+. If an unsupported `which` is provided, NULL is
// returned.
// If successful, the requested directory path will be returned. The directory
// will be created if it does not exist.
//
// When `DIRHELPER_USER_LOCAL_TRANSLOCATION` is provided and the calling process
// is sandboxed, `_dirhelper` will return NULL and the directory will not be
// created.
//
extern "C" char* _dirhelper(dirhelper_which_t which,
                            char* buffer,
                            size_t buffer_length);

namespace {

constexpr char kContentsMacOS[] = "Contents/MacOS";
constexpr char kContentsInfoPlist[] = "Contents/Info.plist";
constexpr char kCodeSignClone[] = "code_sign_clone";
constexpr int kMkdtempFormatXCount = 6;

NSString* g_temp_dir_for_testing = nil;
NSString* g_dirhelper_path_for_testing = nil;

// Removes the quarantine attribute, if any. Removal is best effort.
void RemoveQuarantineAttribute(const base::FilePath& path) {
  if (!base::mac::RemoveQuarantineAttribute(path)) {
    DLOG(ERROR) << "error removing quarantine attribute "
                << std::quoted(path.value());
  }
}

bool ValidateTempDir(const base::FilePath& path) {
  if (!base::MakeAbsoluteFilePath(path).value().starts_with(
          "/private/var/folders/")) {
    DLOG(ERROR) << "failed to validate temporary dir "
                << std::quoted(path.value());
    return false;
  }
  return true;
}

//
// Get a temporary directory that is cleaned on machine boot but not
// periodically. `DIRHELPER_USER_LOCAL_TRANSLOCATION` and `Cleanup At Startup`
// are the only found directories that have this behavior. Use
// `DIRHELPER_USER_LOCAL_TRANSLOCATION` as it can be obtained through an API,
// albeit a private one.
//
// Returns true if a suitable temporary directory path is found. Returns false
// otherwise.
//
// Here are some notes about the various temporary directory options.
//
// `/tmp` (`/private/tmp`) is cleaned on machine boot. Additionally, files that
//   have a birth and access time older than three days are deleted. This is
//   handled by `/usr/libexec/tmp_cleaner`, which is run by the
//   `com.apple.tmp_cleaner` launch daemon.
//
// `/var/tmp` (`/private/var/tmp`) is not cleaned on machine boot or
//   periodically.
//
// `DIRHELPER_USER_LOCAL` (`/var/folders/.../0`) is not cleaned on machine boot
//   or periodically.
//
// `DIRHELPER_USER_LOCAL_TEMP` (`/var/folders/.../T`) is cleaned on machine
//   boot. Additionally, files that have a birth and access time older than
//   three days are deleted. This is handled by `/usr/libexec/dirhelper`, which
//   is run by the `com.apple.bsd.dirhelper` launch daemon. Recent versions of
//   `dirhelper` are not open source but here is the last open version (10.9.2,
//   2014-02-25) for reference if this assumption needs to be revisited.
//   https://github.com/apple-oss-distributions/system_cmds/blob/system_cmds-597.90.1/dirhelper.tproj/dirhelper.c
//
// `DIRHELPER_USER_LOCAL_CACHE` (`/var/folders/.../C`) is not cleaned on machine
//   boot or periodically.
//
// `DIRHELPER_USER_LOCAL_TRANSLOCATION` (`/var/folders/.../X`) is cleaned on
//   machine boot and not otherwise cleaned periodically, but is only available
//   on macOS 11 and later through a private interface.
//
// `Cleanup At Startup` (`/var/folders/.../Cleanup At Startup`) is cleaned on
//   machine boot and not otherwise cleaned periodically, but its path is not
//   available through any known interface.
//
// Note: APFS `access_time` is not updated when the file is read, unless its
// value is prior to the timestamp stored in the `mod_time` field. This is
// applicable to the periodic cleaning that happens in `/tmp` and
// `DIRHELPER_USER_LOCAL_TEMP`. The optional feature flag
// `APFS_FEATURE_STRICTATIME` (the `strictatime` mount option, see `man 8
// mount`) can be set to update `access_time` each time the file is read,
// however the flag is not enabled by default.
// https://developer.apple.com/support/downloads/Apple-File-System-Reference.pdf#page=67
//
bool GetCleanupOnBootTempDir(base::FilePath* path) {
  if (g_temp_dir_for_testing) {
    *path = base::apple::NSStringToFilePath(g_temp_dir_for_testing);
    return true;
  }

  char buffer[PATH_MAX];
  if (!g_dirhelper_path_for_testing &&
      !_dirhelper(DIRHELPER_USER_LOCAL_TRANSLOCATION, buffer, PATH_MAX)) {
    DLOG(ERROR) << "_dirhelper error";
    return false;
  }

  // /var/folders/.../X/
  NSString* temp_dir = g_dirhelper_path_for_testing ?: @(buffer);

  // `_dirhelper` with `DIRHELPER_USER_LOCAL_TRANSLOCATION` shouldn't return
  // any user controlled paths, but validate just to be sure.
  if (!ValidateTempDir(base::apple::NSStringToFilePath(temp_dir))) {
    return false;
  }

  base::FilePath temporary_directory_path =
      base::apple::NSStringToFilePath(temp_dir);

  // `DIRHELPER_USER_LOCAL_TRANSLOCATION` created by `_dirhelper`, from the
  // browser process, will be stamped with a quarantine attribute. Attempt to
  // remove it.
  RemoveQuarantineAttribute(temporary_directory_path);

  *path = temporary_directory_path;
  return true;
}

// Returns the "type" argument identifying a code sign clone cleanup process
// ("--type=code-sign-clone-cleanup").
std::string CodeSignCloneCleanupTypeArg() {
  return base::StringPrintf("--%s=%s", switches::kProcessType,
                            switches::kCodeSignCloneCleanupProcess);
}

// Returns the argument for the unique suffix of the temporary directory. The
// full path will be reconstructed and validated by the helper process.
// ("--unique-temp-dir-suffix=tKdILk").
std::string UniqueTempDirSuffixArg(const std::string& unique_temp_dir_suffix) {
  return base::StringPrintf("--%s=%s", switches::kUniqueTempDirSuffix,
                            unique_temp_dir_suffix.c_str());
}

// Example:
//   /private/var/folders/.../X/org.chromium.Chromium.code_sign_clone
bool GetCloneTempDir(base::FilePath* path) {
  base::FilePath temp_dir;
  if (!GetCleanupOnBootTempDir(&temp_dir)) {
    return false;
  }
  std::string_view prefix = base::apple::BaseBundleID();
  *path = base::MakeAbsoluteFilePath(temp_dir).Append(
      base::StrCat({prefix, ".", kCodeSignClone}));
  return true;
}

// Example:
//   /private/var/folders/.../X/org.chromium.Chromium.code_sign_clone/code_sign_clone.tKdILk
bool CreateUniqueCloneTempDir(base::FilePath* path) {
  base::FilePath clone_temp_dir;
  if (!GetCloneTempDir(&clone_temp_dir)) {
    return false;
  }

  // 0700 was chosen intentionally to avoid giving away filesystem access more
  // broadly to something that might be private and protected.
  if (mkdir(clone_temp_dir.value().c_str(), 0700) != 0 && errno != EEXIST) {
    DPLOG(ERROR) << "mkdir " << std::quoted(clone_temp_dir.value());
    return false;
  }
  RemoveQuarantineAttribute(clone_temp_dir);

  // Example:
  //   code_sign_clone.XXXXXX
  std::string format = base::StrCat(
      {kCodeSignClone, ".", std::string(kMkdtempFormatXCount, 'X')});

  base::FilePath unique_format = clone_temp_dir.Append(format);
  char* buffer = const_cast<char*>(unique_format.value().c_str());
  if (!mkdtemp(buffer)) {
    DPLOG(ERROR) << "mkdtemp " << std::quoted(buffer);
    return false;
  }
  base::FilePath unique_path = base::FilePath(buffer);
  RemoveQuarantineAttribute(unique_path);

  *path = unique_path;
  return true;
}

// Example suffix:
//   tKdILk
// Example return value:
//   /private/var/folders/.../X/org.chromium.Chromium.code_sign_clone/code_sign_clone.tKdILk
// May return an empty path if the constructed path does not resolve.
base::FilePath GetAbsoluteUniqueCloneTempDirForSuffix(
    const std::string& suffix) {
  base::FilePath clone_temp_dir;
  if (!GetCloneTempDir(&clone_temp_dir)) {
    return base::FilePath();
  }
  return base::MakeAbsoluteFilePath(
      clone_temp_dir.Append(base::StrCat({kCodeSignClone, ".", suffix})));
}

void RecordHardLinkError(int error) {
  base::UmaHistogramSparse("Mac.AppHardLinkError", error);
}

// Unlink the destination main executable and replace it with a hard link to
// source main executable.
bool HardLinkMainExecutable(const base::FilePath& source_path,
                            const base::FilePath& destination_path,
                            const base::FilePath& main_executable_name) {
  base::FilePath destination_main_executable_path =
      destination_path.Append(kContentsMacOS).Append(main_executable_name);
  if (unlink(destination_main_executable_path.value().c_str()) != 0 &&
      errno != ENOENT) {
    DPLOG(ERROR) << "unlink "
                 << std::quoted(destination_main_executable_path.value());
    return false;
  }
  base::FilePath source_main_executable_path =
      source_path.Append(kContentsMacOS).Append(main_executable_name);
  if (link(source_main_executable_path.value().c_str(),
           destination_main_executable_path.value().c_str()) != 0) {
    RecordHardLinkError(errno);
    DPLOG(ERROR) << "link " << std::quoted(source_main_executable_path.value())
                 << ", "
                 << std::quoted(destination_main_executable_path.value());
    return false;
  }
  RecordHardLinkError(0);
  return true;
}

void RecordClonefileError(int error) {
  base::UmaHistogramSparse("Mac.AppClonefileError", error);
}

// Copy-on-write clones `source_path` to `destination_path`. The `source_path`
// main executable is then hard linked within the the corresponding
// `destination_path` directory.
bool CloneApp(const base::FilePath& source_path,
              const base::FilePath& destination_path,
              const base::FilePath& main_executable_name) {
  // Clone the entire app.
  // `CLONEFILE(2)` strongly discourages using `clonefile` to clone directories.
  // It suggests using `copyfile` instead. When cloning M125 Chrome on an M1 Max
  // Mac `copyfile` is much slower than `clonefile` (~70ms vs. ~10ms).
  // We are ignoring the warning because of the speed gains with `clonefile`.
  // A feedback has been opened with Apple asking about this warning.
  // FB13814551: clonefile directories
  if (clonefile(source_path.value().c_str(), destination_path.value().c_str(),
                0) != 0) {
    RecordClonefileError(errno);
    DPLOG(ERROR) << "clonefile " << std::quoted(source_path.value()) << ", "
                 << std::quoted(destination_path.value());
    return false;
  }
  RecordClonefileError(0);

  // The top top level directory created by `clonefile` has the quarantine
  // attribute set. The rest of the directory tree does not have the attribute
  // set.
  RemoveQuarantineAttribute(destination_path);

  // Hard link the main executable.
  if (!HardLinkMainExecutable(source_path, destination_path,
                              main_executable_name)) {
    return false;
  }
  return true;
}

// Launch the code-sign-clone-cleanup helper process passing the unique suffix
// of the temporary directory as an argument. The full path will be
// reconstructed and validated in the helper process.
void DeleteUniqueTempDirRecursivelyFromHelperProcess(
    const std::string& unique_suffix) {
  base::FilePath child_path;
  if (!base::PathService::Get(content::CHILD_PROCESS_EXE, &child_path)) {
    DLOG(ERROR) << "No CHILD_PROCESS_EXE";
    return;
  }

  std::vector<std::string> code_sign_clone_cleanup_args{
      child_path.value(),
      CodeSignCloneCleanupTypeArg(),
      UniqueTempDirSuffixArg(unique_suffix),
  };

  // The child helper process should outlive its parent.
  base::LaunchOptions options;
  options.new_process_group = true;

  // Null out stdout and stderr to prevent unexpected output after the browser
  // has exited if launching from a terminal.
  // `base::LaunchProcess` maps stdin to /dev/null.
  base::ScopedFD null_fd(HANDLE_EINTR(open(_PATH_DEVNULL, O_WRONLY)));
  if (!null_fd.is_valid()) {
    DPLOG(ERROR) << "open " << std::quoted(_PATH_DEVNULL);
    return;
  }
  options.fds_to_remap.emplace_back(null_fd.get(), STDOUT_FILENO);
  options.fds_to_remap.emplace_back(null_fd.get(), STDERR_FILENO);

  if (!base::LaunchProcess(code_sign_clone_cleanup_args, options).IsValid()) {
    DLOG(ERROR) << "base::LaunchProcess failed";
    return;
  }
}

// Example of an expected unique_temp_dir_suffix: tKdILk
bool ValidateUniqueDirSuffix(const std::string& unique_temp_dir_suffix) {
  //
  // mkdtemp(XXXXXX) possible values.
  //
  // Quote from:
  // https://pubs.opengroup.org/onlinepubs/9699919799/functions/mkdtemp.html
  //
  // "The mkdtemp() function shall modify the contents of template by replacing
  // six or more 'X' characters at the end of the pathname with the same number
  // of characters from the portable filename character set."
  //
  // The portable filename character set:
  // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282
  //
  static constexpr auto kFormat = base::MakeFixedFlatSet<const char>({
      'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
      'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
      'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
      'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
      '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '_', '-',
  });
  if (unique_temp_dir_suffix.length() != kMkdtempFormatXCount) {
    return false;
  }
  for (const char& c : unique_temp_dir_suffix) {
    if (!kFormat.contains(c)) {
      return false;
    }
  }
  return true;
}

// Example of an expected unique_temp_dir_path:
//   /private/var/folders/.../X/org.chromium.Chromium.code_sign_clone/code_sign_clone.tKdILk
// Make sure the path is within GetCloneTempDir() and has a valid prefix.
bool ValidateUniqueTempDirPath(const base::FilePath& unique_temp_dir_path) {
  base::FilePath clone_temp_dir;
  if (!GetCloneTempDir(&clone_temp_dir)) {
    return false;
  }
  base::FilePath prefix =
      clone_temp_dir.Append(base::StrCat({kCodeSignClone, "."}));
  return unique_temp_dir_path.value().starts_with(prefix.value());
}

void RecordCloneCount() {
  base::FilePath clone_temp_dir;
  if (!GetCloneTempDir(&clone_temp_dir)) {
    return;
  }

  struct attrlist attr_list = {
      // `man 2 getattrlist` explains `ATTR_BIT_MAP_COUNT` must be set.
      .bitmapcount = ATTR_BIT_MAP_COUNT,

      // Get the entry count of the provided dir. The "." and ".." entries are
      // not included in the count.
      .dirattr = ATTR_DIR_ENTRYCOUNT,
  };

  struct alignas(4) {
    uint32_t length;
    uint32_t entry_count;
  } __attribute__((packed)) attr_buff;

  //
  // Count the number of entries in the clone temp dir. The count would be 2 in
  // this example:
  //  /private/var/folders/.../X/org.chromium.Chromium.code_sign_clone/
  //    code_sign_clone.123456
  //    code_sign_clone.654321
  //
  if (getattrlist(clone_temp_dir.value().c_str(), &attr_list, &attr_buff,
                  sizeof(attr_buff), 0) != 0) {
    return;
  }
  DCHECK_GE(sizeof(attr_buff), attr_buff.length);

  // Record the clone count. Each running instance of Chrome maintains a clone
  // of itself. Only a handful (~1-5) of in use clones are expected to be
  // present at a given time. We don't need granularity over 100. A high count
  // indicates a more robust cleanup approach is needed.
  base::UmaHistogramCounts100("Mac.AppCodeSignCloneCount",
                              attr_buff.entry_count);
}

// Don't renumber these values. They are recorded in UMA metrics.
// See enum MacCloneExists in enums.xml.
enum class MacCloneExists {
  kExists = 0,
  kMissingMainExecutable = 1,
  kMissingInfoPlist = 2,
  kMissingMainExecutableAndInfoPlist = 3,
  kMaxValue = kMissingMainExecutableAndInfoPlist,
};

MacCloneExists CloneExists(const base::FilePath& clone_app_path,
                           const base::FilePath& main_executable_name) {
  // Check for the existence of both the main executable and the Info.plist,
  // both are needed for dynamic validation. We have observed that during
  // cleanup, `dirhelper` does not remove hard links. The main executable is a
  // hard link while the Info.plist is a non-linked regular file. Checking both
  // for existence provides a more accurate existence metric.
  base::FilePath main_executable_path =
      clone_app_path.Append(kContentsMacOS).Append(main_executable_name);
  base::FilePath info_plist_path = clone_app_path.Append(kContentsInfoPlist);
  bool main_executable_exists = base::PathExists(main_executable_path);
  bool info_plist_exists = base::PathExists(info_plist_path);
  if (main_executable_exists && info_plist_exists) {
    return MacCloneExists::kExists;
  } else if (!main_executable_exists && info_plist_exists) {
    return MacCloneExists::kMissingMainExecutable;
  } else if (main_executable_exists && !info_plist_exists) {
    return MacCloneExists::kMissingInfoPlist;
  } else if (!main_executable_exists && !info_plist_exists) {
    return MacCloneExists::kMissingMainExecutableAndInfoPlist;
  } else {
    NOTREACHED();
  }
}

void RecordCloneExists(MacCloneExists exists) {
  base::UmaHistogramEnumeration("Mac.AppCodeSignCloneExists", exists);
}

}  // namespace

namespace code_sign_clone_manager {

BASE_FEATURE(kMacAppCodeSignClone,
             "MacAppCodeSignClone",
             base::FEATURE_DISABLED_BY_DEFAULT);

CodeSignCloneManager::CodeSignCloneManager(
    const base::FilePath& src_path,
    const base::FilePath& main_executable_name,
    CloneCallback callback)
    : task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
          {base::MayBlock(), base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) {
  if (!base::FeatureList::IsEnabled(kMacAppCodeSignClone) || src_path.empty() ||
      main_executable_name.empty()) {
    return;
  }

  // Post a background task to perform the clone. If the task has not yet
  // started and Chrome is shutdown, the `SKIP_ON_SHUTDOWN` behavior will drop
  // the task. This is okay. If Chrome is shutting down, there is no need for a
  // code-sign-clone. If the task does not run, `needs_cleanup_` will be `false`
  // which will stop `~CodeSignCloneManager` from unnecessarily launching the
  // cleanup helper. If the task does run, it is guaranteed to complete before
  // `ThreadPoolInstance::Shutdown` returns, which is run before
  // `~CodeSignCloneManager`. It is safe to read `needs_cleanup_` from
  // `~CodeSignCloneManager` without any explicit synchronization. Usage of
  // `base::Unretained(this)` is also safe here for the same reason.
  task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&CodeSignCloneManager::Clone, base::Unretained(this),
                     src_path, main_executable_name, std::move(callback)));
}

CodeSignCloneManager::~CodeSignCloneManager() {
  if (!needs_cleanup_) {
    return;
  }

  // Unlinking M125 takes ~20ms on an M1 Max Mac. When this destructor is
  // called, Chrome is in the process of shutting down and new background tasks
  // can not be posted. Instead of blocking, perform the unlinking from a child
  // helper process.
  DeleteUniqueTempDirRecursivelyFromHelperProcess(unique_temp_dir_suffix_);
}

void CodeSignCloneManager::Clone(const base::FilePath& src_path,
                                 const base::FilePath& main_executable_name,
                                 CloneCallback callback) {
  base::TimeTicks start_time = base::TimeTicks::Now();

  // Intentionally avoiding `base::ScopedTempDir()`. The temp dir is
  // expected to exist beyond the lifetime of this process. The temp dir
  // will be deleted by the clone-cleanup helper process after the browser
  // exits.
  //
  // Also intentionally avoiding `base::GetTempDir()` which uses the
  // `MAC_CHROMIUM_TMPDIR` environment variable (if set). Since
  // `unique_temp_dir_path` will be reconstructed in the clone-cleanup helper
  // process, external control over the `MAC_CHROMIUM_TMPDIR` and the
  // `unique_temp_dir_suffix_` argument could result in misuse. We want
  // to prevent the helper process from being able to delete arbitrary
  // files.
  //
  // Example `unique_temp_dir_path`:
  //   /private/var/folders/.../X/org.chromium.Chromium.code_sign_clone/code_sign_clone.tKdILk
  //
  // The clone will be created inside of this directory.
  base::FilePath unique_temp_dir_path;
  if (!CreateUniqueCloneTempDir(&unique_temp_dir_path)) {
    std::move(callback).Run(base::FilePath());
    return;
  }

  // .tKdILk
  unique_temp_dir_suffix_ = unique_temp_dir_path.FinalExtension();

  // Trim the leading "."
  unique_temp_dir_suffix_.erase(unique_temp_dir_suffix_.begin());

  // `unique_temp_dir_suffix_` will be validated later during cleanup from the
  // helper process. To avoid leaking clones if validation fails, make sure it
  // passes validation here before continuing.
  if (!ValidateUniqueDirSuffix(unique_temp_dir_suffix_)) {
    DLOG(ERROR) << "ValidateUniqueDirSuffix() failed";
    return;
  }

  // Ignore any errors from creating the clone. There are many scenarios where
  // these operations could fail (different filesystems for the source and
  // destination, no clone filesystem support, read only disk, full disk,
  // etc.). If there is a failure, clean up any artifacts and allow Chrome to
  // keep running. Instances of Chrome in this situation will be susceptible to
  // code signature validation errors when an update is staged on disk. This is
  // being tracked via the Mac.AppUpgradeCodeSignatureValidationStatus metric.
  base::FilePath clone_app_path =
      unique_temp_dir_path.Append(src_path.BaseName());
  if (!CloneApp(src_path, clone_app_path, main_executable_name)) {
    base::DeletePathRecursively(unique_temp_dir_path);
    std::move(callback).Run(base::FilePath());
    return;
  }

  base::TimeDelta delta = base::TimeTicks::Now() - start_time;
  base::UmaHistogramTimes("Mac.AppCodeSignCloneCreationTime", delta);

  // Let `~CodeSignCloneManager` know it needs to clean up. `Clone` is run from
  // a posted task which is guaranteed to finish once it has started. It will
  // block shutdown until complete. `ThreadPoolInstance::Shutdown` is run before
  // `~CodeSignCloneManager`. `Clone` and ` ~CodeSignCloneManager` will never
  // overlap, it is safe to set `needs_cleanup_` from this task.
  needs_cleanup_ = true;

  // Record a baseline metric.
  RecordCloneExists(MacCloneExists::kExists);

  // Once the clone is created, start a timer that periodically checks for the
  // clone's existence. `base::RepeatingTimer` is not thread safe. It must be
  // created, started and stopped from the same thread / sequence.
  // `clone_exists_timer_` is created on the main thread, post a task to the
  // main thread to start the timer. The timer will be stopped during
  // `~CodeSignCloneManager` which also happens on the main thread.
  // `base::Unretained(this)` is safe here for the same reason `needs_cleanup_`
  // doesn't need synchronization.
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(&CodeSignCloneManager::StartCloneExistsTimer,
                                base::Unretained(this), clone_app_path,
                                main_executable_name));

  RecordCloneCount();

  // TODO(https://crbug.com/343784575): Search for inactive clones and clean
  // them up if the clone count gets too high.

  std::move(callback).Run(clone_app_path);
}

void CodeSignCloneManager::SetTemporaryDirectoryPathForTesting(
    const base::FilePath& path) {
  g_temp_dir_for_testing = base::apple::FilePathToNSString(path);
}

void CodeSignCloneManager::ClearTemporaryDirectoryPathForTesting() {
  g_temp_dir_for_testing = nil;
}

void CodeSignCloneManager::SetDirhelperPathForTesting(
    const base::FilePath& path) {
  g_dirhelper_path_for_testing = base::apple::FilePathToNSString(path);
}

void CodeSignCloneManager::ClearDirhelperPathForTesting() {
  g_dirhelper_path_for_testing = nil;
}

base::FilePath CodeSignCloneManager::GetCloneTemporaryDirectoryForTesting() {
  base::FilePath clone_temp_dir;
  GetCloneTempDir(&clone_temp_dir);
  return clone_temp_dir;
}

void CodeSignCloneManager::StartCloneExistsTimer(
    const base::FilePath& clone_app_path,
    const base::FilePath& main_executable_name) {
  // `base::Unretained(this)` is safe here because `~CodeSignCloneManager`
  // cancels the timer.
  clone_exists_timer_.Start(
      FROM_HERE, base::Days(1),
      base::BindRepeating(&CodeSignCloneManager::CloneExistsTimerFire,
                          base::Unretained(this), clone_app_path,
                          main_executable_name));
}

void CodeSignCloneManager::StopCloneExistsTimer() {
  clone_exists_timer_.Stop();
}

void CodeSignCloneManager::CloneExistsTimerFire(
    const base::FilePath& clone_app_path,
    const base::FilePath& main_executable_name) {
  // `CloneExists` may block, perform the work on a background thread.
  if (!task_runner_->RunsTasksInCurrentSequence()) {
    task_runner_->PostTask(
        FROM_HERE, base::BindOnce(&CodeSignCloneManager::CloneExistsTimerFire,
                                  base::Unretained(this), clone_app_path,
                                  main_executable_name));
    return;
  }

  MacCloneExists exists = CloneExists(clone_app_path, main_executable_name);

  // If the clone still exists, do nothing. Otherwise, record the state and stop
  // the timer.
  if (exists == MacCloneExists::kExists) {
    return;
  }
  RecordCloneExists(exists);
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(&CodeSignCloneManager::StopCloneExistsTimer,
                                base::Unretained(this)));
}

namespace internal {

// Main entry point for `--type=clone-cleanup` helper process.
// switches::kUniqueTempDirSuffix is expected; the full path will be
// reconstructed and validated. If switches::kWaitForParentExit exists the
// unique temporary directory will be deleted once the parent process exits.
int ChromeCodeSignCloneCleanupMain(
    content::MainFunctionParams main_parameters) {
  // Make sure the unique suffix is the correct format.
  std::string unique_temp_dir_suffix =
      main_parameters.command_line->GetSwitchValueASCII(
          switches::kUniqueTempDirSuffix);
  if (!ValidateUniqueDirSuffix(unique_temp_dir_suffix)) {
    DLOG(ERROR) << "ValidateUniqueDirSuffix() failed";
    return 1;
  }

  // Make sure the resolved path points to the expected location.
  base::FilePath unique_temp_dir_path =
      GetAbsoluteUniqueCloneTempDirForSuffix(unique_temp_dir_suffix);
  if (!ValidateUniqueTempDirPath(unique_temp_dir_path)) {
    DLOG(ERROR) << "ValidateUniqueTempDirPath() failed";
    return 1;
  }

  // The "--type=clone-cleanup" helper process is launched during from the
  // browser process during shutdown. Wait until the parent browser process dies
  // to ensure the clone is not being used.
  // There is no rush to clean up the temporary clone. Prefer polling the ppid
  // over more responsive but complex options.
  if (!main_parameters.command_line->HasSwitch(
          "no-wait-for-parent-exit-for-testing")) {
    while (getppid() != 1) {
      sleep(1);
    }
  }

  base::DeletePathRecursively(unique_temp_dir_path);
  return 0;
}

// `FSIOC_FD_ONLY_OPEN_ONCE` is not a part of the SDK. The definition was
// introduced in XNU 6153.11.26 (macOS 10.15). It may have existed earlier in
// another location, but for Chrome's purposes macOS 10.15+ is just fine.
// https://github.com/apple-oss-distributions/xnu/blob/xnu-6153.11.26/bsd/sys/fsctl.h#L327
#ifndef FSIOC_FD_ONLY_OPEN_ONCE
#define FSIOC_FD_ONLY_OPEN_ONCE _IOWR('A', 21, uint32_t)
#endif

FileOpenMoreThanOnce IsFileOpenMoreThanOnce(int file_descriptor) {
  uint32_t val;
  int result = ffsctl(file_descriptor, FSIOC_FD_ONLY_OPEN_ONCE, &val, 0);
  if (result == -1) {
    if (errno == EBUSY) {
      return FileOpenMoreThanOnce::kYes;
    }
    return FileOpenMoreThanOnce::kError;
  }
  return FileOpenMoreThanOnce::kNo;
}

FileOpenMoreThanOnce IsFileOpenMoreThanOnce(const base::FilePath& path) {
  base::ScopedFD fd(HANDLE_EINTR(open(path.value().c_str(), O_RDONLY)));
  if (fd == -1) {
    DPLOG(ERROR) << "open " << std::quoted(path.value());
    return FileOpenMoreThanOnce::kError;
  }
  return IsFileOpenMoreThanOnce(fd.get());
}

}  // namespace internal

}  // namespace code_sign_clone_manager