chromium/chrome/browser/ash/crosapi/browser_loader.cc

// Copyright 2020 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/crosapi/browser_loader.h"

#include <utility>

#include "ash/constants/ash_switches.h"
#include "base/barrier_callback.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/ranges/algorithm.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/values.h"
#include "chrome/browser/ash/crosapi/lacros_selection_loader.h"
#include "chrome/browser/ash/crosapi/lacros_selection_loader_factory.h"
#include "chrome/browser/ash/crosapi/rootfs_lacros_loader.h"
#include "chrome/browser/ash/crosapi/stateful_lacros_loader.h"
#include "chrome/browser/browser_process.h"
#include "chromeos/ash/components/cryptohome/system_salt_getter.h"
#include "components/component_updater/ash/component_manager_ash.h"

namespace crosapi {

namespace {
// There are 2 lacros selections, rootfs lacros and stateful lacros.
constexpr size_t kLacrosSelectionTypes = 2;

class LacrosSelectionLoaderFactoryImpl : public LacrosSelectionLoaderFactory {
 public:
  explicit LacrosSelectionLoaderFactoryImpl(
      scoped_refptr<component_updater::ComponentManagerAsh> manager)
      : component_manager_(manager) {}

  LacrosSelectionLoaderFactoryImpl(const LacrosSelectionLoaderFactoryImpl&) =
      delete;
  LacrosSelectionLoaderFactoryImpl& operator=(
      const LacrosSelectionLoaderFactoryImpl&) = delete;

  ~LacrosSelectionLoaderFactoryImpl() override = default;

  std::unique_ptr<LacrosSelectionLoader> CreateRootfsLacrosLoader() override {
    return std::make_unique<RootfsLacrosLoader>();
  }

  std::unique_ptr<LacrosSelectionLoader> CreateStatefulLacrosLoader() override {
    return std::make_unique<StatefulLacrosLoader>(component_manager_);
  }

