blob: e260791c54960638c1a30a9a09e53a940f964e78 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "remoting/host/linux/migrate_host_main.h"
#include <unistd.h>
#include <iostream>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/at_exit.h"
#include "base/command_line.h"
#include "base/containers/span.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/functional/bind.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/message_loop/message_pump_type.h"
#include "base/no_destructor.h"
#include "base/notimplemented.h"
#include "base/notreached.h"
#include "base/process/launch.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/task/single_thread_task_executor.h"
#include "base/values.h"
#include "remoting/base/branding.h"
#include "remoting/base/file_path_util_linux.h"
#include "remoting/base/logging.h"
#include "remoting/base/passwd_utils.h"
#include "remoting/base/username.h"
#include "remoting/host/base/host_exit_codes.h"
#include "remoting/host/host_config.h"
#include "remoting/host/linux/host_types.h"
#include "remoting/host/pairing_registry_delegate_linux.h"
#include "remoting/host/setup/daemon_controller.h"
#include "remoting/host/setup/daemon_controller_delegate_linux_multi_process.h"
#include "remoting/host/setup/daemon_controller_delegate_linux_single_process.h"
namespace remoting {
namespace {
// Internal switches that are not meant to be used by users directly.
constexpr char kUserNameSwitch[] = "user-name";
constexpr char kGetMultiProcessConfigSwitch[] = "get-multi-process-config";
constexpr char kCleanupMultiProcessConfigSwitch[] =
"cleanup-multi-process-config";
constexpr char kSaveMultiProcessPairingsSwitch[] =
"save-multi-process-pairings";
constexpr char kKeepConfigSwitch[] = "keep-config";
// Runs the current program with sudo.
// `args`: The arguments to pass to the program.
// `user_name`: If not empty, the program will be run as this user (sudo -u).
// `input`: If not empty, this string will be written to the child's stdin.
// `output`: If not null, the child's stdout will be written to this string.
bool RunWithSudo(base::span<const std::string_view> args,
std::string_view user_name = {},
std::string_view input = {},
std::string* output = nullptr) {
base::CommandLine command_line(base::CommandLine::NO_PROGRAM);
std::vector<std::string> full_args{"/usr/bin/sudo"};
if (!user_name.empty()) {
full_args.push_back("-u");
full_args.emplace_back(user_name);
}
full_args.push_back("--");
full_args.push_back(
base::CommandLine::ForCurrentProcess()->GetProgram().value());
full_args.insert(full_args.end(), args.begin(), args.end());
command_line.InitFromArgv(full_args);
base::LaunchOptions options;
options.allow_new_privs = true;
base::ScopedFD stdin_read_fd;
base::ScopedFD stdin_write_fd;
if (!input.empty()) {
if (!base::CreatePipe(&stdin_read_fd, &stdin_write_fd)) {
PLOG(ERROR) << "Failed to create stdin pipe";
return false;
}
options.fds_to_remap.emplace_back(stdin_read_fd.get(), STDIN_FILENO);
}
base::ScopedFD stdout_read_fd;
base::ScopedFD stdout_write_fd;
if (output) {
if (!base::CreatePipe(&stdout_read_fd, &stdout_write_fd)) {
PLOG(ERROR) << "Failed to create stdout pipe";
return false;
}
options.fds_to_remap.emplace_back(stdout_write_fd.get(), STDOUT_FILENO);
}
base::Process process = base::LaunchProcess(command_line, options);
if (!process.IsValid()) {
LOG(ERROR) << "Failed to launch process: "
<< command_line.GetCommandLineString();
return false;
}
if (!input.empty()) {
// Close the read end in the parent process.
stdin_read_fd.reset();
if (!base::WriteFileDescriptor(stdin_write_fd.get(), input)) {
PLOG(ERROR) << "Failed to write to pipe: "
<< command_line.GetCommandLineString();
return false;
}
// Close the write end to signal EOF.
stdin_write_fd.reset();
}
if (output) {
// Close the write end in the parent process.
stdout_write_fd.reset();
base::File stdout_file(stdout_read_fd.release());
base::ScopedFILE stdout_stream(
base::FileToFILE(std::move(stdout_file), "r"));
if (!stdout_stream ||
!base::ReadStreamToString(stdout_stream.get(), output)) {
PLOG(ERROR) << "Failed to read from pipe: "
<< command_line.GetCommandLineString();
return false;
}
}
int exit_code = -1;
if (!process.WaitForExit(&exit_code)) {
LOG(ERROR) << "Failed to wait for process exit: "
<< command_line.GetCommandLineString();
return false;
}
if (exit_code == -1) {
LOG(ERROR) << "Process terminated unexpectedly: "
<< command_line.GetCommandLineString();
return false;
}
if (exit_code != 0) {
LOG(ERROR) << "Process exited with status " << exit_code << ": "
<< command_line.GetCommandLineString();
return false;
}
return true;
}
bool DisableService(const std::string& unit_name) {
std::cout << "Disabling service: " << unit_name << "\n";
base::LaunchOptions options;
base::CommandLine command_line(base::CommandLine::NO_PROGRAM);
command_line.InitFromArgv({"systemctl", "disable", "--now", unit_name});
if (getuid() != 0) {
options.allow_new_privs = true;
command_line.PrependWrapper("/usr/bin/sudo");
}
int exit_code = -1;
auto process = base::LaunchProcess(command_line, options);
if (!process.IsValid() || !process.WaitForExit(&exit_code)) {
std::cerr << "Failed to disable host service (" << unit_name << ").\n";
return false;
}
if (exit_code == -1) {
std::cerr << "Service disable process terminated unexpectedly: "
<< command_line.GetCommandLineString() << "\n";
return false;
}
if (exit_code != 0) {
std::cerr << "Service disable process exited with status " << exit_code
<< ": " << command_line.GetCommandLineString() << "\n";
return false;
}
return true;
}
void OnDaemonControllerDone(DaemonController::AsyncResult* out_result,
base::OnceClosure quit_closure,
DaemonController::AsyncResult result) {
*out_result = result;
std::move(quit_closure).Run();
}
bool GetMultiProcessConfigAsRoot() {
CHECK_EQ(getuid(), 0u);
base::FilePath config_path =
DaemonControllerDelegateLinuxMultiProcess::GetPrivilegedConfigPath();
std::optional<base::DictValue> config = HostConfigFromJsonFile(config_path);
if (!config) {
std::cerr << "Failed to read multi-process host config.\n";
return false;
}
PairingRegistryDelegateLinux pairing_delegate(
PairingRegistryDelegateLinux::GetDefaultRegistryPath(),
/*use_unprivileged_file=*/false);
base::ListValue pairings = pairing_delegate.LoadAll();
base::DictValue result;
result.Set("host_config", std::move(*config));
result.Set("pairings", std::move(pairings));
std::string json;
if (!base::JSONWriter::Write(result, &json)) {
std::cerr << "Failed to serialize multi-process state to JSON.\n";
return false;
}
std::cout << json << std::endl;
return true;
}
bool CleanupMultiProcessConfigAsRoot() {
CHECK_EQ(getuid(), 0u);
std::cout << "Cleaning up multi-process host config.\n";
bool success = true;
base::FilePath privileged_config =
DaemonControllerDelegateLinuxMultiProcess::GetPrivilegedConfigPath();
if (base::PathExists(privileged_config) &&
!base::DeleteFile(privileged_config)) {
std::cerr << "Failed to delete privileged config file: "
<< privileged_config.value() << "\n";
success = false;
// We continue even if the privileged config can't be deleted, to delete
// other files, since the single-process host has already started.
}
base::FilePath unprivileged_config =
DaemonControllerDelegateLinuxMultiProcess::GetUnprivilegedConfigPath();
if (base::PathExists(unprivileged_config) &&
!base::DeleteFile(unprivileged_config)) {
std::cerr << "Failed to delete unprivileged config file: "
<< unprivileged_config.value() << "\n";
success = false;
}
base::FilePath pairing_dir =
PairingRegistryDelegateLinux::GetDefaultRegistryPath();
if (base::PathExists(pairing_dir) &&
!base::DeletePathRecursively(pairing_dir)) {
std::cerr << "Failed to delete pairing directory: " << pairing_dir.value()
<< "\n";
success = false;
}
return success;
}
bool SaveMultiProcessPairingsAsNetworkUser() {
CHECK_EQ(GetUsername(), GetNetworkProcessUsername());
std::string json;
if (!base::ReadStreamToString(stdin, &json)) {
std::cerr << "Failed to read pairings from stdin.\n";
return false;
}
std::optional<base::Value> pairings_value =
base::JSONReader::Read(json, base::JSON_PARSE_RFC);
if (!pairings_value || !pairings_value->is_list()) {
std::cerr << "Failed to parse pairings JSON from stdin.\n";
return false;
}
// Save pairings to multi-process registry.
PairingRegistryDelegateLinux multi_pairing_delegate;
for (const auto& pairing_value : pairings_value->GetList()) {
if (!pairing_value.is_dict()) {
continue;
}
if (!multi_pairing_delegate.Save(
protocol::PairingRegistry::Pairing::CreateFromValue(
pairing_value.GetDict()))) {
std::cerr << "Failed to save a pairing to multi-process registry.\n";
}
}
return true;
}
bool MigrateToMultiProcess(const base::CommandLine& command_line) {
if (getuid() != 0) {
std::string user_name_switch =
base::StringPrintf("--%s=%s", kUserNameSwitch, GetUsername().c_str());
const base::CommandLine::StringVector& args = command_line.GetArgs();
CHECK_GE(args.size(), 1u);
std::string_view host_type = args[0];
// The code below the if clause will be executed in the child process
// elevated with sudo.
if (!RunWithSudo({host_type, user_name_switch})) {
return false;
}
if (command_line.HasSwitch(kKeepConfigSwitch)) {
std::cout << "Keeping old single-process config as requested.\n";
} else {
// Deleting the old pairing directory as the user.
std::cout << "Deleting old single-process config.\n";
base::FilePath user_config_dir = GetConfigDir();
base::FilePath config_file =
user_config_dir.Append(GetHostHash() + ".json");
base::FilePath user_pairing_dir = user_config_dir.Append(
PairingRegistryDelegateLinux::kRegistryDirectory);
if (base::PathExists(user_pairing_dir) &&
!base::DeletePathRecursively(user_pairing_dir)) {
std::cerr << "Failed to delete old pairing directory.\n";
return false;
}
// Ideally we should delete the old host config file, but an internal
// service will re-provision the host if it detects that the config file
// no longer exists. For now we just clear its content to prevent the
// single-process host from accidentally running.
// TODO: b/495898776 - just delete the file once the tooling is fixed.
if (base::PathExists(config_file) && !base::WriteFile(config_file, "")) {
std::cerr << "Failed to clear old host config file.\n";
return false;
}
}
std::cout << "Successfully migrated to multi-process host.\n";
return true;
}
// Code below is run as root.
std::cout << "Migrating from single-process host to multi-process host.\n";
std::string user_name = command_line.GetSwitchValueASCII(kUserNameSwitch);
if (user_name.empty()) {
std::cerr << "--" << kUserNameSwitch
<< " must be provided when run as root.\n";
return false;
}
auto user_info = GetPasswdUserInfo(user_name);
if (!user_info.has_value()) {
std::cerr << "Failed to look up user: " << user_name << ": "
<< user_info.error() << "\n";
return false;
}
base::FilePath user_config_dir =
user_info->home_dir.Append(GetPerUserConfigRelativeDir());
base::FilePath config_file = user_config_dir.Append(GetHostHash() + ".json");
// The privileged config files are only readable by root.
std::optional<base::DictValue> config = HostConfigFromJsonFile(config_file);
if (!config) {
std::cerr << "Failed to read single-process host config at "
<< config_file.value() << "\n";
return false;
}
base::FilePath user_pairing_dir =
user_config_dir.Append(PairingRegistryDelegateLinux::kRegistryDirectory);
PairingRegistryDelegateLinux user_pairing_delegate(
user_pairing_dir, /*use_unprivileged_file=*/false);
base::ListValue pairings = user_pairing_delegate.LoadAll();
// Code below ensures the multi-process pairing registry directory exists
// and has correct ownership and permissions.
if (!PairingRegistryDelegateLinux::SetupMultiProcessPairingRegistry()) {
return false;
}
std::string pairings_json;
if (!base::JSONWriter::Write(pairings, &pairings_json)) {
std::cerr << "Failed to serialize pairings to JSON.\n";
return false;
}
std::cout << "Saving pairings to multi-process registry.\n";
// We do it in a child process as the network user to ensure the files and
// directories are created with the correct owner and permissions.
if (!RunWithSudo({"--" + std::string(kSaveMultiProcessPairingsSwitch)},
GetNetworkProcessUsername(), pairings_json)) {
std::cerr << "Failed to save pairings to multi-process registry.\n";
return false;
}
auto daemon_controller = DaemonController::Create();
base::RunLoop run_loop;
DaemonController::AsyncResult result = DaemonController::RESULT_FAILED;
std::cout << "Disabling single-process host.\n";
if (!DisableService("chrome-remote-desktop@" + user_name + ".service")) {
return false;
}
std::cout << "Starting multi-process host.\n";
// Note: the `consent` parameter is not used in Linux, and the
// `usage_stats_consent` value in the host config is used instead.
daemon_controller->SetConfigAndStart(
std::move(*config), /*consent=*/true,
base::BindOnce(&OnDaemonControllerDone, &result, run_loop.QuitClosure()));
run_loop.Run();
if (result != DaemonController::RESULT_OK) {
std::cerr << "Failed to start multi-process host.\n";
return false;
}
return true;
}
bool MigrateToSingleProcess(const base::CommandLine& command_line) {
if (getuid() == 0) {
std::cerr << "Migration to single-process host cannot be run as root.\n";
return false;
}
std::cout << "Migrating from multi-process host to single-process host.\n";
std::cout << "Elevating to read current multi-process config.\n";
// This is needed because the privileged config files are only readable by
// root.
std::string output;
if (!RunWithSudo({"--" + std::string(kGetMultiProcessConfigSwitch)},
/*user_name=*/{}, /*input=*/{}, &output)) {
std::cerr << "Failed to get multi-process config.\n";
return false;
}
std::optional<base::Value> multi_state =
base::JSONReader::Read(output, base::JSON_PARSE_RFC);
if (!multi_state || !multi_state->is_dict()) {
std::cerr << "Failed to parse multi-process config JSON.\n";
return false;
}
const base::DictValue& multi_dict = multi_state->GetDict();
const base::DictValue* host_config = multi_dict.FindDict("host_config");
const base::ListValue* pairings = multi_dict.FindList("pairings");
if (!host_config || !pairings) {
std::cerr << "Multi-process config JSON is missing fields.\n";
return false;
}
// Save pairings to single-process registry.
PairingRegistryDelegateLinux user_pairing_delegate;
for (const auto& pairing_value : *pairings) {
if (!pairing_value.is_dict()) {
continue;
}
user_pairing_delegate.Save(
protocol::PairingRegistry::Pairing::CreateFromValue(
pairing_value.GetDict()));
}
std::cout << "Disabling multi-process host.\n";
if (!DisableService("chrome-remote-desktop.service")) {
return false;
}
auto daemon_controller = DaemonController::Create();
base::RunLoop run_loop;
DaemonController::AsyncResult result = DaemonController::RESULT_FAILED;
std::cout << "Starting single-process host.\n";
// Note: the `consent` parameter is not used in Linux, and the
// `usage_stats_consent` value in the host config is used instead.
daemon_controller->SetConfigAndStart(
host_config->Clone(), /*consent=*/true,
base::BindOnce(&OnDaemonControllerDone, &result, run_loop.QuitClosure()));
run_loop.Run();
if (result != DaemonController::RESULT_OK) {
std::cerr << "Failed to start single-process host.\n";
return false;
}
if (command_line.HasSwitch(kKeepConfigSwitch)) {
std::cout << "Keeping multi-process host state as requested.\n";
} else {
std::cout << "Elevating to clean up multi-process host state.\n";
if (!RunWithSudo({"--" + std::string(kCleanupMultiProcessConfigSwitch)})) {
std::cerr << "Failed to clean up multi-process host state.\n";
return false;
}
}
std::cout << "Successfully migrated to single-process host.\n";
return true;
}
} // namespace
int MigrateHostMain(int argc, char** argv) {
base::AtExitManager exit_manager;
base::SingleThreadTaskExecutor io_task_executor(base::MessagePumpType::IO);
base::CommandLine::Init(argc, argv);
InitHostLogging();
const base::CommandLine& command_line =
*base::CommandLine::ForCurrentProcess();
if (command_line.HasSwitch(kGetMultiProcessConfigSwitch)) {
if (getuid() != 0) {
std::cerr << "--" << kGetMultiProcessConfigSwitch
<< " must be run as root.\n";
return -1;
}
return GetMultiProcessConfigAsRoot() ? 0 : -1;
}
if (command_line.HasSwitch(kCleanupMultiProcessConfigSwitch)) {
if (getuid() != 0) {
std::cerr << "--" << kCleanupMultiProcessConfigSwitch
<< " must be run as root.\n";
return -1;
}
return CleanupMultiProcessConfigAsRoot() ? 0 : -1;
}
if (command_line.HasSwitch(kSaveMultiProcessPairingsSwitch)) {
return SaveMultiProcessPairingsAsNetworkUser() ? 0 : -1;
}
const base::CommandLine::StringVector& args = command_line.GetArgs();
if (args.empty()) {
std::cerr << "Usage: migrate_host <host_type> [--keep-config]\n\n";
// Internal switches are not documented here.
HostType::PrintHostTypeHelp();
return -1;
}
std::string host_type_str = args[0];
const HostType* target_host_type = HostType::Find(host_type_str);
if (!target_host_type) {
std::cerr << "Unknown host type: " << host_type_str << "\n\n";
HostType::PrintHostTypeHelp();
return -1;
}
// Skip the state check when run as root and --user-name is set. This only
// happens when the process is elevated from the user process and the state
// check has already been performed. The state check won't work for root
// because the single-process host is not run as root.
if (getuid() != 0 || !command_line.HasSwitch(kUserNameSwitch)) {
scoped_refptr<DaemonController> daemon_controller =
DaemonController::Create();
if (daemon_controller->GetState() != DaemonController::STATE_STARTED) {
std::cerr << "Host migration can only be performed when the host is "
"already started.\n";
return -1;
}
bool is_multi_process = daemon_controller->is_multi_process();
if (is_multi_process == target_host_type->is_multi_process()) {
// We will add more multi-process host types, but for now there is nothing
// to do.
std::cout << "Host is already "
<< (is_multi_process ? "multi-process" : "single-process")
<< ". Nothing to do.\n";
return 0;
}
}
bool success = false;
DaemonController::SetHostType(target_host_type);
if (target_host_type->is_multi_process()) {
success = MigrateToMultiProcess(command_line);
} else {
success = MigrateToSingleProcess(command_line);
}
return success ? 0 : -1;
}
} // namespace remoting