blob: e185d1dc6e70fbd160686c556eb9da8624098068 [file] [log] [blame]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// This implements management of EFI boot entries for systems where we manage
// those entries (UEFI systems not running Chrome OS firmware).
//
// The boot path on generic UEFI systems starts with the firmware deciding which
// efi binary to run. There is some variation in how this is implemented, and
// while most firmware will by default look at the GPT for an EFI System
// Partition and find the appropriate file located at
// `/efi/boot/boot{ia32|x64}.efi`, there are some implementations that don't.
// To ensure that we boot correctly after install on those systems we need to
// actively manage the boot entries.
//
// EFI boot selection is managed by a set of EFI variables.
// * Boot0000 through BootFFFF contain data about specific boot options that can
// be tried or presented to the user.
// * BootOrder contains an ordered list of Boot#### entries to be tried when
// booting, e.g. "try to boot from entry 2, and if that fails try entry 0"
#include <limits>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include <base/containers/contains.h>
#include <base/environment.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/logging.h>
#include <base/numerics/safe_conversions.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <cros_config/cros_config.h>
#include "installer/efi_boot_management.h"
#include "installer/efivar.h"
#include "installer/inst_util.h"
#include "installer/metrics.h"
namespace {
// Default description of the managed boot entry.
const char kCrosEfiDefaultDescription[] = "ChromiumOS";
// For accessing an override for the description of the managed boot entry.
const char kCrosConfigEfiDescriptionPath[] = "/efi";
const char kCrosConfigEfiDescriptionKey[] = "bootvar-name-override";
// The name of the efi variable where Boot order is stored.
const char kBootOrder[] = "BootOrder";
// The base for our standard error message.
const char kCantEnsureBoot[] = "Can't ensure successful boot: ";
const char kUMAEfiManagementEventName[] =
"Installer.Postinstall.EfiManagementEvent";
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class EfiManagementEvent {
kRequiredEntryManagementFailed = 1,
kOptionalEntryManagementFailed = 2,
kMaxValue = kOptionalEntryManagementFailed,
};
// Used to watch for any upward trend in entry count, which could indicate
// bugs related to entry creation or cleanup.
const char kUMAEfiEntryCountName[] = "Installer.Postinstall.EfiBootEntryCount";
const int kUMAEfiEntryCountMin = 1;
const int kUMAEfiEntryCountMax = 30;
const int kUMAEfiEntryCountBuckets = 15;
// We don't expect loading entries to fail under normal circumstances, but we've
// seen bugs result in these failures.
const char kUMAEfiEntryFailedLoadName[] =
"Installer.Postinstall.EfiBootEntryFailedLoad";
const int kUMAEfiEntryFailedLoadMin = 1;
const int kUMAEfiEntryFailedLoadMax = 10;
const int kUMAEfiEntryFailedLoadBuckets = 4;
// There should only ever be zero or one managed entry. Even minor deviations
// are noteworthy.
const char kUMAManagedEfiEntryCountName[] =
"Installer.Postinstall.ManagedEfiBootEntryCount";
const int kUMAManagedEfiEntryCountMax = 5;
// Returns the description to use for managed EFI entries.
// Uses chromeos-config to look for a board-specific override if present,
// falling back to a hard-coded default if the override can't be read.
// An optional `CrosConfigInterface` parameter is supplied for testing.
std::string EfiDescription(
std::unique_ptr<brillo::CrosConfigInterface> config = nullptr) {
// If not passed a CrosConfig, make one.
if (!config) {
config = std::make_unique<brillo::CrosConfig>();
}
std::string description;
if (!config->GetString(kCrosConfigEfiDescriptionPath,
kCrosConfigEfiDescriptionKey, &description)) {
LOG(WARNING) << "Can't read name override, using default entry name";
// If we couldn't read it, return the default.
return kCrosEfiDefaultDescription;
}
return description;
}
// Returns true if the passed string matches the "Boot####" format:
// starts with "Boot", followed by four hex digits, with nothing trailing.
// Returns false otherwise.
bool IsBootNum(const std::string& name) {
return (name.size() == 8 &&
base::StartsWith(name, "Boot", base::CompareCase::SENSITIVE) &&
// Safe because of the size() check.
base::IsHexDigit(name[4]) && base::IsHexDigit(name[5]) &&
base::IsHexDigit(name[6]) && base::IsHexDigit(name[7]));
}
// Get the size of the current EFI platform.
// Returns nullopt if the size could not be determined.
std::optional<int> GetEfiPlatformSize() {
const base::FilePath size_file("/sys/firmware/efi/fw_platform_size");
// Read the EFI platform size to determine which loader to configure. It must
// match the EFI implementation from the firmware not the running kernel.
std::string size_string;
if (!base::ReadFileToString(size_file, &size_string)) {
// The proper target cannot be determined.
// EFI services are likely not available.
return std::nullopt;
}
int size;
base::TrimWhitespaceASCII(size_string, base::TRIM_ALL, &size_string);
if (!base::StringToInt(size_string, &size)) {
return std::nullopt;
}
// Sanity check the size. It should only be one of these.
if (size != 64 && size != 32) {
return std::nullopt;
}
return size;
}
// EFI boot entries are named/numbered with the format Boot####,
// with 4 uppercase hex digits as the numeric portion.
// This class is a minimal wrapper around a boot entry number.
class EfiBootNumber {
public:
// Comparator for using EfiBootNumber as a key in a std::map.
struct Comparator {
bool operator()(const EfiBootNumber& a, const EfiBootNumber& b) const {
return a.Number() < b.Number();
}
};
explicit EfiBootNumber(uint16_t num) : boot_num_(num) {
boot_name_ = base::StringPrintf("Boot%04X", boot_num_);
}
static std::optional<EfiBootNumber> FromName(const std::string& name) {
if (!IsBootNum(name)) {
return std::nullopt;
}
const std::string hex_part = name.substr(4, 4);
// There's no HexStringToUInt16 or similar, so we'll just cram a `uint32_t`
// into our `uint16_t`, knowing that for all compliant UEFI implementations
// this'll be fine. (Also, we're only converting four chars of hex, so it'll
// never exceed 16 bits).
uint32_t num;
const bool success = base::HexStringToUInt(hex_part, &num);
if (success) {
const EfiBootNumber boot_num(base::checked_cast<uint16_t>(num));
return boot_num;
}
return std::nullopt;
}
std::string Name() const { return boot_name_; }
uint16_t Number() const { return boot_num_; }
private:
uint16_t boot_num_;
std::string boot_name_;
};
// EFI boot entries contain attributes, a description/label, a device path,
// and optional data. We only care about the label and the path.
class EfiBootEntryContents {
public:
EfiBootEntryContents(const std::string& description,
const std::vector<uint8_t>& device_path)
: description_(description), device_path_(device_path) {}
bool operator==(const EfiBootEntryContents& other) const {
return description_ == other.description_ &&
device_path_ == other.device_path_;
}
std::string ToString() const {
return base::StringPrintf(
"description: '%s'\npath_data: %s", description_.c_str(),
base::HexEncode(device_path_.data(), device_path_.size()).c_str());
}
std::string Description() const { return description_; }
std::vector<uint8_t> DevicePath() const { return device_path_; }
private:
// This is sometimes called 'description', sometimes 'label':
// The user-friendly name of the entry.
std::string description_;
// device_path_ stores the data represented by libefivar's efidp.
// In our case it will describe the hardware location of the storage media,
// plus the on-disk location of the efi file to load.
std::vector<uint8_t> device_path_;
};
// A wrapper around the BootOrder EFI variable,
// an ordered list of boot entries to be tried, stored as 16-bit uints.
class BootOrder {
public:
BootOrder() = default;
// Read and store the data, an array of 16-bit uints.
// If we can't read it, we'll need to write a new one.
void Load(EfiVarInterface& efivar) {
EfiVarInterface::Bytes data;
size_t data_size;
if (efivar.GetVariable(kBootOrder, data, &data_size)) {
boot_order_.assign(reinterpret_cast<uint16_t*>(data.get()),
reinterpret_cast<uint16_t*>(data.get()) +
(data_size / sizeof(uint16_t)));
// Happy-path logging of the loaded order. If things go wrong later this
// can make it much easier to see why.
LOG(INFO) << "Loaded BootOrder: " << ToString();
} else {
// We couldn't read the boot order, so we need to write a new one.
boot_order_.clear();
needs_write_ = true;
LOG(INFO) << "Creating new BootOrder.";
}
}
// Write the data back to the EFI variable, but only if
// we've made any modifications to the boot order.
// Returns an errno from libefivar on failure, nullopt on success.
std::optional<EfiVarError> WriteIfNeeded(EfiVarInterface& efivar) {
if (!needs_write_) {
LOG(INFO) << "BootOrder: No write needed.";
return std::nullopt;
}
// Copy into `uint8_t`s for writing out.
std::vector<uint8_t> out;
out.assign(reinterpret_cast<uint8_t*>(boot_order_.data()),
reinterpret_cast<uint8_t*>(boot_order_.data()) +
(boot_order_.size() * sizeof(uint16_t)));
const std::optional<EfiVarError> error =
efivar.SetVariable(kBootOrder, kBootVariableAttributes, out);
if (!error) {
needs_write_ = false;
}
return error;
}
bool Contains(const EfiBootNumber& entry) const {
return base::Contains(boot_order_, entry.Number());
}
// Adds an entry to the beginning of the boot order, making a write necessary.
void Add(const EfiBootNumber& entry) {
boot_order_.insert(boot_order_.begin(), entry.Number());
needs_write_ = true;
}
// Completely removes an entry from boot order, making a write necessary if
// the entry was actually present.
void Remove(const EfiBootNumber& entry) {
// Only need to write back out if we successfully erase anything.
if (std::erase(boot_order_, entry.Number()) > 0) {
needs_write_ = true;
}
}
std::string ToString() const {
std::string str;
for (const auto x : boot_order_) {
base::StringAppendF(&str, "%04X ", x);
}
return str;
}
std::vector<uint16_t> Data() const { return boot_order_; }
private:
std::vector<uint16_t> boot_order_;
bool needs_write_ = false;
};
// Manages the list of EFI boot entries and the BootOrder.
class EfiBootManager {
public:
using EntriesMap =
std::map<EfiBootNumber, EfiBootEntryContents, EfiBootNumber::Comparator>;
EfiBootManager(std::unique_ptr<EfiVarInterface> efivar,
std::unique_ptr<MetricsInterface> metrics,
const std::string& description)
: efivar_(std::move(efivar)),
metrics_(std::move(metrics)),
entry_description_(description) {
CHECK(efivar_);
CHECK(metrics_);
}
// Wrapper around libefivar's variable iteration to filter only Boot* entries.
// Returns each Boot entry name in turn until all are read, nullopt after.
std::optional<EfiBootNumber> GetNextBootNum() {
std::optional<std::string> name;
while ((name = efivar_->GetNextVariableName())) {
std::optional<EfiBootNumber> entry_number =
EfiBootNumber::FromName(name.value());
if (entry_number) {
return entry_number;
}
}
return std::nullopt;
}
// Load all the Boot* entries into our map.
// Sends metrics to track number of entries and failures to read entries.
// Returns false if any boot number doesn't meet our expectations or if any
// entry fails to load, returning true when all entries are stored.
bool LoadBootEntries() {
std::optional<EfiBootNumber> entry_number;
uint16_t total = 0;
while ((entry_number = GetNextBootNum())) {
++total;
std::optional<EfiBootEntryContents> entry_contents =
LoadEntry(entry_number.value());
if (entry_contents) {
entries_.emplace(entry_number.value(), entry_contents.value());
}
}
SendEntryCountMetric(total);
const uint16_t failures = total - entries_.size();
SendFailedLoadsMetric(failures);
// If any entry couldn't be loaded, we're not safe to continue.
return failures == 0;
}
// Loads the data for a single boot entry, returning it if correctly loaded.
// Returns nullopt on error.
std::optional<EfiBootEntryContents> LoadEntry(const EfiBootNumber& number) {
EfiVarInterface::Bytes data;
size_t data_size;
if (!efivar_->GetVariable(number.Name(), data, &data_size)) {
// GetVariable logs errors for us.
return std::nullopt;
}
std::string description = efivar_->LoadoptDesc(data.get(), data_size);
std::vector<uint8_t> device_path =
efivar_->LoadoptPath(data.get(), data_size);
return std::optional<EfiBootEntryContents>(std::in_place, description,
device_path);
}
// Writes the boot entry contents to a boot number.
// Returns false for errors writing, true otherwise.
// If an errno is returned by libefivar, it will be stored in error_num.
bool WriteEntry(const EfiBootNumber& number,
const EfiBootEntryContents& contents,
EfiVarError* error_num) {
std::vector<uint8_t> entry_data;
// Ensure we only report an error on SetVariable fail.
*error_num = 0;
// Format the entry data:
// UEFI spec v2.9 section 3.1.3 lists the possible attributes.
// 1 means "active". We always create active entries.
const uint32_t attributes = 1;
auto device_path = contents.DevicePath();
auto description = contents.Description();
if (!efivar_->LoadoptCreate(attributes, device_path, description,
&entry_data)) {
LOG(ERROR) << "Error formatting entry contents for " << number.Name();
return false;
}
const std::optional<EfiVarError> error = efivar_->SetVariable(
number.Name(), kBootVariableAttributes, entry_data);
if (error) {
*error_num = error.value();
return false;
}
return true;
}
// Deletes the entry from disk and if successful removes it from the boot
// order. Returns false for errors deleting from disk, true otherwise.
bool RemoveEntry(const EfiBootNumber& number) {
if (!efivar_->DelVariable(number.Name())) {
// DelVariable logs errors for us.
return false;
}
boot_order_.Remove(number);
return true;
}
// Attempts to define the boot entry we want, for matching against or writing.
// Determines the device path and the description we want, based on:
// * disk
// * partition
// * 32/64-bit EFI
// Returns nullopt for any failure to collect this info.
std::optional<EfiBootEntryContents> BuildDesiredEntry(
const Partition& boot_dev, int efi_size) {
// Select the target boot file based on the platform.
base::FilePath boot_file = base::FilePath("/efi/boot/bootx64.efi");
if (efi_size == 32) {
boot_file = base::FilePath("/efi/boot/bootia32.efi");
}
std::vector<uint8_t> efidp;
if (!efivar_->GenerateFileDevicePathFromEsp(
boot_dev.base_device(), boot_dev.number(), boot_file, efidp)) {
LOG(ERROR)
<< "Can't decide on desired entry: couldn't determine device path";
return std::nullopt;
}
return EfiBootEntryContents(entry_description_, efidp);
}
// Returns an entry with desired contents that also appears in the boot order,
// if one can be found. nullopt otherwise.
std::optional<EfiBootNumber> FindContentsInBootOrder(
const EfiBootEntryContents& desired_contents) {
for (const auto num : boot_order_.Data()) {
const EfiBootNumber entry(num);
auto value = entries_.find(entry);
if (value != entries_.end() && value->second == desired_contents) {
return entry;
}
}
return std::nullopt;
}
// Returns an entry with desired contents, if one can be found.
// nullopt otherwise.
std::optional<EfiBootNumber> FindContents(
const EfiBootEntryContents& desired_contents) {
for (auto const& [key, value] : entries_) {
if (value == desired_contents) {
return key;
}
}
return std::nullopt;
}
// Best-effort removal from disk and boot order for all entries with
// "our description", i.e. managed by us. We only do best-effort because
// entries left behind shouldn't interfere with future boots.
void RemoveAllManagedEntries() {
auto entry_iter = entries_.begin();
while (entry_iter != entries_.end()) {
if (entry_iter->second.Description() == entry_description_) {
// Best effort removal, including from boot order.
LOG(INFO) << "Trying to remove " << entry_iter->first.Name();
if (RemoveEntry(entry_iter->first)) {
// Drop from container if successful so we know the bootnum is
// available.
entry_iter = entries_.erase(entry_iter);
// The `erase` increments our iter for us.
continue;
}
}
entry_iter++;
}
}
// Finds the lowest available boot number, returning it if found and an empty
// optional if all 65536 boot numbers are taken (which shouldn't happen on
// any hardware I'm aware of).
std::optional<EfiBootNumber> NextAvailableBootNum() {
// Four hex chars fit perfectly in a `uint16_t`.
uint16_t free_num = 0;
uint16_t max = std::numeric_limits<decltype(free_num)>::max();
for (free_num = 0; free_num < max; ++free_num) {
EfiBootNumber entry(free_num);
if (!base::Contains(entries_, entry)) {
return entry;
}
}
return std::nullopt;
}
// We consider certain failures when writing "acceptable".
// While the failure will block writing, we believe the risk of the system
// being unable to boot without the entry to be sufficiently low that we'd
// rather allow the install/update to proceed. b/226935367
// Returns true if we consider this failure "acceptable", false otherwise.
bool IsAcceptableWriteFailure(EfiVarError error) {
return error == ENOSPC || error == EINTR;
}
// This is the high level logic of how we maintain our boot entries:
// 1. Figure out what an entry pointing at our install would look like.
// This should be the same for slot A/B.
// 2. Look for an existing entry that matches it.
// If found make sure it's in the boot order.
// 3. Remove any "extra" entries that have the same description,
// assuming that we're responsible for managing all entries with our name.
// 4. If no existing entry found then make one:
// - Pick the lowest available boot number.
// - Write an entry pointing at our install to that number.
// - Add it to the boot order.
bool UpdateEfiBootEntriesImpl(const InstallConfig& install_config,
int efi_size) {
if (!LoadBootEntries()) {
LOG(ERROR) << kCantEnsureBoot << "need to know what boot entries exist.";
return false;
}
// Send metric based on the loaded boot entries, only if we loaded all of
// them. If any failed to load we don't know if our count is valid.
SendManagedEntryCountMetric();
boot_order_.Load(*efivar_);
// Figure out what a "correct" boot entry would look like.
const std::optional<EfiBootEntryContents> desired_contents =
BuildDesiredEntry(install_config.boot, efi_size);
if (!desired_contents) {
LOG(ERROR) << kCantEnsureBoot
<< "need to know what our entry should look like.";
return false;
}
LOG(INFO) << "Looking for an entry matching: "
<< desired_contents->ToString();
std::optional<EfiBootNumber> found_entry =
FindContentsInBootOrder(desired_contents.value());
if (!found_entry) {
found_entry = FindContents(desired_contents.value());
if (found_entry) {
// We found a good entry, but it's not in the boot order. Fix that.
boot_order_.Add(found_entry.value());
}
}
if (found_entry) {
LOG(INFO) << "Found matching entry, no need to create one.";
// If we found something drop it from the list so we don't have to avoid
// deleting it in RemoveAllManagedEntries.
entries_.erase(found_entry.value());
}
// Any remaining cros entries don't match what we want, and should be
// removed.
RemoveAllManagedEntries();
// If we didn't find an existing one, we'll need to create a new entry.
if (!found_entry) {
LOG(INFO) << "Creating EFI boot entry.";
// Try to pick a number.
const std::optional<EfiBootNumber> desired_num = NextAvailableBootNum();
// If we didn't get a number, we've got to bail.
if (!desired_num) {
LOG(ERROR) << kCantEnsureBoot
<< "need an available boot number, all are taken.";
return false;
}
EfiVarError error = 0;
if (!WriteEntry(desired_num.value(), desired_contents.value(), &error)) {
if (!IsAcceptableWriteFailure(error)) {
// For other failures, block completion.
LOG(ERROR) << kCantEnsureBoot << "need to write boot entry.";
return false;
}
// We couldn't make the entry so don't add it to boot order.
LOG(WARNING) << "Couldn't write a boot entry. Proceeding anyway.";
} else {
boot_order_.Add(desired_num.value());
}
}
// This will be needed if we deleted any entries that were in the boot order
// or if we wrote a new one.
const std::optional<EfiVarError> error =
boot_order_.WriteIfNeeded(*efivar_);
if (error) {
if (!IsAcceptableWriteFailure(error.value())) {
LOG(ERROR) << kCantEnsureBoot << "need to write boot order.";
return false;
}
LOG(WARNING) << "Couldn't add entry to boot order. Proceeding anyway.";
}
return true;
}
// A thin wrapper around UpdateEfiBootEntriesImpl to handle sending UMAs and
// choosing an appropriate return code.
bool UpdateEfiBootEntries(const InstallConfig& install_config,
base::Environment& env,
int efi_size) {
if (!UpdateEfiBootEntriesImpl(install_config, efi_size)) {
// On install if we can't manage efi entries we can't be sure that we've
// created a system that will boot (some firmware can't find the default
// location).
const bool management_required = env.HasVar(kEnvIsInstall);
if (management_required) {
SendEfiManagementEvent(
EfiManagementEvent::kRequiredEntryManagementFailed);
LOG(ERROR) << "Failed to manage EFI boot entries, can't continue.";
return false;
} else {
SendEfiManagementEvent(
EfiManagementEvent::kOptionalEntryManagementFailed);
LOG(WARNING) << "Failed to manage EFI boot entries, "
<< "safe because we're updating.";
}
}
return true;
}
void SendEfiManagementEvent(EfiManagementEvent event) {
if (!metrics_->SendEnumMetric(
kUMAEfiManagementEventName, static_cast<int>(event),
static_cast<int>(EfiManagementEvent::kMaxValue))) {
LOG(WARNING) << "Couldn't send event metric";
}
}
void SendEntryCountMetric(uint16_t num) {
if (!metrics_->SendMetric(kUMAEfiEntryCountName, num, kUMAEfiEntryCountMin,
kUMAEfiEntryCountMax, kUMAEfiEntryCountBuckets)) {
LOG(WARNING) << "Couldn't send entry count metric";
}
}
void SendFailedLoadsMetric(uint16_t num) {
if (!metrics_->SendMetric(
kUMAEfiEntryFailedLoadName, num, kUMAEfiEntryFailedLoadMin,
kUMAEfiEntryFailedLoadMax, kUMAEfiEntryFailedLoadBuckets)) {
LOG(WARNING) << "Couldn't send failed loads metric";
}
}
// Count entries matching our description and send an UMA metric.
void SendManagedEntryCountMetric() {
const int managed_entries = std::count_if(
entries_.begin(), entries_.end(),
[&desc = entry_description_](const EntriesMap::value_type& entry) {
return entry.second.Description() == desc;
});
if (!metrics_->SendLinearMetric(kUMAManagedEfiEntryCountName,
managed_entries,
kUMAManagedEfiEntryCountMax)) {
LOG(WARNING) << "Couldn't send managed entry count metric";
}
}
// For testing.
EntriesMap Entries() const { return entries_; }
void SetEntries(const EntriesMap& entries) { entries_ = entries; }
BootOrder Order() const { return boot_order_; }
void SetBootOrder(const BootOrder& order) { boot_order_ = order; }
EfiVarInterface* EfiVar() const { return efivar_.get(); }
MetricsInterface* Metrics() const { return metrics_.get(); }
private:
// An interface around libefivar, handles the actual writing/reading to
// sysfs and other filesystem access.
std::unique_ptr<EfiVarInterface> efivar_;
// An interface around sending metrics.
std::unique_ptr<MetricsInterface> metrics_;
// Container for our entries, mapping boot numbers to entry contents.
EntriesMap entries_;
BootOrder boot_order_;
// The description to be used for managed entries.
const std::string entry_description_;
};
} // namespace
// Wraps some advance checks and final logging around the actual logic in Impl.
bool UpdateEfiBootEntries(const InstallConfig& install_config) {
std::unique_ptr<EfiVarInterface> efivar = std::make_unique<EfiVarImpl>();
if (!efivar->EfiVariablesSupported()) {
LOG(INFO) << "EFI runtime services not available."
" Assuming called from a Legacy context or on a device that"
" intentionally blocks efi runtime services.";
return true;
} else {
LOG(INFO) << "Adding EFI Boot entry.";
}
// Select the target boot file based on the platform.
std::optional<int> efi_size = GetEfiPlatformSize();
if (!efi_size.has_value()) {
LOG(ERROR)
<< "Can't determine EFI platform size, so can't make a boot entry.";
return false;
}
std::unique_ptr<MetricsInterface> metrics =
MetricsInterface::GetMetricsInstance();
auto env = base::Environment::Create();
EfiBootManager efi_boot_manager(std::move(efivar), std::move(metrics),
EfiDescription());
return efi_boot_manager.UpdateEfiBootEntries(install_config, *env,
efi_size.value());
}