Skip to content

Commit

Permalink
Improve EFI, add support for root=boot and writing UUID file
Browse files Browse the repository at this point in the history
In FCOS we never tried to support root=boot, but for bootupd
to do alongside installs in the general case we have to.

There were two things to fix here:

 - Tweak the EFI "trampoline" to check for both $prefix/grub.cfg and $prefix/boot/grub.cfg
 - Add support for writing the boot UUID into both places

(This avoids higher level tools like bootupd needing to know
 about how to find the EFI vendor dir)

Then the bigger issue here is that we need to support invoking
`efibootmgr` to re-synchronize the firmware.  This is particularly
important in cases like bootc "alongside" installs where we're
taking over the target OS.
  • Loading branch information
cgwalters committed Nov 11, 2023
1 parent 9d5d9d5 commit 581102a
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 23 deletions.
1 change: 1 addition & 0 deletions src/bios.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ impl Component for Bios {
src_root: &openat::Dir,
dest_root: &str,
device: &str,
_update_firmware: bool,
) -> Result<InstalledContent> {
let meta = if let Some(meta) = get_component_update(src_root, self)? {
meta
Expand Down
42 changes: 31 additions & 11 deletions src/bootupd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,28 @@ pub(crate) enum ClientRequest {
Status,
}

pub(crate) enum ConfigMode {
None,
Static,
WithUUID,
}

impl ConfigMode {
pub(crate) fn enabled_with_uuid(&self) -> Option<bool> {
match self {
ConfigMode::None => None,
ConfigMode::Static => Some(false),
ConfigMode::WithUUID => Some(true),
}
}
}

pub(crate) fn install(
source_root: &str,
dest_root: &str,
device: Option<&str>,
with_static_configs: bool,
configs: ConfigMode,
update_firmware: bool,
target_components: Option<&[String]>,
auto_components: bool,
) -> Result<()> {
Expand Down Expand Up @@ -78,7 +95,7 @@ pub(crate) fn install(
}

let meta = component
.install(&source_root, dest_root, device)
.install(&source_root, dest_root, device, update_firmware)
.with_context(|| format!("installing component {}", component.name()))?;
log::info!("Installed {} {}", component.name(), meta.meta.version);
state.installed.insert(component.name().into(), meta);
Expand All @@ -89,14 +106,17 @@ pub(crate) fn install(
}
let sysroot = &openat::Dir::open(dest_root)?;

if with_static_configs {
#[cfg(any(
target_arch = "x86_64",
target_arch = "aarch64",
target_arch = "powerpc64"
))]
crate::grubconfigs::install(sysroot, installed_efi)?;
// On other architectures, assume that there's nothing to do.
match configs.enabled_with_uuid() {
Some(uuid) => {
#[cfg(any(
target_arch = "x86_64",
target_arch = "aarch64",
target_arch = "powerpc64"
))]
crate::grubconfigs::install(sysroot, installed_efi, uuid)?;
// On other architectures, assume that there's nothing to do.
}
None => {}
}

