blob: 55ce08c4327fee1f9dd4ec216ce7677121af28bc [file] [log] [blame]
// Copyright 2020 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/updater/browser_updater_client_util.h"
#include <Foundation/Foundation.h>
#import <OpenDirectory/OpenDirectory.h>
#import <ServiceManagement/ServiceManagement.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "base/apple/bridging.h"
#include "base/apple/bundle_locations.h"
#include "base/command_line.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/mac/authorization_util.h"
#include "base/mac/foundation_util.h"
#include "base/mac/scoped_authorizationref.h"
#include "base/memory/scoped_refptr.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/strings/strcat.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "build/buildflag.h"
#include "chrome/browser/updater/browser_updater_client.h"
#include "chrome/browser/updater/browser_updater_client_util.h"
#include "chrome/browser/updater/browser_updater_helper_client_mac.h"
#include "chrome/common/chrome_version.h"
#include "chrome/grit/chromium_strings.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/updater/updater_scope.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/l10n_util_mac.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
constexpr char kInstallCommand[] = "install";
base::FilePath GetUpdaterExecutablePath() {
return base::FilePath(base::StrCat({kUpdaterName, ".app"}))
.Append(FILE_PATH_LITERAL("Contents"))
.Append(FILE_PATH_LITERAL("MacOS"))
.Append(kUpdaterName);
}
bool BundleOwnedByUser(uid_t user_uid) {
const base::FilePath path = base::apple::OuterBundlePath();
base::stat_wrapper_t stat_info = {};
if (base::File::Lstat(path.value().c_str(), &stat_info) != 0) {
VPLOG(2) << "Failed to get information on path " << path.value();
return false;
}
if (S_ISLNK(stat_info.st_mode)) {
VLOG(2) << "Path " << path.value() << " is a symbolic link.";
return false;
}
return stat_info.st_uid == user_uid;
}
bool BundleOwnedByRoot() {
return BundleOwnedByUser(0);
}
bool BundleOwnedByCurrentUser() {
return BundleOwnedByUser(geteuid());
}
bool IsEffectiveUserAdmin() {
NSError* error;
ODNode* search_node = [ODNode nodeWithSession:[ODSession defaultSession]
type:kODNodeTypeLocalNodes
error:&error];
if (!search_node) {
VLOG(2) << "Error creating ODNode: " << search_node;
return false;
}
ODQuery* query =
[ODQuery queryWithNode:search_node
forRecordTypes:kODRecordTypeUsers
attribute:kODAttributeTypeUniqueID
matchType:kODMatchEqualTo
queryValues:[NSString stringWithFormat:@"%d", geteuid()]
returnAttributes:kODAttributeTypeStandardOnly
maximumResults:1
error:&error];
if (!query) {
VLOG(2) << "Error constructing query: " << error;
return false;
}
NSArray<ODRecord*>* results = [query resultsAllowingPartial:NO error:&error];
if (!results) {
VLOG(2) << "Error executing query: " << error;
return false;
}
ODRecord* admin_group = [search_node recordWithRecordType:kODRecordTypeGroups
name:@"admin"
attributes:nil
error:&error];
if (!admin_group) {
VLOG(2) << "Failed to get 'admin' group: " << error;
return false;
}
bool result = [admin_group isMemberRecord:results.firstObject error:&error];
VLOG_IF(2, error) << "Failed to get member record: " << error;
return result;
}
bool ShouldPromoteUpdater() {
// 1) Should promote if browser is owned by root and not installed. The not
// installed part of this case is handled in version_updater_mac.mm
if (BundleOwnedByRoot())
return true;
// 2) If the effective user is root and the browser is not owned by root (i.e.
// if the current user has run with sudo).
if (geteuid() == 0)
return true;
// 3) If effective user is not the owner of the browser and is an
// administrator.
return !BundleOwnedByCurrentUser() && IsEffectiveUserAdmin();
}
int RunCommand(const base::FilePath& exe_path, const char* cmd_switch) {
base::CommandLine command(exe_path);
command.AppendSwitch(cmd_switch);
int exit_code = -1;
auto process = base::LaunchProcess(command, {});
if (!process.IsValid())
return exit_code;
process.WaitForExitWithTimeout(base::Seconds(120), &exit_code);
return exit_code;
}
// Only works in kUser scope.
void RegisterBrowser(base::OnceClosure complete) {
BrowserUpdaterClient::Create(updater::UpdaterScope::kUser)
->Register(std::move(complete));
}
// Only works in kUser scope.
void InstallUpdaterAndRegisterBrowser(base::OnceClosure complete) {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE,
{base::MayBlock(), base::WithBaseSyncPrimitives(),
base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
base::BindOnce([]() {
// The updater executable should be in
// BRANDING.app/Contents/Frameworks/BRANDING.framework/Versions/V/
// Helpers/Updater.app/Contents/MacOS/Updater
const base::FilePath updater_executable_path =
base::apple::FrameworkBundlePath()
.Append(FILE_PATH_LITERAL("Helpers"))
.Append(GetUpdaterExecutablePath());
if (!base::PathExists(updater_executable_path)) {
VLOG(1) << "The updater does not exist in the bundle.";
return false;
}
int exit_code = RunCommand(updater_executable_path, kInstallCommand);
if (exit_code != 0) {
VLOG(1) << "Couldn't install the updater. Exit code: " << exit_code;
return false;
}
return true;
}),
base::BindOnce(
[](base::OnceClosure complete, bool success) {
if (success) {
RegisterBrowser(std::move(complete));
} else {
std::move(complete).Run();
}
},
std::move(complete)));
}
} // namespace
std::string CurrentlyInstalledVersion() {
base::FilePath outer_bundle = base::apple::OuterBundlePath();
base::FilePath plist_path =
outer_bundle.Append("Contents").Append("Info.plist");
NSDictionary* info_plist = [NSDictionary
dictionaryWithContentsOfFile:base::mac::FilePathToNSString(plist_path)];
return base::SysNSStringToUTF8(
base::mac::ObjCCast<NSString>(info_plist[@"CFBundleShortVersionString"]));
}
updater::UpdaterScope GetUpdaterScope() {
return BundleOwnedByRoot() ? updater::UpdaterScope::kSystem
: updater::UpdaterScope::kUser;
}
void EnsureUpdater(base::OnceClosure prompt, base::OnceClosure complete) {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()}, base::BindOnce(&GetUpdaterScope),
base::BindOnce(
[](base::OnceClosure prompt, base::OnceClosure complete,
updater::UpdaterScope scope) {
scoped_refptr<BrowserUpdaterClient> client =
BrowserUpdaterClient::Create(scope);
client->IsBrowserRegistered(base::BindOnce(
[](scoped_refptr<BrowserUpdaterClient> client,
base::OnceClosure prompt, base::OnceClosure complete,
bool registered) {
if (registered) {
std::move(complete).Run();
return;
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()},
base::BindOnce(&ShouldPromoteUpdater),
base::BindOnce(
[](scoped_refptr<BrowserUpdaterClient> client,
base::OnceClosure prompt,
base::OnceClosure complete, bool promote) {
if (promote) {
// User intervention is required; prompt.
std::move(prompt).Run();
std::move(complete).Run();
return;
}
// Check whether an updater exists.
client->GetUpdaterVersion(base::BindOnce(
[](base::OnceClosure complete,
const base::Version& version) {
if (!version.IsValid()) {
InstallUpdaterAndRegisterBrowser(
std::move(complete));
} else {
RegisterBrowser(std::move(complete));
}
},
std::move(complete)));
},
client, std::move(prompt), std::move(complete)));
},
client, std::move(prompt), std::move(complete)));
},
std::move(prompt), std::move(complete)));
}
void SetupSystemUpdater() {
NSString* prompt = l10n_util::GetNSStringFWithFixup(
IDS_PROMOTE_AUTHENTICATION_PROMPT,
l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
base::mac::ScopedAuthorizationRef authorization =
base::mac::AuthorizationCreateToRunAsRoot(
base::apple::NSToCFPtrCast(prompt));
if (!authorization.get()) {
VLOG(0) << "Could not get authorization to run as root.";
return;
}
base::ScopedCFTypeRef<CFErrorRef> error;
Boolean result = SMJobBless(kSMDomainSystemLaunchd,
base::SysUTF8ToCFStringRef(kPrivilegedHelperName),
authorization, error.InitializeInto());
if (!result) {
base::ScopedCFTypeRef<CFStringRef> desc(CFErrorCopyDescription(error));
VLOG(0) << "Could not bless the privileged helper. Resulting error: "
<< base::SysCFStringRefToUTF8(desc);
}
base::MakeRefCounted<BrowserUpdaterHelperClientMac>()->SetupSystemUpdater(
base::BindOnce([](int result) {
VLOG_IF(1, result != 0)
<< "There was a problem with performing the system "
"updater tasks. Result: "
<< result;
}));
}