 private:
  scoped_refptr<component_updater::ComponentManagerAsh> component_manager_;
};

bool IsUnloading(LacrosSelectionLoader* loader) {
  return loader && loader->IsUnloading();
}

}  // namespace

BrowserLoader::BrowserLoader(
    scoped_refptr<component_updater::ComponentManagerAsh> manager)
    : factory_(std::make_unique<LacrosSelectionLoaderFactoryImpl>(manager)) {}

BrowserLoader::BrowserLoader(
    std::unique_ptr<LacrosSelectionLoaderFactory> factory)
    : factory_(std::move(factory)) {}

BrowserLoader::~BrowserLoader() = default;

BrowserLoader::LacrosSelectionVersion::LacrosSelectionVersion(
    LacrosSelection selection,
    base::Version version)
    : selection(selection), version(std::move(version)) {
  CHECK_NE(selection, LacrosSelection::kDeployedLocally);
}

// static.
bool BrowserLoader::WillLoadStatefulComponentBuilds() {
  // If the lacros chrome path is specified BrowserLoader will always attempt to
  // load lacros from this path and component manager builds are ignored.
  const base::FilePath lacros_chrome_path =
      base::CommandLine::ForCurrentProcess()->GetSwitchValuePath(
          ash::switches::kLacrosChromePath);
  if (!lacros_chrome_path.empty())
    return false;

  // If the lacros selection is forced by the user or by policy to rootfs it
  // will always be loaded and stateful component manager builds are ignored.
  std::optional<ash::standalone_browser::LacrosSelection> lacros_selection =
      ash::standalone_browser::DetermineLacrosSelection();
  if (lacros_selection == ash::standalone_browser::LacrosSelection::kRootfs) {
    return false;
  }

  return true;
}

void BrowserLoader::SelectRootfsLacros(LoadCompletionCallback callback,
                                       LacrosSelectionSource source) {
  CHECK(rootfs_lacros_loader_);

  LOG(WARNING) << "rootfs lacros is selected by " << source;

  rootfs_lacros_loader_->Load(
      base::BindOnce(&BrowserLoader::OnLoadComplete, weak_factory_.GetWeakPtr(),
                     std::move(callback), LacrosSelection::kRootfs),
      source == LacrosSelectionSource::kForced);
}

void BrowserLoader::SelectStatefulLacros(LoadCompletionCallback callback,
                                         LacrosSelectionSource source) {
  CHECK(stateful_lacros_loader_);

  LOG(WARNING) << "stateful lacros is selected by " << source;

  stateful_lacros_loader_->Load(
      base::BindOnce(&BrowserLoader::OnLoadComplete, weak_factory_.GetWeakPtr(),
                     std::move(callback), LacrosSelection::kStateful),
      source == LacrosSelectionSource::kForced);

  // Unmount the rootfs lacros-chrome when using stateful lacros-chrome.
  // This will keep stateful lacros-chrome only mounted and not hold the rootfs
  // lacros-chrome mount until an `Unload`.
  if (rootfs_lacros_loader_) {
    rootfs_lacros_loader_->Unload(
        base::BindOnce(&BrowserLoader::OnUnloadCompleted,
                       weak_factory_.GetWeakPtr(), LacrosSelection::kRootfs));
  }
}

void BrowserLoader::Load(LoadCompletionCallback callback) {
  // Load should NOT be called after Unload is requested to BrowserLoader.
  CHECK(!is_unload_requested_);

  // If either of rootfs or stateful lacros loader is still unloading, wait
  // until the unload completion.
  if (IsUnloading(rootfs_lacros_loader_.get()) ||
      IsUnloading(stateful_lacros_loader_.get())) {
    LOG(WARNING) << "Wait load until unload completes";
    callback_on_unload_completion_ =
        base::BindOnce(&BrowserLoader::LoadNow, weak_factory_.GetWeakPtr(),
                       std::move(callback));
    return;
  }

  LoadNow(std::move(callback));
}

void BrowserLoader::LoadNow(LoadCompletionCallback callback) {
  // Reset lacros selection loaders since it may be already initialized one if
  // this is reloading.
  // TODO(elkurin): We should call Unload before reloading if these loaders
  // exist, then we can remove `reset` here.
  rootfs_lacros_loader_.reset();
  stateful_lacros_loader_.reset();

  lacros_start_load_time_ = base::TimeTicks::Now();
  // TODO(crbug.com/40689435): Remove non-error logging from this class.
  LOG(WARNING) << "Starting lacros component load.";

  // If the user has specified a path for the lacros-chrome binary, use that
  // rather than component manager.
  base::FilePath lacros_chrome_path =
      base::CommandLine::ForCurrentProcess()->GetSwitchValuePath(
          ash::switches::kLacrosChromePath);
  if (!lacros_chrome_path.empty()) {
    OnLoadComplete(std::move(callback), LacrosSelection::kDeployedLocally,
                   base::Version(), lacros_chrome_path);
    return;
  }

  // If the LacrosSelection policy or the user have specified to force using
  // stateful or rootfs lacros-chrome binary, force the selection. Otherwise,
  // load the newest available binary.
  if (std::optional<ash::standalone_browser::LacrosSelection> lacros_selection =
          ash::standalone_browser::DetermineLacrosSelection()) {
    // TODO(crbug.com/40213424): We should check the version compatibility here,
    // too.
    switch (lacros_selection.value()) {
      case ash::standalone_browser::LacrosSelection::kRootfs:
        rootfs_lacros_loader_ = factory_->CreateRootfsLacrosLoader();
        SelectRootfsLacros(std::move(callback), LacrosSelectionSource::kForced);
        return;
      case ash::standalone_browser::LacrosSelection::kStateful:
        stateful_lacros_loader_ = factory_->CreateStatefulLacrosLoader();
        SelectStatefulLacros(std::move(callback),
                             LacrosSelectionSource::kForced);
        return;
      case ash::standalone_browser::LacrosSelection::kDeployedLocally:
        NOTREACHED_IN_MIGRATION();
        std::move(callback).Run(base::FilePath(),
                                LacrosSelection::kDeployedLocally,
                                base::Version());
        return;
    }
  }

  rootfs_lacros_loader_ = factory_->CreateRootfsLacrosLoader();
  stateful_lacros_loader_ = factory_->CreateStatefulLacrosLoader();

  // Proceed to load/mount the stateful lacros-chrome binary.
  // In the case that the stateful lacros-chrome binary wasn't installed, this
  // might take some time.
  auto barrier_callback = base::BarrierCallback<LacrosSelectionVersion>(
      kLacrosSelectionTypes,
      base::BindOnce(&BrowserLoader::OnLoadVersions, weak_factory_.GetWeakPtr(),
                     std::move(callback)));

  rootfs_lacros_loader_->GetVersion(
      base::BindOnce(&BrowserLoader::OnGetVersion, weak_factory_.GetWeakPtr(),
                     LacrosSelection::kRootfs, barrier_callback));
  stateful_lacros_loader_->GetVersion(
      base::BindOnce(&BrowserLoader::OnGetVersion, weak_factory_.GetWeakPtr(),
                     LacrosSelection::kStateful, barrier_callback));
}

void BrowserLoader::OnGetVersion(
    LacrosSelection selection,
    base::OnceCallback<void(LacrosSelectionVersion)> barrier_callback,
    const base::Version& version) {
  std::move(barrier_callback).Run(LacrosSelectionVersion(selection, version));
}

void BrowserLoader::OnLoadVersions(
    LoadCompletionCallback callback,
    std::vector<LacrosSelectionVersion> versions) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK_EQ(versions.size(), kLacrosSelectionTypes);

