// 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.
// GoogleUpdateSetup.exe is the first exe that is run when chrome is being
// installed. It has two main jobs:
// 1) unpack the resources (possibly decompressing some)
// 2) run the real installer (updater.exe) with appropriate flags (--install).
//
// All files needed by the updater are archived together as an uncompressed
// LZMA file, which is further compressed as one file, and inserted as a
// binary resource in the resource section of the setup program.
#include "chrome/updater/win/installer/installer.h"
#include <shellapi.h>
#include <shlobj.h>
#include <optional>
#include <string>
#include "base/check.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/files/scoped_temp_dir.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/strings/strcat.h"
#include "base/strings/sys_string_conversions.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "base/types/expected_macros.h"
#include "base/win/scoped_com_initializer.h"
#include "base/win/scoped_localalloc.h"
#include "base/win/windows_version.h"
#include "chrome/installer/util/lzma_util.h"
#include "chrome/updater/constants.h"
#include "chrome/updater/tag.h"
#include "chrome/updater/updater_branding.h"
#include "chrome/updater/updater_scope.h"
#include "chrome/updater/util/util.h"
#include "chrome/updater/util/win_util.h"
#include "chrome/updater/win/installer/configuration.h"
#include "chrome/updater/win/installer/installer_constants.h"
#include "chrome/updater/win/installer/pe_resource.h"
#include "chrome/updater/win/ui/l10n_util.h"
#include "chrome/updater/win/win_constants.h"
namespace updater {
using PathString = StackString<MAX_PATH>;
namespace {
// Returns the tag if the tag can be extracted. The tag is read from the
// program file image used to create this process. The implementation of this
// function only handles UTF8 tags.
std::string ExtractTag() {
PathString path;
return (::GetModuleFileName(nullptr, path.get(), path.capacity()) > 0 &&
::GetLastError() == ERROR_SUCCESS)
? tagging::BinaryReadTagString(base::FilePath(path.get()))
: std::string();
}
} // namespace
// This structure passes data back and forth for the processing
// of resource callbacks.
struct Context {
// Input to the call back method. Specifies the dir to save resources into.
const wchar_t* base_path = nullptr;
// First output from call back method. Specifies the path of resource archive.
raw_ptr<PathString> updater_resource_path = nullptr;
};
// Calls CreateProcess with good default parameters and waits for the process to
// terminate returning the process exit code. In case of CreateProcess failure,
// returns a results object with the provided codes as follows:
// - ERROR_FILE_NOT_FOUND: (file_not_found_code, attributes of setup.exe).
// - ERROR_PATH_NOT_FOUND: (path_not_found_code, attributes of setup.exe).
// - Otherwise: (generic_failure_code, CreateProcess error code).
// In case of error waiting for the process to exit, returns a results object
// with (WAIT_FOR_PROCESS_FAILED, last error code). Otherwise, returns a results
// object with the subprocess's exit code.
ProcessExitResult RunProcessAndWait(const wchar_t* exe_path, wchar_t* cmdline) {
STARTUPINFOW si = {sizeof(si)};
PROCESS_INFORMATION pi = {0};
if (!::CreateProcess(exe_path, cmdline, nullptr, nullptr, FALSE,
CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) {
// Split specific failure modes. If the process couldn't be launched because
// its file/path couldn't be found, report its attributes in ExtraCode1.
// This will help diagnose the prevalence of launch failures due to Image
// File Execution Options tampering. See https://crbug.com/672813 for more
// details.
const DWORD last_error = ::GetLastError();
const DWORD attributes = ::GetFileAttributes(exe_path);
switch (last_error) {
case ERROR_FILE_NOT_FOUND:
return ProcessExitResult(RUN_SETUP_FAILED_FILE_NOT_FOUND, attributes);
case ERROR_PATH_NOT_FOUND:
return ProcessExitResult(RUN_SETUP_FAILED_PATH_NOT_FOUND, attributes);
default:
break;
}
// Lump all other errors into a distinct failure bucket.
return ProcessExitResult(RUN_SETUP_FAILED_COULD_NOT_CREATE_PROCESS,
last_error);
}
::CloseHandle(pi.hThread);
DWORD updater_exit_code = 0;
DWORD wr = ::WaitForSingleObject(pi.hProcess, INFINITE);
if (WAIT_OBJECT_0 != wr ||
!::GetExitCodeProcess(pi.hProcess, &updater_exit_code)) {
// Note: We've assumed that WAIT_OBJECT_0 != wr means a failure. The call
// could return a different object but since we never spawn more than one
// sub-process at a time that case should never happen.
return ProcessExitResult(WAIT_FOR_PROCESS_FAILED, ::GetLastError());
}
::CloseHandle(pi.hProcess);
return ProcessExitResult(UPDATER_EXIT_CODE, updater_exit_code);
}
// Windows defined callback used in the EnumResourceNames call. For each
// matching resource found, the callback is invoked and at this point we write
// it to disk. We expect resource names to start with the 'updater' prefix.
// Any other name is treated as an error.
BOOL CALLBACK OnResourceFound(HMODULE module,
const wchar_t* type,
wchar_t* name,
LONG_PTR context) {
Context* ctx = reinterpret_cast<Context*>(context);
if (!ctx) {
return FALSE;
}
if (!StrStartsWith(name, kUpdaterArchivePrefix)) {
return FALSE;
}
PEResource resource(name, type, module);
if (!resource.IsValid() || resource.Size() < 1) {
return FALSE;
}
PathString full_path;
if (!full_path.assign(ctx->base_path) || !full_path.append(name) ||
!resource.WriteToDisk(full_path.get())) {
return FALSE;
}
if (!ctx->updater_resource_path->assign(full_path.get())) {
return FALSE;
}
return TRUE;
}
std::optional<base::FilePath> FindOfflineDir(
const base::FilePath& unpack_path) {
base::FileEnumerator file_enumerator(
unpack_path.Append(L"bin").Append(L"Offline"), false,
base::FileEnumerator::DIRECTORIES);
for (base::FilePath path = file_enumerator.Next(); !path.empty();
path = file_enumerator.Next()) {
if (IsGuid(path.BaseName().value())) {
return path;
}
}
return {};
}
// Finds and writes to disk resources of type 'B7' (7zip archive). Returns false
// if there is a problem in writing any resource to disk.
ProcessExitResult UnpackBinaryResources(const Configuration& configuration,
HMODULE module,
const wchar_t* base_path,
PathString* archive_path) {
// Prepare the input to OnResourceFound method that needs a location where
// it will write all the resources.
Context context = {base_path, archive_path};
// Get the resources of type 'B7' (7zip archive).
if (!::EnumResourceNames(module, kLZMAResourceType, OnResourceFound,
reinterpret_cast<LONG_PTR>(&context))) {
return ProcessExitResult(UNABLE_TO_EXTRACT_ARCHIVE, ::GetLastError());
}
if (archive_path->length() == 0) {
return ProcessExitResult(UNABLE_TO_EXTRACT_ARCHIVE);
}
ProcessExitResult exit_code = ProcessExitResult(SUCCESS_EXIT_CODE);
return exit_code;
}
ProcessExitResult BuildInstallerCommandLineArgumentsInternal(
wchar_t* cmd_line_args,
size_t cmd_line_args_capacity,
base::CommandLine args = GetCommandLineLegacyCompatible()) {
CHECK(cmd_line_args);
CHECK(cmd_line_args_capacity);
*cmd_line_args = '\0';
// Use the tag from the `--install` command line argument if such argument
// exists. Otherwise, try extracting a tag embedded in the program image of
// the meta installer.
if (args.GetSwitchValueASCII(kInstallSwitch).empty()) {
const std::string tag = ExtractTag();
if (!tag.empty()) {
args.AppendSwitchASCII(kInstallSwitch, tag.c_str());
}
}
// If there is nothing, return an error.
if (args.GetSwitches().size() == 0 && args.GetArgs().size() == 0) {
return ProcessExitResult(INVALID_OPTION);
}
// Append logging-related arguments for debugging purposes.
if (!args.HasSwitch(kEnableLoggingSwitch)) {
args.AppendSwitch(kEnableLoggingSwitch);
}
if (!args.HasSwitch(kLoggingModuleSwitch)) {
args.AppendSwitchASCII(kLoggingModuleSwitch, kLoggingModuleSwitchValue);
}
std::wstring args_str = args.GetArgumentsString();
if (args_str.size() >= cmd_line_args_capacity) {
return ProcessExitResult(COMMAND_STRING_OVERFLOW);
}
SafeStrCopy(cmd_line_args, cmd_line_args_capacity, args_str.c_str());
return ProcessExitResult(SUCCESS_EXIT_CODE);
}
ProcessExitResult BuildInstallerCommandLineArguments(
const wchar_t* cmd_line,
wchar_t* cmd_line_args,
size_t cmd_line_args_capacity) {
CHECK(cmd_line);
return BuildInstallerCommandLineArgumentsInternal(
cmd_line_args, cmd_line_args_capacity,
base::CommandLine::FromString(cmd_line));
}
// Executes updater.exe, waits for it to finish and returns the exit code.
ProcessExitResult RunSetup(const wchar_t* setup_path,
const wchar_t* cmd_line_args) {
CHECK(setup_path && *setup_path);
CHECK(cmd_line_args && *cmd_line_args);
CommandString cmd_line;
// Put the quoted path to setup.exe in cmd_line first, then the args.
if (!cmd_line.assign(
base::StrCat(
{base::CommandLine::QuoteForCommandLineToArgvW(setup_path), L" ",
cmd_line_args})
.c_str())) {
return ProcessExitResult(COMMAND_STRING_OVERFLOW);
}
return RunProcessAndWait(setup_path, cmd_line.get());
}
ProcessExitResult HandleRunElevated(const base::CommandLine& command_line) {
CHECK(!::IsUserAnAdmin());
CHECK(!command_line.HasSwitch(kCmdLinePrefersUser));
if (command_line.HasSwitch(kCmdLineExpectElevated)) {
VLOG(1) << __func__ << "Unexpected elevation loop! "
<< command_line.GetCommandLineString();
return ProcessExitResult(UNEXPECTED_ELEVATION_LOOP);
}
if (command_line.HasSwitch(kSilentSwitch)) {
VLOG(1) << __func__ << ": cannot show an elevation prompt with `/silent`: "
<< command_line.GetCommandLineString();
return ProcessExitResult(UNEXPECTED_ELEVATION_LOOP_SILENT);
}
// The metainstaller needs elevation because unpacking files and running
// updater.exe must happen from a secure directory.
base::CommandLine elevated_command_line = command_line;
elevated_command_line.AppendSwitchASCII(kCmdLineExpectElevated, {});
ASSIGN_OR_RETURN(DWORD result,
RunElevated(command_line.GetProgram(),
elevated_command_line.GetArgumentsString()),
[](HRESULT error) {
return ProcessExitResult(FAILED_TO_ELEVATE_METAINSTALLER,
error);
});
return ProcessExitResult(UPDATER_EXIT_CODE, result);
}
ProcessExitResult HandleRunDeElevated(const base::CommandLine& command_line) {
CHECK(::IsUserAnAdmin());
if (command_line.HasSwitch(kCmdLineExpectDeElevated)) {
VLOG(1) << __func__ << "Unexpected de-elevation loop! "
<< command_line.GetCommandLineString();
return ProcessExitResult(UNEXPECTED_DE_ELEVATION_LOOP);
}
base::win::ScopedCOMInitializer com_initializer(
base::win::ScopedCOMInitializer::kMTA);
CHECK(com_initializer.Succeeded());
// De-elevate the metainstaller.
ASSIGN_OR_RETURN(
DWORD result, RunDeElevated([&] {
base::CommandLine de_elevate_command_line = command_line;
de_elevate_command_line.AppendSwitch(kCmdLineExpectDeElevated);
return de_elevate_command_line;
}()),
[](HRESULT error) {
return ProcessExitResult(FAILED_TO_DE_ELEVATE_METAINSTALLER, error);
});
return ProcessExitResult(UPDATER_EXIT_CODE, result);
}
ProcessExitResult InstallerMain(HMODULE module) {
CHECK(EnableSecureDllLoading());
EnableProcessHeapMetadataProtection();
if (base::win::GetVersion() < base::win::Version::WIN10) {
return ProcessExitResult(UNSUPPORTED_WINDOWS_VERSION);
}
CommandString cmd_line_args;
ProcessExitResult args_result = BuildInstallerCommandLineArgumentsInternal(
cmd_line_args.get(), cmd_line_args.capacity());
if (args_result.exit_code != SUCCESS_EXIT_CODE) {
return args_result;
}
// Both `RunElevated` and `RunDeElevated` use shell APIs to run the process,
// which can have issues with relative paths. So we use the full exe path for
// the program in the command line.
base::FilePath exe_path;
if (!base::PathService::Get(base::FILE_EXE, &exe_path)) {
return ProcessExitResult(UNABLE_TO_GET_EXE_PATH);
}
const base::CommandLine command_line =
base::CommandLine::FromString(base::StrCat(
{base::CommandLine::QuoteForCommandLineToArgvW(exe_path.value()),
L" ", cmd_line_args.get()}));
const UpdaterScope scope = GetUpdaterScopeForCommandLine(command_line);
if (!::IsUserAnAdmin() && IsSystemInstall(scope)) {
ProcessExitResult run_elevated_result = HandleRunElevated(command_line);
if (run_elevated_result.exit_code == UPDATER_EXIT_CODE ||
!IsPrefersForCommandLine(command_line)) {
return run_elevated_result;
}
// "needsadmin=prefers" case: Could not elevate. So fall through to
// install as a per-user app.
if (!cmd_line_args.append(L" --") ||
!cmd_line_args.append(
base::SysUTF8ToWide(kCmdLinePrefersUser).c_str())) {
return ProcessExitResult(COMMAND_STRING_OVERFLOW);
}
} else if (::IsUserAnAdmin() && !IsSystemInstall(scope) && IsUACOn()) {
return HandleRunDeElevated(command_line);
}
base::CommandLine::Init(0, nullptr);
*base::CommandLine::ForCurrentProcess() = command_line;
InitLogging(scope);
VLOG(1) << command_line.GetCommandLineString();
ProcessExitResult exit_code = ProcessExitResult(SUCCESS_EXIT_CODE);
// Parse configuration from the command line and resources.
Configuration configuration;
if (!configuration.Initialize(module)) {
return ProcessExitResult(GENERIC_INITIALIZATION_FAILURE, ::GetLastError());
}
// Exit early if an invalid switch was found on the command line.
if (configuration.has_invalid_switch()) {
return ProcessExitResult(INVALID_OPTION);
}
// First get a path where we can extract the resource payload, which is
// a compressed LZMA archive of a single file.
std::optional<base::ScopedTempDir> base_path_owner = CreateSecureTempDir();
if (!base_path_owner) {
return ProcessExitResult(TEMP_DIR_FAILED);
}
PathString base_path;
if (!base_path.assign(
base_path_owner->GetPath().AsEndingWithSeparator().value().c_str())) {
return ProcessExitResult(PATH_STRING_OVERFLOW);
}
PathString compressed_archive;
exit_code = UnpackBinaryResources(configuration, module, base_path.get(),
&compressed_archive);
// Create a temp folder where the archives are unpacked.
std::optional<base::ScopedTempDir> temp_path = CreateSecureTempDir();
if (!temp_path) {
return ProcessExitResult(TEMP_DIR_FAILED);
}
const base::FilePath unpack_path = temp_path->GetPath();
// Unpack the compressed archive to extract the uncompressed archive file.
UnPackStatus unpack_status =
UnPackArchive(base::FilePath(compressed_archive.get()), unpack_path,
/*output_file=*/nullptr);
if (unpack_status != UNPACK_NO_ERROR) {
return ProcessExitResult(UNPACKING_FAILED);
}
// Unpack the uncompressed archive to extract the updater files.
base::FilePath uncompressed_archive =
unpack_path.Append(FILE_PATH_LITERAL("updater.7z"));
unpack_status =
UnPackArchive(uncompressed_archive, unpack_path, /*output_file=*/nullptr);
if (unpack_status != UNPACK_NO_ERROR) {
return ProcessExitResult(UNPACKING_FAILED);
}
// While unpacking the binaries, we paged in a whole bunch of memory that
// we don't need anymore. Let's give it back to the pool before running
// setup.
::SetProcessWorkingSetSize(::GetCurrentProcess(), static_cast<SIZE_T>(-1),
static_cast<SIZE_T>(-1));
// Determine if an offlinedir is embedded and, if it is, add an
// --offlinedir={GUID} switch to indicate that an offline install should
// be performed.
const std::optional<base::FilePath> offline_dir = FindOfflineDir(unpack_path);
if (offline_dir.has_value()) {
if (!cmd_line_args.append(L" --") ||
!cmd_line_args.append(base::SysUTF8ToWide(kOfflineDirSwitch).c_str()) ||
!cmd_line_args.append(L"=") ||
!cmd_line_args.append(offline_dir->BaseName().value().c_str())) {
return ProcessExitResult(COMMAND_STRING_OVERFLOW);
}
}
PathString setup_path;
if (!setup_path.assign(unpack_path.value().c_str()) ||
!setup_path.append(L"\\bin\\updater.exe")) {
exit_code = ProcessExitResult(PATH_STRING_OVERFLOW);
}
if (exit_code.IsSuccess()) {
exit_code = RunSetup(setup_path.get(), cmd_line_args.get());
// Because there is a race condition between the exit of the setup process
// and deleting its program file, the scoped temporary directory may fail
// to clean up the directory because the directory is not empty. To avoid
// this race condition, delete the program file before returning from the
// function.
base::TimeTicks deadline = base::TimeTicks::Now() + base::Seconds(5);
while (base::TimeTicks::Now() < deadline) {
if (base::DeleteFile(base::FilePath(setup_path.get()))) {
return exit_code;
}
base::PlatformThread::Sleep(base::Milliseconds(100));
}
VLOG(1) << "Setup file can leak on file system: " << setup_path.get();
}
return exit_code;
}
int WMain(HMODULE module) {
const ProcessExitResult result = InstallerMain(module);
VLOG(1) << "Metainstaller WMain returned: " << result.exit_code
<< ", Windows error: " << result.windows_error;
// Display UI only for metainstaller errors.
if (result.exit_code != SUCCESS_EXIT_CODE &&
result.exit_code != UPDATER_EXIT_CODE &&
!GetCommandLineLegacyCompatible().HasSwitch(kSilentSwitch)) {
base::FilePath exe_path;
base::PathService::Get(base::FILE_EXE, &exe_path);
::MessageBoxEx(nullptr,
GetLocalizedMetainstallerErrorString(result.exit_code,
result.windows_error)
.c_str(),
exe_path.BaseName().value().c_str(), 0, 0);
}
return result.exit_code == UPDATER_EXIT_CODE ? result.windows_error
: result.exit_code;
}
} // namespace updater