| // 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 "build/branding_buildflags.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_ENABLED_BY_DEFAULT); |
| |
| BASE_FEATURE(kMacAppCodeSignCloneRenameAsBundle, |
| "MacAppCodeSignCloneRenameAsBundle", |
| base::FEATURE_ENABLED_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; |
| } |
| // Chrome for Testing does not support auto-updates and |
| // this feature is specific to the update functionality, |
| // therefore, we disable this feature for Chrome for Testing. |
| // See crbug.com/379125944. |
| #if !BUILDFLAG(CHROME_FOR_TESTING) |
| // 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))); |
| #endif // !BUILDFLAG(CHROME_FOR_TESTING) |
| } |
| |
| 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; |
| } |
| |
| // Give the clone a ".bundle" extension. Launch Services aggressively tracks |
| // the existence of applications, and creating a duplicate copy of the |
| // Chromium app leads to trouble when it comes to Launch Services tracking the |
| // default browser (see https://crbug.com/381199182 for gory details). |
| // Labeling the clone a "bundle" is good enough to solve the problem of code |
| // signature validation, but avoids issues. |
| base::FilePath app_name = src_path.BaseName(); |
| if (base::FeatureList::IsEnabled(kMacAppCodeSignCloneRenameAsBundle)) { |
| app_name = app_name.AddExtension(".bundle"); |
| } |
| base::FilePath clone_app_path = unique_temp_dir_path.Append(app_name); |
| |
| // 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. |
| 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 |