| // Copyright 2022 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/win/app_command_runner.h" |
| |
| #include <windows.h> |
| |
| #include <shellapi.h> |
| |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/base_paths_win.h" |
| #include "base/command_line.h" |
| #include "base/containers/span.h" |
| #include "base/files/file_path.h" |
| #include "base/logging.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/path_service.h" |
| #include "base/process/launch.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/string_util_impl_helpers.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/version.h" |
| #include "base/win/registry.h" |
| #include "base/win/scoped_localalloc.h" |
| #include "chrome/updater/updater_branding.h" |
| #include "chrome/updater/updater_scope.h" |
| #include "chrome/updater/util/win_util.h" |
| #include "chrome/updater/win/win_constants.h" |
| |
| namespace updater { |
| |
| namespace { |
| |
| // Loads the AppCommand under: |
| // Update\Clients\{`app_id`}\Commands\`command_id` |
| // REG_SZ "CommandLine" == {command format} |
| HRESULT LoadAppCommandFormat(UpdaterScope scope, |
| const std::wstring& app_id, |
| const std::wstring& command_id, |
| std::wstring& command_format) { |
| base::win::RegKey command_key; |
| HRESULT hr = HRESULT_FROM_WIN32(command_key.Open( |
| UpdaterScopeToHKeyRoot(scope), |
| GetAppCommandKey(app_id, command_id).c_str(), Wow6432(KEY_QUERY_VALUE))); |
| return SUCCEEDED(hr) ? HRESULT_FROM_WIN32(command_key.ReadValue( |
| kRegValueCommandLine, &command_format)) |
| : hr; |
| } |
| |
| // Loads the ProcessLauncher command in HKLM under: |
| // Update\Clients\{`app_id`} |
| // REG_SZ `command_id` == {command format} |
| // |
| // The legacy process launcher format is only supported for Google Chrome |
| // versions 110.0.5435.0 and below with the "cmd" command id. This is because |
| // the legacy process launcher command layout format can be used to interpret |
| // and/or execute unrelated registry entries. For instance, if the app_id is |
| // `{8A69D345-D564-463c-AFF1-A69D9E530F96}`, the older command would be |
| // registered under |
| // `SOFTWARE\Google\Update\Clients\{8A69D345-D564-463c-AFF1-A69D9E530F96}` |
| // REG_SZ `cmd`. Along with `cmd`, there are other properties of the app |
| // registered, such as the version "pv"="107.0.5304.107". So, `pv` is also a |
| // potential "command" for `IProcessLauncher`, which is unexpected. |
| HRESULT LoadLegacyProcessLauncherFormat(const std::wstring& app_id, |
| const std::wstring& command_id, |
| std::wstring& command_format) { |
| static constexpr wchar_t kAllowedLegacyProcessLauncherAppNamePrefix[] = |
| L"" BROWSER_PRODUCT_NAME_STRING; |
| static constexpr char kAllowedLegacyProcessLauncherMaxAppVersion[] = |
| "110.0.5435.0"; |
| static constexpr wchar_t kAllowedLegacyProcessLauncherCommandId[] = L"cmd"; |
| |
| std::wstring pv; |
| std::wstring name; |
| if (command_id == kAllowedLegacyProcessLauncherCommandId) { |
| base::win::RegKey app_key; |
| HRESULT hr = HRESULT_FROM_WIN32( |
| app_key.Open(HKEY_LOCAL_MACHINE, GetAppClientsKey(app_id).c_str(), |
| Wow6432(KEY_QUERY_VALUE))); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| |
| app_key.ReadValue(kRegValuePV, &pv); |
| app_key.ReadValue(kRegValueName, &name); |
| const base::Version app_version(base::WideToUTF8(pv)); |
| |
| if (app_version.IsValid() && |
| app_version.CompareTo( |
| base::Version(kAllowedLegacyProcessLauncherMaxAppVersion)) <= 0 && |
| base::StartsWith(name, kAllowedLegacyProcessLauncherAppNamePrefix)) { |
| return HRESULT_FROM_WIN32( |
| app_key.ReadValue(command_id.c_str(), &command_format)); |
| } |
| } |
| |
| LOG(WARNING) |
| << __func__ |
| << "Legacy ProcessLauncher format not supported, use more secure " |
| "AppCommand format: " |
| << app_id << ": " << pv << ": " << name << ": " << command_id; |
| return E_INVALIDARG; |
| } |
| |
| bool IsParentOf(int key, const base::FilePath& child) { |
| base::FilePath path; |
| return base::PathService::Get(key, &path) && path.IsParent(child); |
| } |
| |
| bool IsSecureAppCommandExePath(UpdaterScope scope, |
| const base::FilePath& exe_path) { |
| return exe_path.IsAbsolute() && |
| (!IsSystemInstall(scope) || |
| IsParentOf(base::DIR_PROGRAM_FILES, exe_path) || |
| IsParentOf(base::DIR_PROGRAM_FILESX86, exe_path) || |
| IsParentOf(base::DIR_PROGRAM_FILES6432, exe_path)); |
| } |
| |
| } // namespace |
| |
| AppCommandRunner::AppCommandRunner() = default; |
| AppCommandRunner::AppCommandRunner(const AppCommandRunner&) = default; |
| AppCommandRunner& AppCommandRunner::operator=(const AppCommandRunner&) = |
| default; |
| AppCommandRunner::~AppCommandRunner() = default; |
| |
| // static |
| HResultOr<AppCommandRunner> AppCommandRunner::LoadAppCommand( |
| UpdaterScope scope, |
| const std::wstring& app_id, |
| const std::wstring& command_id) { |
| std::wstring command_format; |
| HRESULT hr = LoadAppCommandFormat(scope, app_id, command_id, command_format); |
| if (FAILED(hr)) { |
| if (IsSystemInstall(scope)) { |
| hr = LoadLegacyProcessLauncherFormat(app_id, command_id, command_format); |
| } |
| if (FAILED(hr)) { |
| return base::unexpected(hr); |
| } |
| } |
| |
| AppCommandRunner app_command_runner; |
| hr = GetAppCommandFormatComponents(scope, command_format, |
| app_command_runner.executable_, |
| app_command_runner.parameters_); |
| if (FAILED(hr)) { |
| return base::unexpected(hr); |
| } |
| |
| return app_command_runner; |
| } |
| |
| // static |
| std::vector<AppCommandRunner> |
| AppCommandRunner::LoadAutoRunOnOsUpgradeAppCommands( |
| UpdaterScope scope, |
| const std::wstring& app_id) { |
| const HKEY root = UpdaterScopeToHKeyRoot(scope); |
| const std::wstring commands_key_name = GetAppCommandKey(app_id, L""); |
| |
| std::vector<AppCommandRunner> app_command_runners; |
| for (base::win::RegistryKeyIterator it(root, commands_key_name.c_str(), |
| KEY_WOW64_32KEY); |
| it.Valid(); ++it) { |
| const base::win::RegKey command_key( |
| root, base::StrCat({commands_key_name, it.Name()}).c_str(), |
| Wow6432(KEY_QUERY_VALUE)); |
| if (!command_key.Valid()) { |
| continue; |
| } |
| |
| DWORD auto_run = 0; |
| if (command_key.ReadValueDW(kRegValueAutoRunOnOSUpgrade, &auto_run) != |
| ERROR_SUCCESS || |
| !auto_run) { |
| continue; |
| } |
| |
| HResultOr<AppCommandRunner> runner = |
| LoadAppCommand(scope, app_id, it.Name()); |
| if (runner.has_value()) { |
| app_command_runners.push_back(*std::move(runner)); |
| } |
| } |
| |
| return app_command_runners; |
| } |
| |
| HRESULT AppCommandRunner::Run(base::span<const std::wstring> substitutions, |
| base::Process& process) const { |
| if (executable_.empty() || process.IsValid()) { |
| return E_UNEXPECTED; |
| } |
| |
| return ExecuteAppCommand(executable_, parameters_, substitutions, process); |
| } |
| |
| // static |
| HRESULT AppCommandRunner::StartProcess(const base::FilePath& executable, |
| const std::wstring& parameters, |
| base::Process& process) { |
| VLOG(2) << __func__ << ": " << executable << ": " << parameters; |
| |
| if (executable.empty() || process.IsValid()) { |
| return E_UNEXPECTED; |
| } |
| |
| // `executable` needs to be a full path to prevent `::CreateProcess` (which |
| // `base::LaunchProcess` uses internally) from using the search path for path |
| // resolution. |
| if (!executable.IsAbsolute()) { |
| LOG(ERROR) << __func__ << "!executable.IsAbsolute(): " << executable; |
| return E_INVALIDARG; |
| } |
| |
| base::LaunchOptions options = {}; |
| options.feedback_cursor_off = true; |
| options.start_hidden = true; |
| |
| process = base::LaunchProcess( |
| base::StrCat( |
| {base::CommandLine::QuoteForCommandLineToArgvW(executable.value()), |
| L" ", parameters}), |
| options); |
| if (!process.IsValid()) { |
| const HRESULT hr = HRESULTFromLastError(); |
| LOG(ERROR) << __func__ << "base::LaunchProcess failed: " << hr; |
| return hr; |
| } |
| |
| VLOG(2) << __func__ << "Started process with PID: " << process.Pid(); |
| return S_OK; |
| } |
| |
| // static |
| HRESULT AppCommandRunner::GetAppCommandFormatComponents( |
| UpdaterScope scope, |
| std::wstring command_format, |
| base::FilePath& executable, |
| std::vector<std::wstring>& parameters) { |
| VLOG(2) << __func__ << ": " << scope << ": " << command_format; |
| |
| int num_args = 0; |
| base::win::ScopedLocalAllocTyped<wchar_t*> argv( |
| ::CommandLineToArgvW(&command_format[0], &num_args)); |
| if (!argv || num_args < 1) { |
| LOG(ERROR) << __func__ << "!argv || num_args < 1: " << num_args; |
| return E_INVALIDARG; |
| } |
| |
| // SAFETY: the unsafe buffer is present due to the ::CommandLineToArgvW call. |
| // When constructing the span, `num_args` is validated and checked as a valid |
| // size_t value. |
| UNSAFE_BUFFERS(const base::span<wchar_t*> safe_args{ |
| argv.get(), base::checked_cast<size_t>(num_args)}); |
| |
| const base::FilePath exe(safe_args[0]); |
| if (!IsSecureAppCommandExePath(scope, exe)) { |
| LOG(WARNING) << __func__ |
| << ": !IsSecureAppCommandExePath(scope, exe): " << exe; |
| return E_INVALIDARG; |
| } |
| |
| executable = exe; |
| parameters.clear(); |
| for (size_t i = 1; i < safe_args.size(); ++i) { |
| parameters.push_back(safe_args[i]); |
| } |
| |
| return S_OK; |
| } |
| |
| // static |
| std::optional<std::wstring> AppCommandRunner::FormatParameter( |
| const std::wstring& parameter, |
| base::span<const std::wstring> substitutions) { |
| return base::internal::DoReplaceStringPlaceholders( |
| /*format_string=*/parameter, /*subst=*/substitutions, |
| /*placeholder_prefix=*/L'%', |
| /*should_escape_multiple_placeholder_prefixes=*/false, |
| /*is_strict_mode=*/true, /*offsets=*/nullptr); |
| } |
| |
| // static |
| std::optional<std::wstring> AppCommandRunner::FormatAppCommandLine( |
| const std::vector<std::wstring>& parameters, |
| base::span<const std::wstring> substitutions) { |
| std::wstring formatted_command_line; |
| for (size_t i = 0; i < parameters.size(); ++i) { |
| std::optional<std::wstring> formatted_parameter = |
| FormatParameter(parameters[i], substitutions); |
| if (!formatted_parameter) { |
| VLOG(1) << __func__ << " FormatParameter failed: " << parameters[i] |
| << ": " << substitutions.size(); |
| return std::nullopt; |
| } |
| |
| static constexpr wchar_t kQuotableCharacters[] = L" \t\\\""; |
| formatted_command_line.append( |
| formatted_parameter->find_first_of(kQuotableCharacters) == |
| std::wstring::npos |
| ? *formatted_parameter // no quoting needed, use as-is. |
| : base::CommandLine::QuoteForCommandLineToArgvW( |
| *formatted_parameter)); |
| |
| if (i + 1 < parameters.size()) { |
| formatted_command_line.push_back(L' '); |
| } |
| } |
| |
| return formatted_command_line; |
| } |
| |
| // static |
| HRESULT AppCommandRunner::ExecuteAppCommand( |
| const base::FilePath& executable, |
| const std::vector<std::wstring>& parameters, |
| base::span<const std::wstring> substitutions, |
| base::Process& process) { |
| VLOG(2) << __func__ << ": " << executable << ": " |
| << base::JoinString(parameters, L",") << " : " |
| << base::JoinString(substitutions, L","); |
| |
| const std::optional<std::wstring> command_line_parameters = |
| FormatAppCommandLine(parameters, substitutions); |
| if (!command_line_parameters) { |
| LOG(ERROR) << __func__ << "!command_line_parameters"; |
| return E_INVALIDARG; |
| } |
| |
| return StartProcess(executable, command_line_parameters.value(), process); |
| } |
| |
| } // namespace updater |