chromium/chrome/browser/ash/bruschetta/bruschetta_installer_impl.cc

// 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