// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "chrome/updater/util/mac_util.h"
#import <CoreFoundation/CoreFoundation.h>
#include <optional>
#include "base/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/command_line.h"
#include "base/files/file.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/mac/mac_util.h"
#include "base/process/launch.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/sys_string_conversions.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/version.h"
#include "chrome/updater/constants.h"
#include "chrome/updater/mac/setup/keystone.h"
#include "chrome/updater/registration_data.h"
#include "chrome/updater/updater_branding.h"
#include "chrome/updater/updater_scope.h"
#include "chrome/updater/updater_version.h"
#include "chrome/updater/util/posix_util.h"
#include "chrome/updater/util/util.h"
namespace updater {
namespace {
constexpr base::FilePath::CharType kZipExePath[] =
FILE_PATH_LITERAL("/usr/bin/unzip");
constexpr base::FilePath::CharType kGkToolPath[] =
FILE_PATH_LITERAL("/usr/bin/gktool");
base::FilePath ExecutableFolderPath() {
return base::FilePath(
base::StrCat({PRODUCT_FULLNAME_STRING, kExecutableSuffix, ".app"}))
.Append(FILE_PATH_LITERAL("Contents"))
.Append(FILE_PATH_LITERAL("MacOS"));
}
// Recursively remove quarantine attributes on the path. Emits a log message
// if it fails.
bool RemoveQuarantineAttributes(const base::FilePath& updater_bundle_path) {
bool success = base::mac::RemoveQuarantineAttribute(updater_bundle_path);
base::FileEnumerator file_enumerator(
base::FilePath(updater_bundle_path), true,
base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES |
base::FileEnumerator::SHOW_SYM_LINKS);
for (base::FilePath name = file_enumerator.Next(); !name.empty();
name = file_enumerator.Next()) {
success = base::mac::RemoveQuarantineAttribute(name) && success;
}
VLOG_IF(0, !success) << "Failed to remove quarantine attributes from "
<< updater_bundle_path;
return success;
}
// On supported versions of macOS, scan the specified bundle with Gatekeeper
// so it won't pop up a user-visible "Verifying..." box for the duration of
// the scan when an executable in the bundle is later launched for the first
// time. On unsupported macOS versions, this does nothing and returns 0.
//
// On supported macOS versions, this returns the return code from `gktool`.
// If attempting to launch `gktool` fails, this returns -1.
int PrewarmGatekeeperIfSupported(const base::FilePath& bundle_path) {
// gktool is only available on macOS 14 and later.
if (@available(macOS 14, *)) {
base::FilePath tool_path(kGkToolPath);
base::CommandLine command(tool_path);
command.AppendArg("scan");
command.AppendArg(bundle_path.value());
std::string output;
int exit_code = -1;
if (!base::GetAppOutputWithExitCode(command, &output, &exit_code)) {
VLOG(0) << "Something went wrong trying to run gktool from "
<< kGkToolPath;
return -1;
}
VLOG_IF(0, exit_code) << "gktool returned " << exit_code;
VLOG_IF(0, exit_code) << "gktool output: " << output;
return exit_code;
}
return 0;
}
} // namespace
std::string GetDomain(UpdaterScope scope) {
switch (scope) {
case UpdaterScope::kSystem:
return "system";
case UpdaterScope::kUser:
return base::StrCat({"gui/", base::NumberToString(geteuid())});
}
}
std::optional<base::FilePath> GetLibraryFolderPath(UpdaterScope scope) {
switch (scope) {
case UpdaterScope::kUser:
return base::apple::GetUserLibraryPath();
case UpdaterScope::kSystem: {
base::FilePath local_library_path;
if (!base::apple::GetLocalDirectory(NSLibraryDirectory,
&local_library_path)) {
VLOG(1) << "Could not get local library path";
return std::nullopt;
}
return local_library_path;
}
}
}
std::optional<base::FilePath> GetApplicationSupportDirectory(
UpdaterScope scope) {
base::FilePath path;
switch (scope) {
case UpdaterScope::kUser:
if (base::apple::GetUserDirectory(NSApplicationSupportDirectory, &path)) {
return path;
}
break;
case UpdaterScope::kSystem:
if (base::apple::GetLocalDirectory(NSApplicationSupportDirectory,
&path)) {
return path;
}
break;
}
VLOG(1) << "Could not get applications support path";
return std::nullopt;
}
std::optional<base::FilePath> GetKSAdminPath(UpdaterScope scope) {
const std::optional<base::FilePath> keystone_folder_path =
GetKeystoneFolderPath(scope);
if (!keystone_folder_path) {
return std::nullopt;
}
return std::make_optional(
keystone_folder_path->Append(FILE_PATH_LITERAL(KEYSTONE_NAME ".bundle"))
.Append(FILE_PATH_LITERAL("Contents"))
.Append(FILE_PATH_LITERAL("Helpers"))
.Append(FILE_PATH_LITERAL("ksadmin")));
}
std::string GetWakeLaunchdName(UpdaterScope scope) {
return IsSystemInstall(scope) ? MAC_BUNDLE_IDENTIFIER_STRING ".wake.system"
: MAC_BUNDLE_IDENTIFIER_STRING ".wake";
}
bool RemoveWakeJobFromLaunchd(UpdaterScope scope) {
const std::optional<base::FilePath> path = GetWakeTaskPlistPath(scope);
if (!path) {
return false;
}
// This may block while deleting the launchd plist file.
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
base::CommandLine command_line(base::FilePath("/bin/launchctl"));
command_line.AppendArg("bootout");
command_line.AppendArg(GetDomain(scope));
command_line.AppendArgPath(*path);
int exit_code = -1;
std::string output;
if (base::GetAppOutputWithExitCode(command_line, &output, &exit_code) &&
exit_code != 0) {
VLOG(2) << "launchctl bootout exited " << exit_code << ": " << output;
}
return base::DeleteFile(*path);
}
bool UnzipWithExe(const base::FilePath& src_path,
const base::FilePath& dest_path) {
base::FilePath file_path(kZipExePath);
base::CommandLine command(file_path);
command.AppendArg(src_path.value());
command.AppendArg("-d");
command.AppendArg(dest_path.value());
std::string output;
int exit_code = 0;
if (!base::GetAppOutputWithExitCode(command, &output, &exit_code)) {
VLOG(0) << "Something went wrong while running the unzipping with "
<< kZipExePath;
return false;
}
// Unzip utility having 0 is success and 1 is a warning.
if (exit_code > 1) {
VLOG(0) << "Output from unzipping: " << output;
VLOG(0) << "Exit code: " << exit_code;
}
return exit_code <= 1;
}
std::optional<base::FilePath> GetExecutableFolderPathForVersion(
UpdaterScope scope,
const base::Version& version) {
std::optional<base::FilePath> path =
GetVersionedInstallDirectory(scope, version);
if (!path) {
return std::nullopt;
}
return path->Append(ExecutableFolderPath());
}
std::optional<base::FilePath> GetUpdaterAppBundlePath(UpdaterScope scope) {
std::optional<base::FilePath> path = GetVersionedInstallDirectory(scope);
if (!path) {
return std::nullopt;
}
return path->Append(
base::StrCat({PRODUCT_FULLNAME_STRING, kExecutableSuffix, ".app"}));
}
base::FilePath GetExecutableRelativePath() {
return ExecutableFolderPath().Append(
base::StrCat({PRODUCT_FULLNAME_STRING, kExecutableSuffix}));
}
std::optional<base::FilePath> GetKeystoneFolderPath(UpdaterScope scope) {
std::optional<base::FilePath> path = GetLibraryFolderPath(scope);
if (!path) {
return std::nullopt;
}
return path->Append(FILE_PATH_LITERAL(COMPANY_SHORTNAME_STRING))
.Append(FILE_PATH_LITERAL(KEYSTONE_NAME));
}
bool ConfirmFilePermissions(const base::FilePath& root_path,
int kPermissionsMask) {
base::FileEnumerator file_enumerator(
root_path, false,
base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES |
base::FileEnumerator::SHOW_SYM_LINKS);
for (base::FilePath path = file_enumerator.Next(); !path.empty();
path = file_enumerator.Next()) {
if (!SetPosixFilePermissions(path, kPermissionsMask)) {
VLOG(0) << "Couldn't set file permissions for for: " << path.value();
return false;
}
base::File::Info file_info;
if (!base::GetFileInfo(path, &file_info)) {
VLOG(0) << "Couldn't get file info for: " << path.value();
return false;
}
// If file path is real directory and not a link, recurse into it.
if (file_info.is_directory && !base::IsLink(path)) {
if (!ConfirmFilePermissions(path, kPermissionsMask)) {
return false;
}
}
}
return true;
}
std::optional<base::FilePath> GetInstallDirectory(UpdaterScope scope) {
std::optional<base::FilePath> path = GetLibraryFolderPath(scope);
return path ? std::optional<base::FilePath>(
path->Append("Application Support")
.Append(COMPANY_SHORTNAME_STRING)
.Append(PRODUCT_FULLNAME_STRING))
: std::nullopt;
}
std::optional<base::FilePath> GetCacheBaseDirectory(UpdaterScope scope) {
base::FilePath caches_path;
if (!base::apple::GetLocalDirectory(NSCachesDirectory, &caches_path)) {
VLOG(1) << "Could not get Caches path";
return std::nullopt;
}
return std::optional<base::FilePath>(
caches_path.AppendASCII(MAC_BUNDLE_IDENTIFIER_STRING));
}
std::optional<base::FilePath> GetUpdateServiceLauncherPath(UpdaterScope scope) {
std::optional<base::FilePath> install_dir = GetInstallDirectory(scope);
return install_dir
? std::optional<base::FilePath>(
install_dir->Append("Current")
.Append(base::StrCat({PRODUCT_FULLNAME_STRING,
kExecutableSuffix, ".app"}))
.Append("Contents")
.Append("Helpers")
.Append("launcher"))
: std::nullopt;
}
bool PrepareToRunBundle(const base::FilePath& bundle_path) {
// Do not return early. Cleaning up attributes and prewarming Gatekeeper
// avoids popups visible to the user, but we must continue to try to update
// even if these fail, so we should do as much of the prep as we can.
bool dequarantine_ok = RemoveQuarantineAttributes(bundle_path);
bool prewarm_ok = PrewarmGatekeeperIfSupported(bundle_path) == 0;
return prewarm_ok && dequarantine_ok;
}
std::optional<base::FilePath> GetWakeTaskPlistPath(UpdaterScope scope) {
@autoreleasepool {
NSArray* library_paths = NSSearchPathForDirectoriesInDomains(
NSLibraryDirectory,
IsSystemInstall(scope) ? NSLocalDomainMask : NSUserDomainMask, YES);
if ([library_paths count] < 1) {
return std::nullopt;
}
return base::apple::NSStringToFilePath(library_paths[0])
.Append(IsSystemInstall(scope) ? "LaunchDaemons" : "LaunchAgents")
.AppendASCII(base::StrCat({GetWakeLaunchdName(scope), ".plist"}));
}
}
std::optional<std::string> ReadValueFromPlist(const base::FilePath& path,
const std::string& key) {
if (key.empty() || path.empty()) {
return std::nullopt;
}
NSData* data;
{
base::ScopedBlockingCall scoped_blocking_call(
FROM_HERE, base::BlockingType::WILL_BLOCK);
data =
[NSData dataWithContentsOfFile:base::apple::FilePathToNSString(path)];
}
if ([data length] == 0) {
return std::nullopt;
}
NSDictionary* all_keys = base::apple::ObjCCastStrict<NSDictionary>(
[NSPropertyListSerialization propertyListWithData:data
options:NSPropertyListImmutable
format:nil
error:nil]);
if (all_keys == nil) {
return std::nullopt;
}
CFStringRef value = base::apple::GetValueFromDictionary<CFStringRef>(
base::apple::NSToCFPtrCast(all_keys),
base::SysUTF8ToCFStringRef(key).get());
if (value == nullptr) {
return std::nullopt;
}
return base::SysCFStringRefToUTF8(value);
}
bool MigrateLegacyUpdaters(
UpdaterScope scope,
base::RepeatingCallback<void(const RegistrationRequest&)>
register_callback) {
return MigrateKeystoneApps(GetKeystoneFolderPath(scope).value(),
register_callback);
}
} // namespace updater