| // 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 <optional> |
| |
| #include "base/apple/bridging.h" |
| #include "base/apple/bundle_locations.h" |
| #include "base/apple/foundation_util.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/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/threading/scoped_blocking_call.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/branded_strings.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "chrome/updater/constants.h" |
| #include "chrome/updater/updater_scope.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/l10n/l10n_util_mac.h" |
| |
| 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); |
| } |
| |
| std::optional<uid_t> GetBundleOwner() { |
| const base::FilePath path = base::apple::OuterBundlePath(); |
| base::stat_wrapper_t stat_info = {}; |
| if (base::File::Lstat(path, &stat_info) != 0) { |
| VPLOG(2) << "Failed to get information on path " << path.value(); |
| return std::nullopt; |
| } |
| |
| if (S_ISLNK(stat_info.st_mode)) { |
| VLOG(2) << "Path " << path.value() << " is a symbolic link."; |
| return std::nullopt; |
| } |
| |
| return stat_info.st_uid; |
| } |
| |
| 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() { |
| std::optional<uid_t> owner = GetBundleOwner(); |
| |
| // 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 (owner && *owner == 0) { |
| 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 owner && *owner != geteuid() && IsEffectiveUserAdmin(); |
| } |
| |
| int RunCommand(const base::FilePath& exe_path, const char* cmd_switch) { |
| base::CommandLine command(exe_path); |
| command.AppendSwitch(cmd_switch); |
| command.AppendSwitch(updater::kEnableLoggingSwitch); |
| command.AppendSwitchASCII(updater::kLoggingModuleSwitch, |
| updater::kLoggingModuleSwitchValue); |
| |
| 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))); |
| } |
| |
| // Marks the browser as active, and schedules a call 1 hour later to mark the |
| // browser as active again. |
| void SetActive() { |
| base::FilePath actives_dir = |
| base::GetHomeDir() |
| .AppendASCII("Library") |
| .Append(FILE_PATH_LITERAL(COMPANY_SHORTNAME_STRING)) |
| .Append(FILE_PATH_LITERAL(COMPANY_SHORTNAME_STRING "SoftwareUpdate")) |
| .AppendASCII("Actives"); |
| if (!CreateDirectory(actives_dir)) { |
| return; |
| } |
| base::WriteFile(actives_dir.Append(base::apple::BaseBundleID()), ""); |
| base::ThreadPool::PostDelayedTask( |
| FROM_HERE, |
| {base::MayBlock(), base::TaskPriority::BEST_EFFORT, |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}, |
| base::BindOnce(&SetActive), base::Hours(1)); |
| } |
| |
| } // namespace |
| |
| std::string CurrentlyInstalledVersion() { |
| base::ScopedBlockingCall blocks(FROM_HERE, base::BlockingType::WILL_BLOCK); |
| base::FilePath outer_bundle = base::apple::OuterBundlePath(); |
| base::FilePath plist_path = |
| outer_bundle.Append("Contents").Append("Info.plist"); |
| NSDictionary* info_plist = [NSDictionary |
| dictionaryWithContentsOfFile:base::apple::FilePathToNSString(plist_path)]; |
| return base::SysNSStringToUTF8(base::apple::ObjCCast<NSString>( |
| info_plist[@"CFBundleShortVersionString"])); |
| } |
| |
| updater::UpdaterScope GetBrowserUpdaterScope() { |
| std::optional<uid_t> owner = GetBundleOwner(); |
| return owner && (*owner == 0 || *owner != geteuid()) |
| ? updater::UpdaterScope::kSystem |
| : updater::UpdaterScope::kUser; |
| } |
| |
| void EnsureUpdater(base::OnceClosure prompt, base::OnceClosure complete) { |
| base::ThreadPool::PostTask(FROM_HERE, |
| {base::MayBlock(), base::TaskPriority::BEST_EFFORT, |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}, |
| base::BindOnce(&SetActive)); |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, |
| {base::MayBlock(), base::TaskPriority::BEST_EFFORT, |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}, |
| base::BindOnce(&GetBrowserUpdaterScope), |
| 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::apple::ScopedCFTypeRef<CFErrorRef> error; |
| Boolean result = |
| SMJobBless(kSMDomainSystemLaunchd, |
| base::SysUTF8ToCFStringRef(kPrivilegedHelperName).get(), |
| authorization, error.InitializeInto()); |
| if (!result) { |
| base::apple::ScopedCFTypeRef<CFStringRef> desc( |
| CFErrorCopyDescription(error.get())); |
| VLOG(0) << "Could not bless the privileged helper. Resulting error: " |
| << base::SysCFStringRefToUTF8(desc.get()); |
| return; |
| } |
| |
| 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; |
| })); |
| } |