// 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 "chrome/updater/posix/setup.h"
#import <ServiceManagement/ServiceManagement.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#include <optional>
#include "base/apple/bundle_locations.h"
#include "base/apple/foundation_util.h"
#include "base/at_exit.h"
#include "base/command_line.h"
#include "base/debug/dump_without_crashing.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/thread_pool.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "base/threading/platform_thread.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/time/time.h"
#include "chrome/updater/constants.h"
#include "chrome/updater/crash_client.h"
#include "chrome/updater/crash_reporter.h"
#include "chrome/updater/mac/setup/keystone.h"
#include "chrome/updater/mac/setup/wake_task.h"
#include "chrome/updater/setup.h"
#include "chrome/updater/updater_branding.h"
#include "chrome/updater/updater_scope.h"
#include "chrome/updater/updater_version.h"
#import "chrome/updater/util/mac_util.h"
#include "chrome/updater/util/posix_util.h"
#include "chrome/updater/util/util.h"
#include "components/crash/core/common/crash_key.h"
namespace updater {
namespace {
bool CopyBundle(UpdaterScope scope) {
std::optional<base::FilePath> base_install_dir = GetInstallDirectory(scope);
std::optional<base::FilePath> versioned_install_dir =
GetVersionedInstallDirectory(scope);
if (!base_install_dir || !versioned_install_dir) {
LOG(ERROR) << "Failed to get install directory.";
return false;
}
if (base::PathExists(*versioned_install_dir)) {
if (!DeleteExcept(versioned_install_dir->Append("Crashpad"))) {
LOG(ERROR) << "Could not remove existing copy of this updater.";
return false;
}
}
base::File::Error error;
if (!base::CreateDirectoryAndGetError(*versioned_install_dir, &error)) {
LOG(ERROR) << "Failed to create '" << versioned_install_dir->value().c_str()
<< "' directory: " << base::File::ErrorToString(error);
return false;
}
// For system installs, set file permissions to be drwxr-xr-x
if (IsSystemInstall(scope)) {
constexpr int kPermissionsMask = base::FILE_PERMISSION_USER_MASK |
base::FILE_PERMISSION_READ_BY_GROUP |
base::FILE_PERMISSION_EXECUTE_BY_GROUP |
base::FILE_PERMISSION_READ_BY_OTHERS |
base::FILE_PERMISSION_EXECUTE_BY_OTHERS;
if (!base::SetPosixFilePermissions(base_install_dir->DirName(),
kPermissionsMask) ||
!base::SetPosixFilePermissions(*base_install_dir, kPermissionsMask) ||
!base::SetPosixFilePermissions(*versioned_install_dir,
kPermissionsMask)) {
LOG(ERROR) << "Failed to set permissions to drwxr-xr-x at "
<< versioned_install_dir->value();
return false;
}
}
if (!CopyDir(base::apple::OuterBundlePath(), *versioned_install_dir,
scope == UpdaterScope::kSystem)) {
LOG(ERROR) << "Copying app to '" << versioned_install_dir->value().c_str()
<< "' failed";
return false;
}
return true;
}
bool BootstrapPlist(UpdaterScope scope, const base::FilePath& path) {
std::string output;
int exit_code = 0;
base::CommandLine launchctl(base::FilePath("/bin/launchctl"));
launchctl.AppendArg("bootstrap");
launchctl.AppendArg(GetDomain(scope));
launchctl.AppendArgPath(path);
if (!base::GetAppOutputWithExitCode(launchctl, &output, &exit_code) ||
exit_code != 0) {
VLOG(1) << "launchctl bootstrap of " << path << " failed: " << exit_code
<< ": " << output;
return false;
}
return true;
}
// Ensure that the LaunchAgents/LaunchDaemons directory contains the wake item
// plist, with the specified contents. If not, the plist will be overwritten and
// the item reloaded. May block.
bool EnsureWakeLaunchItemPresence(UpdaterScope scope, NSDictionary* contents) {
const std::optional<base::FilePath> path = GetWakeTaskPlistPath(scope);
if (!path) {
VLOG(1) << "Failed to find wake plist path.";
return false;
}
const bool previousPlistExists = base::PathExists(*path);
if (!base::CreateDirectory(path->DirName())) {
VLOG(1) << "Failed to create " << path->DirName();
return false;
}
@autoreleasepool {
NSURL* const url = base::apple::FilePathToNSURL(*path);
// If the file is unchanged, avoid a spammy notification by not touching it.
if (previousPlistExists &&
[contents isEqualToDictionary:[NSDictionary
dictionaryWithContentsOfURL:url
error:nil]]) {
VLOG(2) << "Skipping unnecessary update to " << path;
return true;
}
// Save a backup of the previous plist.
base::ScopedTempDir backup_dir;
if (previousPlistExists &&
(!backup_dir.CreateUniqueTempDir() ||
!base::CopyFile(*path, backup_dir.GetPath().Append("backup_plist")))) {
VLOG(1) << "Failed to back up previous plist.";
return false;
}
// Bootout the old plist.
{
std::string output;
int exit_code = 0;
base::CommandLine launchctl(base::FilePath("/bin/launchctl"));
launchctl.AppendArg("bootout");
launchctl.AppendArg(GetDomain(scope));
launchctl.AppendArgPath(*path);
if (!base::GetAppOutputWithExitCode(launchctl, &output, &exit_code)) {
VLOG(1) << "Failed to launch launchctl.";
} else if (exit_code != 0) {
// This is expected in cases where there the service doesn't exist.
// Unfortunately, in the user case, bootout returns 5 both for does-not-
// exist errors and other errors.
VLOG(2) << "launchctl bootout exited: " << exit_code
<< ", stdout: " << output;
}
}
// Update app registration with LaunchServices.
const std::optional<base::FilePath> install_path =
GetInstallDirectory(scope);
if (install_path) {
OSStatus ls_result = LSRegisterURL(
base::apple::FilePathToCFURL(
install_path->Append("Current").Append(base::StrCat(
{PRODUCT_FULLNAME_STRING, kExecutableSuffix, ".app"})))
.get(),
true);
VLOG_IF(1, ls_result != noErr) << "LSRegisterURL failed: " << ls_result;
} else {
VLOG(1) << "Failed to retrieve bundle path, skipping LSRegisterURL.";
}
// Overwrite the plist.
NSData* data = [NSPropertyListSerialization
dataWithPropertyList:contents
format:NSPropertyListXMLFormat_v1_0
options:0
error:nil];
NSError* error;
if (![data writeToURL:url options:NSDataWritingAtomic error:&error]) {
VLOG(1) << "Failed to write " << url << " error " << error.description;
return false;
}
// Bootstrap the new plist.
if (!BootstrapPlist(scope, *path)) {
// The plist has already been replaced! If launchctl doesn't like it,
// this installation is now broken. Try to recover by restoring and
// bootstrapping the backup.
if (previousPlistExists &&
(!base::Move(backup_dir.GetPath().Append("backup_plist"), *path) ||
!BootstrapPlist(scope, *path))) {
VLOG(1) << "Failed to restore backup plist.";
}
return false;
}
return true;
}
}
bool CreateWakeLaunchdJobPlist(UpdaterScope scope) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
NSDictionary* plist = CreateWakeLaunchdPlist(scope);
if (!plist) {
return false;
}
return EnsureWakeLaunchItemPresence(scope, plist);
}
void CleanAfterInstallFailure(UpdaterScope scope) {
// If install fails at any point, attempt to clean the install.
DeleteCandidateInstallFolder(scope);
}
int DoSetup(UpdaterScope scope) {
if (!CopyBundle(scope)) {
return kErrorFailedToCopyBundle;
}
// Quarantine attribute needs to be removed here as the copied bundle might be
// given com.apple.quarantine attribute, and the server is attempted to be
// launched below, Gatekeeper could prompt the user.
const std::optional<base::FilePath> install_dir = GetInstallDirectory(scope);
if (!install_dir) {
return kErrorFailedToGetInstallDir;
}
if (!PrepareToRunBundle(*install_dir)) {
VLOG(1) << "PrepareToRunBundle failed. Gatekeeper may prompt.";
}
// If there is no Current symlink, create one now.
base::FilePath current_symlink = install_dir->Append("Current");
if (!base::PathExists(current_symlink)) {
if (base::DeleteFile(current_symlink) &&
symlink(kUpdaterVersion, current_symlink.value().c_str())) {
return kErrorFailedToLinkCurrent;
}
}
if (!CreateWakeLaunchdJobPlist(scope)) {
return kErrorFailedToCreateWakeLaunchdJobPlist;
}
if (scope == UpdaterScope::kSystem) {
const std::optional<base::FilePath> bundle_path =
GetUpdaterAppBundlePath(scope);
if (bundle_path) {
base::FilePath path =
bundle_path->Append("Contents").Append("Helpers").Append("launcher");
struct stat info;
if (lstat(path.value().c_str(), &info) || info.st_uid ||
!(S_IFREG & info.st_mode) ||
lchmod(path.value().c_str(),
S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH | S_ISUID)) {
VPLOG(1)
<< "Launcher lchmod failed. Cross-user on-demand will not work";
base::debug::DumpWithoutCrashing();
}
}
}
return kErrorOk;
}
} // namespace
int Setup(UpdaterScope scope) {
int error = DoSetup(scope);
if (error) {
CleanAfterInstallFailure(scope);
}
return error;
}
int PromoteCandidate(UpdaterScope scope) {
const std::optional<base::FilePath> updater_executable_path =
GetUpdaterExecutablePath(scope);
const std::optional<base::FilePath> install_dir = GetInstallDirectory(scope);
const std::optional<base::FilePath> bundle_path =
GetUpdaterAppBundlePath(scope);
if (!updater_executable_path || !install_dir || !bundle_path) {
return kErrorFailedToGetVersionedInstallDirectory;
}
// Update the Current symlink.
base::FilePath tmp_current_name = install_dir->Append("NewCurrent");
if (!base::DeleteFile(tmp_current_name)) {
VLOG(1) << "Failed to delete existing " << tmp_current_name.value();
}
if (symlink(kUpdaterVersion, tmp_current_name.value().c_str())) {
return kErrorFailedToLinkCurrent;
}
if (rename(tmp_current_name.value().c_str(),
install_dir->Append("Current").value().c_str())) {
return kErrorFailedToRenameCurrent;
}
if (!CreateWakeLaunchdJobPlist(scope)) {
return kErrorFailedToCreateWakeLaunchdJobPlist;
}
if (!InstallKeystone(scope)) {
return kErrorFailedToInstallLegacyUpdater;
}
return kErrorOk;
}
#pragma mark Uninstall
int UninstallCandidate(UpdaterScope scope) {
return !DeleteCandidateInstallFolder(scope) ? kErrorFailedToDeleteFolder
: kErrorOk;
}
int Uninstall(UpdaterScope scope) {
VLOG(1) << base::CommandLine::ForCurrentProcess()->GetCommandLineString()
<< " : " << __func__;
int exit = UninstallCandidate(scope);
if (!RemoveWakeJobFromLaunchd(scope)) {
exit = kErrorFailedToRemoveWakeJobFromLaunchd;
}
base::ThreadPool::PostTask(FROM_HERE,
{base::MayBlock(), base::WithBaseSyncPrimitives()},
base::BindOnce(&UninstallKeystone, scope));
// Delete Keystone shim plists.
if (IsSystemInstall(scope)) {
base::DeleteFile(GetLibraryFolderPath(scope)
->Append("LaunchDaemons")
.Append(base::ToLowerASCII(LEGACY_GOOGLE_UPDATE_APPID
".daemon.plist")));
} else {
base::FilePath launch_agent_dir =
GetLibraryFolderPath(scope)->Append("LaunchAgents");
base::DeleteFile(launch_agent_dir.Append(
base::ToLowerASCII(LEGACY_GOOGLE_UPDATE_APPID ".agent.plist"))) &&
base::DeleteFile(launch_agent_dir.Append(base::ToLowerASCII(
LEGACY_GOOGLE_UPDATE_APPID ".xpcservice.plist")));
}
// Delete the updater's caches. On Mac, this is different from the
// install directory.
DeleteFolder(GetCacheBaseDirectory(scope));
// Deleting the install folder is best-effort. Current running processes such
// as the crash handler process may still write to the updater log file, thus
// it is not always possible to delete the log file. Additionally, the log
// file is helpful for debugging.
if (!DeleteExcept(GetLogFilePath(scope))) {
VLOG(0) << "Failed to delete install directory.";
}
return exit;
}
} // namespace updater