  if (is_unload_requested_) {
    LOG(WARNING) << "Unload is requested during collecting Lacros version.";
    std::move(callback).Run(base::FilePath(), LacrosSelection::kStateful,
                            base::Version());
    return;
  }

  // Compare the rootfs vs stateful lacros-chrome binary versions.
  // If the rootfs lacros-chrome is greater than lacros-chrome version,
  // prioritize using the rootfs lacros-chrome. If the stateful lacros-chrome is
  // not installed, let stateful lacros-chrome load in the background.
  auto selected = base::ranges::max_element(
      versions,
      [](const LacrosSelectionVersion& lhs, const LacrosSelectionVersion& rhs) {
        if (!lhs.version.IsValid()) {
          return true;
        }

        if (!rhs.version.IsValid()) {
          return false;
        }

        if (lhs.version != rhs.version) {
          return lhs.version < rhs.version;
        }

        // If the versions are the same, stateful lacros-chrome should be
        // prioritized, so considers LacrosSelectionVersion with kRootfs to be
        // smaller. Note that this comparison only happens between kRootfs and
        // kStateful.
        return lhs.selection == LacrosSelection::kRootfs;
      });

  if (!selected->version.IsValid()) {
    // Neither rootfs lacros nor stateful lacros are available.
    // Returning an empty file path to notify error.
    LOG(ERROR) << "No lacros is available";
    std::move(callback).Run(base::FilePath(), LacrosSelection::kStateful,
                            base::Version());
    return;
  }

  // Selected lacros may be older than the one which was running in a previous
  // sessions, accidentally. For experiment, now we intentionally ignore
  // the case and forcibly load the selected one, which is the best we could do
  // at this moment.
  // TODO(crbug.com/40213424): Check the condition and report it via UMA stats.

  switch (selected->selection) {
    case LacrosSelection::kRootfs: {
      SelectRootfsLacros(std::move(callback),
                         LacrosSelectionSource::kCompatibilityCheck);
      break;
    }
    case LacrosSelection::kStateful: {
      SelectStatefulLacros(std::move(callback),
                           LacrosSelectionSource::kCompatibilityCheck);
      break;
    }
    case LacrosSelection::kDeployedLocally: {
      NOTREACHED_IN_MIGRATION();
      std::move(callback).Run(
          base::FilePath(), LacrosSelection::kDeployedLocally, base::Version());
      return;
    }
  }
}

void BrowserLoader::Unload() {
  is_unload_requested_ = true;

  // Can be called even if Lacros isn't enabled, to clean up the old install.
  // Unmount the rootfs/stateful lacros-chrome if it was mounted.
  if (rootfs_lacros_loader_) {
    rootfs_lacros_loader_->Unload(
        base::BindOnce(&BrowserLoader::OnUnloadCompleted,
                       weak_factory_.GetWeakPtr(), LacrosSelection::kRootfs));
  }

  if (stateful_lacros_loader_) {
    stateful_lacros_loader_->Unload(
        base::BindOnce(&BrowserLoader::OnUnloadCompleted,
                       weak_factory_.GetWeakPtr(), LacrosSelection::kStateful));
  }
}

