// 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/browser/ash/bruschetta/bruschetta_installer_impl.h"
#include <memory>
#include "base/check.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/thread_pool.h"
#include "bruschetta_installer.h"
#include "chrome/browser/ash/bruschetta/bruschetta_download.h"
#include "chrome/browser/ash/bruschetta/bruschetta_installer.h"
#include "chrome/browser/ash/bruschetta/bruschetta_pref_names.h"
#include "chrome/browser/ash/bruschetta/bruschetta_service.h"
#include "chrome/browser/ash/bruschetta/bruschetta_util.h"
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chrome/browser/ash/guest_os/guest_id.h"
#include "chrome/browser/ash/guest_os/guest_os_dlc_helper.h"
#include "chrome/browser/ash/guest_os/guest_os_terminal.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_key.h"
#include "chromeos/ash/components/dbus/attestation/attestation_client.h"
#include "chromeos/ash/components/dbus/concierge/concierge_client.h"
#include "components/prefs/pref_service.h"
namespace bruschetta {
// Also referenced by BruschettaInstallerTest.
extern const char kInstallResultMetric[] = "Bruschetta.InstallResult";
namespace {
// The vTPM EK key label.
// Should be synced with the value in the chromiumos repo:
// src/platform2/vtpm/backends/attested_virtual_endorsement.cc
constexpr char kVtpmEkLabel[] = "vtpm-ek";
std::unique_ptr<BruschettaInstallerImpl::Fds> OpenFdsBlocking(
base::FilePath boot_disk_path,
base::FilePath pflash_path,
base::FilePath profile_path);
} // namespace
struct BruschettaInstallerImpl::Fds {
base::ScopedFD boot_disk;
std::optional<base::ScopedFD> pflash;
};
BruschettaInstallerImpl::BruschettaInstallerImpl(
Profile* profile,
base::OnceClosure close_closure)
: profile_(profile), close_closure_(std::move(close_closure)) {}
BruschettaInstallerImpl::~BruschettaInstallerImpl() = default;
bool BruschettaInstallerImpl::MaybeClose() {
if (!install_running_) {
if (close_closure_) {
std::move(close_closure_).Run();
}
return true;
}
return false;
}
void BruschettaInstallerImpl::Cancel() {
if (MaybeClose()) {
return;
}
install_running_ = false;
}
void BruschettaInstallerImpl::Install(std::string vm_name,
std::string config_id) {
if (install_running_) {
LOG(ERROR) << "Install requested while an install is already running";
return;
}
auto new_guest_id = MakeBruschettaId(config_id);
for (const auto& guest_id :
guest_os::GetContainers(profile_, guest_os::VmType::BRUSCHETTA)) {
if (guest_id == new_guest_id) {
Error(BruschettaInstallResult::kVmAlreadyExists);
LOG(ERROR) << "Tried to install a VM that already exists";
return;
}
}
NotifyObserver(State::kInstallStarted);
install_running_ = true;
auto config_ptr = GetInstallableConfig(profile_, config_id);
if (config_ptr.has_value()) {
config_ = config_ptr.value()->Clone();
config_id_ = std::move(config_id);
vm_name_ = std::move(vm_name);
InstallToolsDlc();
} else {
install_running_ = false;
Error(BruschettaInstallResult::kInstallationProhibited);
LOG(ERROR) << "Installation prohibited by policy";
return;
}
// Reset mic permissions, as these should not persist across reinstall.
profile_->GetPrefs()->SetBoolean(prefs::kBruschettaMicAllowed, false);
}
void BruschettaInstallerImpl::InstallToolsDlc() {
VLOG(2) << "Installing tools DLC";
NotifyObserver(State::kToolsDlcInstall);
in_progress_dlc_ = std::make_unique<guest_os::GuestOsDlcInstallation>(
kToolsDlc,
base::BindOnce(&BruschettaInstallerImpl::OnToolsDlcInstalled,
weak_ptr_factory_.GetWeakPtr()),
base::DoNothing());
}
void BruschettaInstallerImpl::OnToolsDlcInstalled(
guest_os::GuestOsDlcInstallation::Result install_result) {
in_progress_dlc_.reset();
if (MaybeClose()) {
return;
}
if (!install_result.has_value()) {
install_running_ = false;
BruschettaInstallResult result;
switch (install_result.error()) {
case guest_os::GuestOsDlcInstallation::Error::Offline:
result = BruschettaInstallResult::kToolsDlcOfflineError;
break;
case guest_os::GuestOsDlcInstallation::Error::NeedUpdate:
result = BruschettaInstallResult::kToolsDlcNeedUpdateError;
break;
case guest_os::GuestOsDlcInstallation::Error::NeedReboot:
result = BruschettaInstallResult::kToolsDlcNeedRebootError;
break;
case guest_os::GuestOsDlcInstallation::Error::DiskFull:
result = BruschettaInstallResult::kToolsDlcDiskFullError;
break;
case guest_os::GuestOsDlcInstallation::Error::Busy:
result = BruschettaInstallResult::kToolsDlcBusyError;
break;
case guest_os::GuestOsDlcInstallation::Error::Internal:
case guest_os::GuestOsDlcInstallation::Error::Invalid:
case guest_os::GuestOsDlcInstallation::Error::UnknownFailure:
case guest_os::GuestOsDlcInstallation::Error::Cancelled:
default:
result = BruschettaInstallResult::kToolsDlcUnknownError;
break;
}
Error(result);
LOG(ERROR) << "Failed to install tools dlc: " << install_result.error();
return;
}
InstallFirmwareDlc();
}
void BruschettaInstallerImpl::InstallFirmwareDlc() {
VLOG(2) << "Installing firmware DLC";
NotifyObserver(State::kFirmwareDlcInstall);
in_progress_dlc_ = std::make_unique<guest_os::GuestOsDlcInstallation>(
kUefiDlc,
base::BindOnce(&BruschettaInstallerImpl::OnFirmwareDlcInstalled,
weak_ptr_factory_.GetWeakPtr()),
base::DoNothing());
}
void BruschettaInstallerImpl::OnFirmwareDlcInstalled(
guest_os::GuestOsDlcInstallation::Result install_result) {
if (MaybeClose()) {
return;
}
if (!install_result.has_value()) {
install_running_ = false;
BruschettaInstallResult result;
switch (install_result.error()) {
case guest_os::GuestOsDlcInstallation::Error::Offline:
result = BruschettaInstallResult::kFirmwareDlcOfflineError;
break;
case guest_os::GuestOsDlcInstallation::Error::NeedUpdate:
result = BruschettaInstallResult::kFirmwareDlcNeedUpdateError;
break;
case guest_os::GuestOsDlcInstallation::Error::NeedReboot:
result = BruschettaInstallResult::kFirmwareDlcNeedRebootError;
break;
case guest_os::GuestOsDlcInstallation::Error::DiskFull:
result = BruschettaInstallResult::kFirmwareDlcDiskFullError;
break;
case guest_os::GuestOsDlcInstallation::Error::Busy:
result = BruschettaInstallResult::kFirmwareDlcBusyError;
break;
case guest_os::GuestOsDlcInstallation::Error::Internal:
case guest_os::GuestOsDlcInstallation::Error::Invalid:
case guest_os::GuestOsDlcInstallation::Error::UnknownFailure:
case guest_os::GuestOsDlcInstallation::Error::Cancelled:
default:
result = BruschettaInstallResult::kFirmwareDlcUnknownError;
break;
}
Error(result);
LOG(ERROR) << "Failed to install firmware dlc: " << install_result.error();
return;
}
DownloadBootDisk();
}
void BruschettaInstallerImpl::DownloadBootDisk() {
VLOG(2) << "Downloading boot disk";
NotifyObserver(State::kBootDiskDownload);
const std::string* url = config_.FindDict(prefs::kPolicyImageKey)
->FindString(prefs::kPolicyURLKey);
boot_disk_download_ = download_factory_.Run();
boot_disk_download_->StartDownload(
profile_, GURL(*url),
base::BindOnce(&bruschetta::BruschettaInstallerImpl::OnBootDiskDownloaded,
weak_ptr_factory_.GetWeakPtr()));
}
void BruschettaInstallerImpl::OnBootDiskDownloaded(base::FilePath path,
std::string hash) {
if (MaybeClose()) {
return;
}
if (path.empty()) {
install_running_ = false;
Error(BruschettaInstallResult::kDownloadError);
return;
}
const std::string* expected = config_.FindDict(prefs::kPolicyImageKey)
->FindString(prefs::kPolicyHashKey);
if (!base::EqualsCaseInsensitiveASCII(hash, *expected)) {
install_running_ = false;
Error(BruschettaInstallResult::kInvalidBootDisk);
LOG(ERROR) << "Downloaded boot disk has incorrect hash";
LOG(ERROR) << "Actual " << hash;
LOG(ERROR) << "Expected " << expected;
return;
}
boot_disk_path_ = path;
DownloadPflash();
}
void BruschettaInstallerImpl::DownloadPflash() {
VLOG(2) << "Downloading pflash";
NotifyObserver(State::kPflashDownload);
const base::Value::Dict* pflash = config_.FindDict(prefs::kPolicyPflashKey);
if (!pflash) {
VLOG(2) << "No pflash file set, skipping to OpenFds";
OpenFds();
return;
}
const std::string* url = pflash->FindString(prefs::kPolicyURLKey);
pflash_download_ = download_factory_.Run();
pflash_download_->StartDownload(
profile_, GURL(*url),
base::BindOnce(&bruschetta::BruschettaInstallerImpl::OnPflashDownloaded,
weak_ptr_factory_.GetWeakPtr()));
}
void BruschettaInstallerImpl::OnPflashDownloaded(base::FilePath path,
std::string hash) {
if (MaybeClose()) {
return;
}
if (path.empty()) {
install_running_ = false;
Error(BruschettaInstallResult::kDownloadError);
return;
}
const std::string* expected = config_.FindDict(prefs::kPolicyPflashKey)
->FindString(prefs::kPolicyHashKey);
if (!base::EqualsCaseInsensitiveASCII(hash, *expected)) {
install_running_ = false;
Error(BruschettaInstallResult::kInvalidPflash);
LOG(ERROR) << "Downloaded pflash has incorrect hash";
LOG(ERROR) << "Actual " << hash;
LOG(ERROR) << "Expected " << expected;
return;
}
pflash_path_ = path;
OpenFds();
}
void BruschettaInstallerImpl::OpenFds() {
VLOG(2) << "Opening fds";
NotifyObserver(State::kOpenFiles);
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()},
base::BindOnce(&OpenFdsBlocking, boot_disk_path_, pflash_path_,
profile_->GetPath()),
base::BindOnce(&BruschettaInstallerImpl::OnOpenFds,
weak_ptr_factory_.GetWeakPtr()));
}
namespace {
std::unique_ptr<BruschettaInstallerImpl::Fds> OpenFdsBlocking(
base::FilePath boot_disk_path,
base::FilePath pflash_path,
base::FilePath profile_path) {
base::File boot_disk(boot_disk_path,
base::File::FLAG_OPEN | base::File::FLAG_READ);
if (!boot_disk.IsValid()) {
PLOG(ERROR) << "Failed to open boot disk";
return nullptr;
}
std::optional<base::ScopedFD> pflash_fd;
if (pflash_path.empty()) {
pflash_fd = std::nullopt;
} else {
base::File pflash(pflash_path,
base::File::FLAG_OPEN | base::File::FLAG_READ);
if (!pflash.IsValid()) {
PLOG(ERROR) << "Failed to open pflash";
return nullptr;
}
pflash_fd = base::ScopedFD(pflash.TakePlatformFile());
}
BruschettaInstallerImpl::Fds fds{
.boot_disk = base::ScopedFD(boot_disk.TakePlatformFile()),
.pflash = std::move(pflash_fd),
};
return std::make_unique<BruschettaInstallerImpl::Fds>(std::move(fds));
}
} // namespace
void BruschettaInstallerImpl::OnOpenFds(std::unique_ptr<Fds> fds) {
if (MaybeClose()) {
return;
}
if (!fds) {
install_running_ = false;
Error(BruschettaInstallResult::kUnableToOpenImages);
LOG(ERROR) << "Failed to open image files";
return;
}
fds_ = std::move(fds);
EnsureConciergeAvailable();
}
void BruschettaInstallerImpl::EnsureConciergeAvailable() {
auto* client = ash::ConciergeClient::Get();
DCHECK(client) << "This code requires a ConciergeClient";
client->WaitForServiceToBeAvailable(
base::BindOnce(&BruschettaInstallerImpl::OnConciergeAvailable,
weak_ptr_factory_.GetWeakPtr()));
}
void BruschettaInstallerImpl::OnConciergeAvailable(bool service_is_available) {
if (!service_is_available) {
install_running_ = false;
Error(BruschettaInstallResult::kConciergeUnavailableError);
LOG(ERROR) << "vm_concierge is not available";
return;
}
CreateVmDisk();
}
void BruschettaInstallerImpl::CreateVmDisk() {
VLOG(2) << "Creating VM disk";
NotifyObserver(State::kCreateVmDisk);
auto* client = ash::ConciergeClient::Get();
DCHECK(client) << "This code requires a ConciergeClient";
std::string user_hash =
ash::ProfileHelper::GetUserIdHashFromProfile(profile_);
vm_tools::concierge::CreateDiskImageRequest request;
request.set_cryptohome_id(std::move(user_hash));
request.set_vm_name(vm_name_);
request.set_image_type(vm_tools::concierge::DiskImageType::DISK_IMAGE_AUTO);
request.set_storage_ballooning(true);
client->CreateDiskImage(
request, base::BindOnce(&BruschettaInstallerImpl::OnCreateVmDisk,
weak_ptr_factory_.GetWeakPtr()));
}
void BruschettaInstallerImpl::OnCreateVmDisk(
std::optional<vm_tools::concierge::CreateDiskImageResponse> result) {
if (MaybeClose()) {
return;
}
if (!result ||
result->status() !=
vm_tools::concierge::DiskImageStatus::DISK_STATUS_CREATED) {
install_running_ = false;
Error(BruschettaInstallResult::kCreateDiskError);
if (result) {
LOG(ERROR) << "Create VM disk failed: " << result->failure_reason();
} else {
LOG(ERROR) << "Create VM disk failed, no response";
}
return;
}
disk_path_ = result->disk_path();
InstallPflash();
}
void BruschettaInstallerImpl::InstallPflash() {
VLOG(2) << "Installing pflash file for VM";
NotifyObserver(State::kInstallPflash);
if (!fds_->pflash.has_value()) {
VLOG(2) << "No pflash file expected, skipping to StartVm";
ClearVek();
return;
}
auto* client = ash::ConciergeClient::Get();
DCHECK(client) << "This code requires a ConciergeClient";
std::string user_hash =
ash::ProfileHelper::GetUserIdHashFromProfile(profile_);
vm_tools::concierge::InstallPflashRequest request;
request.set_owner_id(std::move(user_hash));
request.set_vm_name(vm_name_);
client->InstallPflash(
std::move(*fds_->pflash), request,
base::BindOnce(&BruschettaInstallerImpl::OnInstallPflash,
weak_ptr_factory_.GetWeakPtr()));
}
void BruschettaInstallerImpl::OnInstallPflash(
std::optional<vm_tools::concierge::InstallPflashResponse> result) {
if (MaybeClose()) {
return;
}
if (!result || !result->success()) {
install_running_ = false;
Error(BruschettaInstallResult::kInstallPflashError);
if (result) {
LOG(ERROR) << "Install pflash failed: " << result->failure_reason();
} else {
LOG(ERROR) << "Install pflash failed, no response";
}
return;
}
ClearVek();
}
void BruschettaInstallerImpl::ClearVek() {
VLOG(2) << "Clearing VEK";
NotifyObserver(State::kClearVek);
attestation::DeleteKeysRequest request;
request.set_username("");
request.set_key_label_match(kVtpmEkLabel);
request.set_match_behavior(
attestation::DeleteKeysRequest::MATCH_BEHAVIOR_EXACT);
auto* client = ash::AttestationClient::Get();
DCHECK(client) << "This code requires a AttestationClient";
client->DeleteKeys(request,
base::BindOnce(&BruschettaInstallerImpl::OnClearVek,
weak_ptr_factory_.GetWeakPtr()));
}
void BruschettaInstallerImpl::OnClearVek(
const attestation::DeleteKeysReply& result) {
if (MaybeClose()) {
return;
}
if (result.status() != attestation::STATUS_SUCCESS) {
install_running_ = false;
Error(BruschettaInstallResult::kClearVekFailed);
LOG(ERROR) << "Delete vEK failed: " << result.status();
return;
}
StartVm();
}
void BruschettaInstallerImpl::StartVm() {
VLOG(2) << "Starting VM";
NotifyObserver(State::kStartVm);
auto launch_policy_opt = GetLaunchPolicyForConfig(profile_, config_id_);
auto full_policy_opt = GetInstallableConfig(profile_, config_id_);
if (!full_policy_opt.has_value() || !launch_policy_opt.has_value()) {
// Policy has changed to prohibit installation, so bail out before actually
// starting the VM.
install_running_ = false;
Error(BruschettaInstallResult::kInstallationProhibited);
LOG(ERROR) << "Installation prohibited by policy";
return;
}
auto launch_policy = *launch_policy_opt;
const auto* full_policy = *full_policy_opt;
auto* client = ash::ConciergeClient::Get();
DCHECK(client) << "This code requires a ConciergeClient";
std::string user_hash =
ash::ProfileHelper::GetUserIdHashFromProfile(profile_);
std::string vm_username = GetVmUsername(profile_);
vm_tools::concierge::StartVmRequest request;
request.set_name(vm_name_);
request.set_owner_id(std::move(user_hash));
request.set_vm_username(vm_username);
request.mutable_vm()->set_tools_dlc_id(kToolsDlc);
request.mutable_vm()->set_bios_dlc_id(kUefiDlc);
request.set_start_termina(false);
request.set_vtpm_proxy(launch_policy.vtpm_enabled);
auto* disk = request.add_disks();
disk->set_path(std::move(disk_path_));
disk->set_writable(true);
for (const auto& oem_string :
*full_policy->FindList(prefs::kPolicyOEMStringsKey)) {
request.add_oem_strings(oem_string.GetString());
}
request.set_timeout(240);
request.add_fds(vm_tools::concierge::StartVmRequest::STORAGE);
client->StartVmWithFd(
std::move(fds_->boot_disk), request,
base::BindOnce(&BruschettaInstallerImpl::OnStartVm,
weak_ptr_factory_.GetWeakPtr(), launch_policy));
fds_.reset();
}
void BruschettaInstallerImpl::OnStartVm(
RunningVmPolicy launch_policy,
std::optional<vm_tools::concierge::StartVmResponse> result) {
if (MaybeClose()) {
return;
}
if (!result || !result->success()) {
install_running_ = false;
Error(BruschettaInstallResult::kStartVmFailed);
if (result) {
LOG(ERROR) << "VM failed to start: " << result->failure_reason();
} else {
LOG(ERROR) << "VM failed to start, no response";
}
return;
}
BruschettaService::GetForProfile(profile_)->RegisterVmLaunch(vm_name_,
launch_policy);
profile_->GetPrefs()->SetBoolean(bruschetta::prefs::kBruschettaInstalled,
true);
LaunchTerminal();
}
void BruschettaInstallerImpl::LaunchTerminal() {
VLOG(2) << "Launching terminal";
NotifyObserver(State::kLaunchTerminal);
// TODO(b/231899688): Implement Bruschetta sending an RPC when installation
// finishes so that we only add to prefs on success.
auto guest_id = MakeBruschettaId(std::move(vm_name_));
BruschettaService::GetForProfile(profile_)->RegisterInPrefs(
guest_id, std::move(config_id_));
guest_id.container_name = "";
// kInvalidDisplayId will launch terminal on the current active display.
guest_os::LaunchTerminal(profile_, display::kInvalidDisplayId, guest_id);
// Close dialog.
base::UmaHistogramEnumeration(kInstallResultMetric,
BruschettaInstallResult::kSuccess);
std::move(close_closure_).Run();
}
void BruschettaInstallerImpl::NotifyObserver(State state) {
if (observer_) {
observer_->StateChanged(state);
}
}
void BruschettaInstallerImpl::Error(BruschettaInstallResult error) {
VLOG(2) << "Error installing: " << BruschettaInstallResultString(error);
base::UmaHistogramEnumeration(kInstallResultMetric, error);
if (observer_) {
observer_->Error(error);
}
}
void BruschettaInstallerImpl::AddObserver(Observer* observer) {
// We only support a single observer for now, since we'll only ever have one
// (the UI calling us).
DCHECK(observer_ == nullptr);
observer_ = observer;
}
void BruschettaInstallerImpl::RemoveObserver(Observer* observer) {
DCHECK(observer_ == observer);
observer_ = nullptr;
}
} // namespace bruschetta