// Unmount the ESP, etc.
Expand Down Expand Up @@ -126,7 +146,7 @@ pub(crate) fn get_components_impl(auto: bool) -> Components {
#[cfg(target_arch = "x86_64")]
{
if auto {
let is_efi_booted = Path::new("/sys/firmware/efi").exists();
let is_efi_booted = crate::efi::is_efi_booted().unwrap();
log::info!(
"System boot method: {}",
if is_efi_booted { "EFI" } else { "BIOS" }
Expand Down
21 changes: 19 additions & 2 deletions src/cli/bootupd.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::bootupd;
use crate::bootupd::{self, ConfigMode};
use anyhow::{Context, Result};
use clap::Parser;
use log::LevelFilter;
Expand Down Expand Up @@ -56,6 +56,15 @@ pub struct InstallOpts {
#[clap(long)]
with_static_configs: bool,

/// Implies `--with-static-configs`. When present, this also writes a
/// file with the UUID of the target filesystems.
#[clap(long)]
write_uuid: bool,

/// On EFI systems, invoke `efibootmgr` to update the firmware.
#[clap(long)]
update_firmware: bool,

#[clap(long = "component", conflicts_with = "auto")]
/// Only install these components
components: Option<Vec<String>>,
Expand Down Expand Up @@ -97,11 +106,19 @@ impl DCommand {

/// Runner for `install` verb.
pub(crate) fn run_install(opts: InstallOpts) -> Result<()> {
let configmode = if opts.write_uuid {
ConfigMode::WithUUID
} else if opts.with_static_configs {
ConfigMode::Static
} else {
ConfigMode::None
};
bootupd::install(
&opts.src_root,
&opts.dest_root,
opts.device.as_deref(),
opts.with_static_configs,
configmode,
opts.update_firmware,
opts.components.as_deref(),
opts.auto,
)
Expand Down
1 change: 1 addition & 0 deletions src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub(crate) trait Component {
src_root: &openat::Dir,
dest_root: &str,
device: &str,
update_firmware: bool,
) -> Result<InstalledContent>;

/// Implementation of `bootupd generate-update-metadata` for a given component.
Expand Down
105 changes: 99 additions & 6 deletions src/efi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use std::path::{Path, PathBuf};
use std::process::Command;

use anyhow::{bail, Context, Result};
use fn_error_context::context;
use openat_ext::OpenatDirExt;
use widestring::U16CString;

Expand All @@ -22,6 +23,10 @@ use crate::{component::*, packagesystem};
/// Well-known paths to the ESP that may have been mounted external to us.
pub(crate) const ESP_MOUNTS: &[&str] = &["boot/efi", "efi", "boot"];

/// The binary to change EFI boot ordering
const EFIBOOTMGR: &str = "efibootmgr";
const SHIM: &str = "shimx64.efi";

/// The ESP partition label on Fedora CoreOS derivatives
pub(crate) const COREOS_ESP_PART_LABEL: &str = "EFI-SYSTEM";
pub(crate) const ANACONDA_ESP_PART_LABEL: &str = "EFI\\x20System\\x20Partition";
Expand All @@ -30,6 +35,13 @@ pub(crate) const ANACONDA_ESP_PART_LABEL: &str = "EFI\\x20System\\x20Partition";
const LOADER_INFO_VAR_STR: &str = "LoaderInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f";
const STUB_INFO_VAR_STR: &str = "StubInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f";

/// Return `true` if the system is booted via EFI
pub(crate) fn is_efi_booted() -> Result<bool> {
Path::new("/sys/firmware/efi")
.try_exists()
.map_err(Into::into)
}

#[derive(Default)]
pub(crate) struct Efi {
mountpoint: RefCell<Option<PathBuf>>,
Expand Down Expand Up @@ -113,6 +125,17 @@ impl Efi {
}
Ok(())
}

#[context("Updating EFI firmware variables")]
fn update_firmware(&self, device: &str, espdir: &openat::Dir) -> Result<()> {
let efidir = &espdir.sub_dir("EFI").context("Opening EFI")?;
let vendordir = super::grubconfigs::find_efi_vendordir(efidir)?;
let vendordir = vendordir
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid non-UTF-8 vendordir"))?;
clear_efi_current()?;
set_efi_current(device, espdir, vendordir)
}
}

/// Convert a nul-terminated UTF-16 byte array to a String.
Expand Down Expand Up @@ -259,7 +282,8 @@ impl Component for Efi {
&self,
src_root: &openat::Dir,
dest_root: &str,
_: &str,
device: &str,
update_firmware: bool,
) -> Result<InstalledContent> {
let meta = if let Some(meta) = get_component_update(src_root, self)? {
meta
Expand All @@ -270,11 +294,11 @@ impl Component for Efi {
let srcdir_name = component_updatedirname(self);
let ft = crate::filetree::FileTree::new_from_dir(&src_root.sub_dir(&srcdir_name)?)?;
let destdir = &self.ensure_mounted_esp(Path::new(dest_root))?;
{
let destd = openat::Dir::open(destdir)
.with_context(|| format!("opening dest dir {}", destdir.display()))?;
validate_esp(&destd)?;
}

let destd = &openat::Dir::open(destdir)
.with_context(|| format!("opening dest dir {}", destdir.display()))?;
validate_esp(destd)?;

// TODO - add some sort of API that allows directly setting the working
// directory to a file descriptor.
let r = std::process::Command::new("cp")
Expand All @@ -286,6 +310,9 @@ impl Component for Efi {
if !r.success() {
anyhow::bail!("Failed to copy");
}
if update_firmware {
self.update_firmware(device, destd)?
}
Ok(InstalledContent {
meta,
filetree: Some(ft),
Expand Down Expand Up @@ -399,3 +426,69 @@ fn validate_esp(dir: &openat::Dir) -> Result<()> {
};
Ok(())
}

#[context("Clearing current EFI boot entry")]
pub(crate) fn clear_efi_current() -> Result<()> {
const BOOTCURRENT: &str = "BootCurrent";
if !crate::efi::is_efi_booted()? {
log::debug!("System is not booted via EFI");
return Ok(());
}
let output = Command::new(EFIBOOTMGR).output()?;
if !output.status.success() {
anyhow::bail!("Failed to invoke {EFIBOOTMGR}")
}
let output = String::from_utf8(output.stdout)?;
let current = output
.lines()
.filter_map(|l| l.split_once(':'))
.filter_map(|(k, v)| (k == BOOTCURRENT).then_some(v.trim()))
.next()
.ok_or_else(|| anyhow::anyhow!("Failed to find BootCurrent"))?;
let output = Command::new(EFIBOOTMGR)
.args(["-b", current, "-B"])
.output()?;
if !output.status.success() {
std::io::copy(
&mut std::io::Cursor::new(output.stderr),
&mut std::io::stderr().lock(),
)?;
anyhow::bail!("Failed to invoke {EFIBOOTMGR}");
}
anyhow::Ok(())
}

#[context("Adding new EFI boot entry")]
pub(crate) fn set_efi_current(device: &str, espdir: &openat::Dir, vendordir: &str) -> Result<()> {
let fsinfo = crate::filesystem::inspect_filesystem(espdir, ".")?;
let source = fsinfo.source;
let devname = source
.rsplit_once('/')
.ok_or_else(|| anyhow::anyhow!("Failed to parse {source}"))?
.1;
let partition_path = format!("/sys/class/block/{devname}/partition");
let partition_number = std::fs::read_to_string(&partition_path)
.with_context(|| format!("Failed to read {partition_path}"))?;
let shim = format!("{vendordir}/{SHIM}");
if espdir.exists(&shim)? {
anyhow::bail!("Failed to find {SHIM}");
}
let loader = format!("\\EFI\\{}\\shimx64.efi", vendordir);
let st = Command::new(EFIBOOTMGR)
.args([
"--create",
"--disk",
device,
"--part",
partition_number.as_str(),
"--loader",
loader.as_str(),
"--label",
vendordir,
])
.status()?;
if !st.success() {
anyhow::bail!("Failed to invoke {EFIBOOTMGR}")
}
anyhow::Ok(())
}
44 changes: 44 additions & 0 deletions src/filesystem.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use std::os::fd::AsRawFd;
use std::os::unix::process::CommandExt;
use std::process::Command;

use anyhow::{Context, Result};
use fn_error_context::context;
use serde::Deserialize;

#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
#[allow(dead_code)]
pub(crate) struct Filesystem {
pub(crate) source: String,
pub(crate) fstype: String,
pub(crate) options: String,
pub(crate) uuid: Option<String>,
}

#[derive(Deserialize, Debug)]
pub(crate) struct Findmnt {
pub(crate) filesystems: Vec<Filesystem>,
}

#[context("Inspecting filesystem {path:?}")]
pub(crate) fn inspect_filesystem(root: &openat::Dir, path: &str) -> Result<Filesystem> {
let rootfd = root.as_raw_fd();
// SAFETY: This is unsafe just for the pre_exec, when we port to cap-std we can use cap-std-ext
let o = unsafe {
Command::new("findmnt")
.args(["-J", "-v", "--output-all", path])
.pre_exec(move || nix::unistd::fchdir(rootfd).map_err(Into::into))
.output()?
};
let st = o.status;
if !st.success() {
anyhow::bail!("findmnt failed: {st:?}");
}
let o: Findmnt = serde_json::from_reader(std::io::Cursor::new(&o.stdout))
.context("Parsing findmnt output")?;
o.filesystems
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("findmnt returned no data"))
}
9 changes: 7 additions & 2 deletions src/grub2/grub-static-efi.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ else
search --label boot --set prefix --no-floppy
fi
fi
set prefix=($prefix)/grub2
configfile $prefix/grub.cfg
if [ -d ($prefix)/grub2 ]; then
set prefix=($prefix)/grub2
configfile $prefix/grub.cfg
else
set prefix=($prefix)/boot/grub2
configfile $prefix/grub.cfg
fi
boot

Loading

0 comments on commit 581102a

Please sign in to comment.