void BrowserLoader::OnUnloadCompleted(LacrosSelection selection) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  switch (selection) {
    case LacrosSelection::kRootfs:
      CHECK(rootfs_lacros_loader_->IsUnloaded());
      rootfs_lacros_loader_.reset();
      break;
    case LacrosSelection::kStateful:
      CHECK(stateful_lacros_loader_->IsUnloaded());
      stateful_lacros_loader_.reset();
      break;
    case LacrosSelection::kDeployedLocally:
      NOTREACHED_IN_MIGRATION();
      break;
  }

  // If either of rootfs or stateful lacros loader is still in the process of
  // unload, wait running completion callback.
  if (IsUnloading(rootfs_lacros_loader_.get()) ||
      IsUnloading(stateful_lacros_loader_.get())) {
    return;
  }

  // If both of the rootfs and stateful lacros load completed unloading, run the
  // stored callback if exists.
  if (callback_on_unload_completion_) {
    std::move(callback_on_unload_completion_).Run();
  }
}

base::FilePath DetermineLacrosBinaryPath(const base::FilePath& path) {
  // Interpret path as directory. If that fails, interpret it as the executable.
  base::FilePath expanded =
      path.Append(LacrosSelectionLoader::kLacrosChromeBinary);
  if (base::PathExists(expanded)) {
    return expanded;
  }
  if (base::PathExists(path)) {
    return path;
  }
  return {};
}

void BrowserLoader::OnLoadComplete(LoadCompletionCallback callback,
                                   LacrosSelection selection,
                                   base::Version version,
                                   const base::FilePath& path) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (is_unload_requested_) {
    LOG(WARNING) << "Unload is requested during loading.";
    std::move(callback).Run(base::FilePath(), LacrosSelection::kStateful,
                            base::Version());
    return;
  }

  // Bail out on empty `path` which implies there was an error on loading
  // lacros.
  if (path.empty()) {
    std::move(callback).Run(base::FilePath(), selection, base::Version());
    return;
  }

  // Fail early if the chrome binary still doesn't exist, such that
  // (1) we end up with an error message in Ash's log, and
  // (2) BrowserManager doesn't endlessly try to spawn Lacros.
  // For example, in the past there have been issues with mounting rootfs Lacros
  // that resulted in /run/lacros being empty at this point.
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(&DetermineLacrosBinaryPath, path),
      base::BindOnce(&BrowserLoader::FinishOnLoadComplete,
                     weak_factory_.GetWeakPtr(), std::move(callback), path,
                     selection, std::move(version)));
}

void BrowserLoader::FinishOnLoadComplete(LoadCompletionCallback callback,
                                         const base::FilePath& path,
                                         LacrosSelection selection,
                                         base::Version version,
                                         const base::FilePath& lacros_binary) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (is_unload_requested_) {
    LOG(WARNING) << "Unload is requested during determining lacros path.";
    std::move(callback).Run(base::FilePath(), LacrosSelection::kStateful,
                            base::Version());
    return;
  }

  if (lacros_binary.empty()) {
    LOG(ERROR) << "Failed to find binary at " << path;
    std::move(callback).Run(base::FilePath(), selection, base::Version());
    return;
  }

  base::UmaHistogramMediumTimes(
      "ChromeOS.Lacros.LoadTime",
      base::TimeTicks::Now() - lacros_start_load_time_);

  // Log the path on success.
  LOG(WARNING) << "Loaded lacros image with binary " << lacros_binary;
  std::move(callback).Run(lacros_binary, selection, std::move(version));
}

std::ostream& operator<<(std::ostream& ostream,
                         BrowserLoader::LacrosSelectionSource source) {
  switch (source) {
    case BrowserLoader::LacrosSelectionSource::kUnknown:
      return ostream << "Unknown";
    case BrowserLoader::LacrosSelectionSource::kCompatibilityCheck:
      return ostream << "CompatibilityCheck";
    case BrowserLoader::LacrosSelectionSource::kForced:
      return ostream << "Forced";
    case BrowserLoader::LacrosSelectionSource::kDeployedPath:
      return ostream << "DeployedPath";
  }
}

}  // namespace crosapi