blob: 7bfab995902a4095e36dfebaff1f670fa6cc8f5a [file] [edit]
// Copyright 2012 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "installer/chromeos_legacy.h"
#include <stdio.h>
#include <unistd.h>
#include <optional>
#include <string>
#include <vector>
#include <base/files/file_enumerator.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/logging.h>
#include <base/strings/string_split.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <brillo/files/file_util.h>
#include "installer/chromeos_legacy_private.h"
#include "installer/efi_boot_management.h"
#include "installer/inst_util.h"
#include "installer/platform.h"
using std::string;
using std::vector;
namespace {
struct GrubQuirkEntry {
std::string_view sys_vendor;
std::string_view product_name;
};
// Vendor and product_name prefix to check for the grub workaround
// for b:431021440.
constexpr GrubQuirkEntry kGrubQuirk =
GrubQuirkEntry{.sys_vendor = "Acer", .product_name = "TravelMate"};
} // namespace
// String matching the kernel boot lines in grub.cfg files.
const std::string CommandPatternForSlot(BootSlot slot) {
switch (slot) {
case BootSlot::A:
return "/syslinux/vmlinuz.A";
case BootSlot::B:
return "/syslinux/vmlinuz.B";
}
}
std::string EfiGrubCfg::GetKernelCommand(BootSlot slot,
EfiGrubCfg::DmOption dm) const {
const string kernel_pattern = CommandPatternForSlot(slot);
const bool want_empty_dm = dm == EfiGrubCfg::DmOption::None;
for (const auto& line : file_lines_) {
if (line.find(kernel_pattern) == string::npos) {
continue;
}
if (ExtractKernelArg(line, "dm").empty() == want_empty_dm) {
return line;
}
}
return "";
}
bool EfiGrubCfg::ReplaceKernelCommand(BootSlot slot,
EfiGrubCfg::DmOption dm,
std::string cmd) {
const string kernel_pattern = CommandPatternForSlot(slot);
const bool want_empty_dm = dm == EfiGrubCfg::DmOption::None;
bool did_set = false;
for (auto& line : file_lines_) {
if (line.find(kernel_pattern) == string::npos) {
continue;
}
if (ExtractKernelArg(line, "dm").empty() == want_empty_dm) {
DLOG(INFO) << "Replacing: " << line;
line = cmd;
// Continue to replace all matching lines.
// It is not expected that there are multiple entries
// however replace them if they occur.
did_set = true;
}
}
return did_set;
}
bool EfiGrubCfg::LoadFile(const base::FilePath& path) {
string grub_src;
if (!base::ReadFileToString(path, &grub_src)) {
PLOG(ERROR) << "Unable to read grub template file: " << path.value();
return false;
}
// Split the file contents into lines.
file_lines_ = base::SplitString(grub_src, "\n", base::KEEP_WHITESPACE,
base::SPLIT_WANT_ALL);
return true;
}
std::string EfiGrubCfg::ToString() const {
return base::JoinString(file_lines_, "\n");
}
bool EfiGrubCfg::UpdateBootParameters(BootSlot slot,
const string& root_uuid,
const string& verity_args) {
const string kernel_pattern = CommandPatternForSlot(slot);
for (auto& line : file_lines_) {
// Convert all "linuxefi" grub commands to "linux" for the updated
// version of grub.
base::ReplaceFirstSubstringAfterOffset(&line, 0, "linuxefi", "linux");
if (line.find(kernel_pattern) == string::npos) {
continue;
}
DLOG(INFO) << "Updating command: " << line;
if (ExtractKernelArg(line, "dm").empty()) {
// If it's an unverified boot line, just set the root partition to boot.
if (!SetKernelArg("root", "PARTUUID=" + root_uuid, &line)) {
LOG(ERROR) << "Unable to update unverified root flag in " << line;
return false;
}
} else if (!SetKernelArg("dm", verity_args, &line)) {
LOG(INFO) << "Unable to update verified dm flag.";
return false;
}
}
return true;
}
bool UpdateLegacyKernel(const InstallConfig& install_config) {
const base::FilePath root_mount(install_config.root.mount());
const base::FilePath boot_mount(install_config.boot.mount());
const base::FilePath kernel_from = root_mount.Append("boot/vmlinuz");
const base::FilePath kernel_to =
boot_mount.Append("syslinux").Append("vmlinuz." + install_config.slot);
// In the event of a typical install, `kernel_from` may not exist.
// There is an expectation that `include_vmlinuz` be added to the board
// overlay's `profiles/base/make.defaults` as a `USE=` flag. Without this,
// `src/scripts/build_library/base_image_util.sh` will move the Kernel during
// `build_image`.
if (install_config.is_install &&
(install_config.bios_type == BiosType::kLegacy ||
install_config.bios_type == BiosType::kEFI)) {
// This is a non-fatal condition. The new Kernel is already present at the
// destination. Log a warning and continue.
if (!base::PathExists(kernel_from) && base::PathExists(kernel_to)) {
LOG(WARNING) << "Legacy Kernel '" << kernel_from
<< "' does not exist. Consider adding "
<< "`USE=\"${USE} include_vmlinuz\"` "
<< "to the board's `make.defaults`.";
return true;
}
}
// In any other scenario (like an update), ensure we copy the new Kernel.
return base::CopyFile(kernel_from, kernel_to);
}
string ExpandVerityArguments(const string& kernel_config,
const string& root_uuid) {
string kernel_config_dm = ExtractKernelArg(kernel_config, "dm");
// The verity config from the kernel contains short hand symbols for
// partition names that we have to expand to specific UUIDs.
// %U+1 -> XXX-YYY-ZZZ
ReplaceAll(&kernel_config_dm, "%U+1", root_uuid);
// PARTUUID=%U/PARTNROFF=1 -> PARTUUID=XXX-YYY-ZZZ
ReplaceAll(&kernel_config_dm, "%U/PARTNROFF=1", root_uuid);
return kernel_config_dm;
}
bool RunLegacyPostInstall(const Platform& platform,
const InstallConfig& install_config) {
const base::FilePath root_mount(install_config.root.mount());
const base::FilePath root_syslinux = root_mount.Append("boot/syslinux");
const base::FilePath boot_mount(install_config.boot.mount());
const base::FilePath boot_syslinux = boot_mount.Append("syslinux");
LOG(INFO) << "Running LegacyPostInstall.";
if (RunCommand({"cp", "-nR", root_syslinux.value(), boot_mount.value()}) !=
0) {
return false;
}
if (!UpdateLegacyKernel(install_config)) {
return false;
}
string kernel_config =
platform.DumpKernelConfig(install_config.kernel.device());
base::FilePath kernel_config_root =
base::FilePath(ExtractKernelArg(kernel_config, "root"));
// Prepare the new default.cfg
string verity_enabled =
(IsReadonly(kernel_config_root) ? "chromeos-vhd" : "chromeos-hd");
string default_syslinux_cfg = base::StringPrintf(
"DEFAULT %s.%s\n", verity_enabled.c_str(), install_config.slot.c_str());
const base::FilePath syslinux_cfg = boot_syslinux.Append("default.cfg");
if (!base::WriteFile(syslinux_cfg, default_syslinux_cfg)) {
return false;
}
// Prepare the new root.A/B.cfg
const base::FilePath old_root_cfg_file =
root_syslinux.Append("root." + install_config.slot + ".cfg");
const base::FilePath new_root_cfg_file =
boot_syslinux.Append(old_root_cfg_file.BaseName());
// Copy over the unmodified version for this release...
if (!base::CopyFile(old_root_cfg_file, new_root_cfg_file)) {
return false;
}
// Insert the proper root device for non-verity boots
const string root_opt = "PARTUUID=" + install_config.root.uuid(platform);
if (!ReplaceInFile("HDROOT" + install_config.slot, root_opt,
new_root_cfg_file)) {
return false;
}
string kernel_config_dm =
ExpandVerityArguments(kernel_config, install_config.root.uuid(platform));
if (kernel_config_dm.empty()) {
LOG(ERROR) << "Failed to extract Verity arguments.";
return false;
}
// Insert the proper verity options for verity boots
if (!ReplaceInFile("DMTABLE" + install_config.slot, kernel_config_dm,
new_root_cfg_file)) {
return false;
}
return true;
}
// Copy a file from the root partition to the boot partition.
bool CopyBootFile(const InstallConfig& install_config,
const std::string& src,
const std::string& dst) {
bool result = true;
const base::FilePath root_mount(install_config.root.mount());
const base::FilePath boot_mount(install_config.boot.mount());
const base::FilePath src_path = root_mount.Append(src);
const base::FilePath dst_path = boot_mount.Append(dst);
// If the source file file exists, copy it into place, else do nothing.
if (base::PathExists(src_path)) {
LOG(INFO) << "Copying " << src_path << " to " << dst_path;
result = base::CopyFile(src_path, dst_path);
} else {
LOG(INFO) << "Not present to install: " << src_path;
}
return result;
}
bool RunLegacyUBootPostInstall(const InstallConfig& install_config) {
bool result = true;
LOG(INFO) << "Running LegacyUBootPostInstall.";
result &= CopyBootFile(install_config,
"boot/boot-" + install_config.slot + ".scr.uimg",
"u-boot/boot.scr.uimg");
result &= CopyBootFile(
install_config, "boot/uEnv." + install_config.slot + ".txt", "uEnv.txt");
result &= CopyBootFile(install_config, "boot/MLO", "MLO");
result &= CopyBootFile(install_config, "boot/u-boot.img", "u-boot.img");
return result;
}
bool UpdateEfiBootloaders(const Platform& platform,
const InstallConfig& install_config) {
bool result = true;
const base::FilePath src_dir =
install_config.root.mount().Append("boot/efi/boot");
const base::FilePath dest_dir =
install_config.boot.mount().Append("efi/boot");
base::FileEnumerator file_enum(src_dir, /*recursive=*/false,
base::FileEnumerator::FILES);
for (auto src = file_enum.Next(); !src.empty(); src = file_enum.Next()) {
// Filter files by extension.
// .efi files are UEFI executables.
// .sig files are detached signatures.
const auto extension = src.FinalExtension();
if (extension != ".efi" && extension != ".sig") {
continue;
}
const base::FilePath dest = dest_dir.Append(src.BaseName());
if (!base::CopyFile(src, dest)) {
result = false;
}
}
// The grub workaround must only be applied for crdyboot builds.
// crdybootx64.efi will only exist when crdyboot is in use.
if (base::PathExists(dest_dir.Append("crdybootx64.efi")) &&
CheckRequiresGrubQuirk(platform)) {
// This is copying the shim for crdyboot, bootx64.efi (crdyshim), to
// the additional path grubx64.efi.
// On buggy firmware (those matching CheckRequiresGrubQuirk) the
// firmware makes it difficult to enter its menus if this path
// does not exist (b:431021440).
// crdyshim will load and verify crdyboot from either of
// these locations.
if (!base::CopyFile(dest_dir.Append("bootx64.efi"),
dest_dir.Append("grubx64.efi"))) {
result = false;
}
}
return result;
}
// Convert a slot string into the BootSlot enum value.
// Returns false when the slot_string is not a valid enum value.
bool StringToSlot(const std::string& slot_string, BootSlot* slot) {
if (slot_string == "A") {
*slot = BootSlot::A;
} else if (slot_string == "B") {
*slot = BootSlot::B;
} else {
return false;
}
return true;
}
// Modifies the slot's command line arguments in the boot
// grub.cfg for the update.
//
// The rootfs and dm= arguments will be taken from target kernel.
// The rest of the kernel parameters will come from the grub.cfg
// template in the target rootfs.
//
// Returns true if the boot grub.cfg file was successfully updated.
bool UpdateEfiGrubCfg(const Platform& platform,
const InstallConfig& install_config) {
// Of the form: PARTUUID=XXX-YYY-ZZZ
string kernel_config =
platform.DumpKernelConfig(install_config.kernel.device());
string root_uuid = install_config.root.uuid(platform);
string kernel_config_dm = ExpandVerityArguments(kernel_config, root_uuid);
BootSlot slot;
if (!StringToSlot(install_config.slot, &slot)) {
LOG(ERROR) << "Invalid slot value.";
return false;
}
// Path to the target grub.cfg to be updated in the EFI partition.
const base::FilePath boot_grub_path =
install_config.boot.mount().Append("efi/boot/grub.cfg");
// Grub.cfg source in the new root filesystem.
const base::FilePath root_grub_path =
install_config.root.mount().Append("boot/efi/boot/grub.cfg");
EfiGrubCfg boot_cfg;
if (!boot_cfg.LoadFile(boot_grub_path)) {
LOG(ERROR) << "Unable to read the target grub config.";
return false;
}
EfiGrubCfg root_cfg;
if (!root_cfg.LoadFile(root_grub_path)) {
LOG(ERROR) << "Unable to read the source grub kernel config. ";
return false;
}
// Extract the dm and non-dm kernel command lines from the grub config
// on new rootfs.
string dm_entry =
root_cfg.GetKernelCommand(slot, EfiGrubCfg::DmOption::Present);
if (dm_entry.empty()) {
LOG(ERROR) << "Unable to to find dm entry from the root grub.cfg";
return false;
}
string no_dm_entry =
root_cfg.GetKernelCommand(slot, EfiGrubCfg::DmOption::None);
if (no_dm_entry.empty()) {
LOG(ERROR) << "Unable to to find non-dm entry from the root grub.cfg";
return false;
}
// Replace the kernel command lines with those taken from the root's
// grub.cfg.
if (!boot_cfg.ReplaceKernelCommand(slot, EfiGrubCfg::DmOption::Present,
dm_entry)) {
LOG(ERROR) << "Unable to update the grub kernel boot options.";
return false;
}
if (!boot_cfg.ReplaceKernelCommand(slot, EfiGrubCfg::DmOption::None,
no_dm_entry)) {
LOG(ERROR) << "Unable to update the grub kernel boot options.";
return false;
}
// Update the root partition parameters in the boot grub.cfg.
if (!boot_cfg.UpdateBootParameters(slot, root_uuid, kernel_config_dm)) {
LOG(ERROR) << "Unable to update the rootfs grub configuration.";
return false;
}
// Write out the new grub.cfg.
if (!base::WriteFile(boot_grub_path, boot_cfg.ToString())) {
PLOG(ERROR) << "Unable to write boot menu file: " << boot_grub_path;
return false;
}
return true;
}
bool CheckRequiresGrubQuirk(const Platform& platform) {
std::optional<std::string> vendor = platform.ReadDmi(DmiKey::kSysVendor);
std::optional<std::string> product_name =
platform.ReadDmi(DmiKey::kProductName);
// If there is no vendor or product name the quirk can't
// match.
if (!vendor.has_value() || !product_name.has_value()) {
// Warn as it is expected this DMI information is available
// on reven.
LOG(WARNING) << "Unable to read DMI information.";
return false;
}
if (vendor == kGrubQuirk.sys_vendor &&
base::StartsWith(*product_name, kGrubQuirk.product_name)) {
LOG(INFO) << "Matched for the grub copy quirk.";
return true;
}
return false;
}
bool RunEfiPostInstall(const Platform& platform,
const InstallConfig& install_config) {
LOG(INFO) << "Running EfiPostInstall.";
// Update the kernel we are about to use.
if (!UpdateLegacyKernel(install_config)) {
return false;
}
if (!UpdateEfiBootloaders(platform, install_config)) {
return false;
}
// Update the grub.cfg configuration files.
if (!UpdateEfiGrubCfg(platform, install_config)) {
return false;
}
if (!UpdateEfiBootEntries(install_config)) {
return false;
}
// We finished.
return true;
}
// If crdyboot is in use and the post install context is an update (not
// a fresh install), delete the legacy kernel files from the ESP.
//
// This frees up more space on the ESP for UEFI firmware updates.
//
// Precondition: the boot mode must be kEFI.
//
// Returns true on success (including if there are no files to delete),
// false otherwise.
bool MaybeDeleteLegacyKernels(const InstallConfig& install_config) {
CHECK_EQ(install_config.bios_type, BiosType::kEFI);
// Only delete the legacy kernel for updates, not fresh installs.
//
// For installs, the user could be booting the USB in a different mode
// from the mode the installed system is ultimately booted in. See
// crrev.com/c/3389662 and the linked bugs for details.
//
// Also, fresh installs have a larger ESP already, so freeing up space
// is not necessary.
if (!install_config.is_update) {
return true;
}
// Only delete the legacy kernel if crdyboot is in use. Grub still
// uses the legacy kernel.
const base::FilePath crdyboot_path =
install_config.boot.mount().Append("efi/boot/crdybootx64.efi");
if (!base::PathExists(crdyboot_path)) {
return true;
}
const base::FilePath syslinux_dir =
install_config.boot.mount().Append("syslinux");
// Note: DeleteFile does not return an error if the file does not exist.
return brillo::DeleteFile(syslinux_dir.Append("vmlinuz.A")) &&
brillo::DeleteFile(syslinux_dir.Append("vmlinuz.B"));
}
bool RunNonChromebookPostInstall(const Platform& platform,
const InstallConfig& install_config) {
switch (install_config.bios_type) {
case BiosType::kUnknown:
case BiosType::kSecure:
return false;
case BiosType::kUBoot:
// The Arm platform only uses U-Boot, but may set cros_legacy to mean
// U-Boot without secure boot modifications. This may need handling.
if (!RunLegacyUBootPostInstall(install_config)) {
LOG(ERROR) << "Legacy PostInstall failed.";
return false;
}
return true;
case BiosType::kLegacy:
if (!RunLegacyPostInstall(platform, install_config)) {
LOG(ERROR) << "Legacy PostInstall failed.";
return false;
}
// Configure EFI entries in addition to the legacy.
// Allows devices that can boot installers in legacy
// but will boot the installed target in EFI mode.
// Errors here are not necessarily fatal as the common
// case is the machine will boot successfully from legacy.
if (USE_POSTINSTALL_CONFIG_EFI_AND_LEGACY) {
if (!RunEfiPostInstall(platform, install_config)) {
LOG(WARNING) << "Ignored secondary EFI PostInstall failure.";
}
}
return true;
case BiosType::kEFI:
if (!RunEfiPostInstall(platform, install_config)) {
LOG(ERROR) << "EFI PostInstall failed.";
return false;
}
// Optionally update the legacy boot entries to support
// devices that can boot from the USB in EFI mode with the
// installed disk booting in legacy mode.
if (USE_POSTINSTALL_CONFIG_EFI_AND_LEGACY) {
if (!RunLegacyPostInstall(platform, install_config)) {
LOG(WARNING) << "Ignored secondary Legacy PostInstall failure.";
}
// Conditionally free up space on the ESP for UEFI firmware updates.
if (!MaybeDeleteLegacyKernels(install_config)) {
LOG(WARNING) << "Ignored MaybeDeleteLegacyKernels failure";
}
}
return true;
}
}