diff --git a/rust/tool/shared/src/generation.rs b/rust/tool/shared/src/generation.rs index 0e46fde6..088d7c6c 100644 --- a/rust/tool/shared/src/generation.rs +++ b/rust/tool/shared/src/generation.rs @@ -19,6 +19,7 @@ use time::Date; pub struct ExtendedBootJson { pub bootspec: BootSpec, pub lanzaboote_extension: LanzabooteExtension, + pub xen_extension: Option, } #[derive(Debug, Clone, Deserialize)] @@ -52,6 +53,40 @@ impl From for LanzabooteExtension { } } +// TODO: Should it be moved to bootspec crate itself? +// Probably after its standardization, bootspec crate is only used in lanzaboote for now, bootspec support is +// ad-hoc in nixpkgs right now. +// +// Aliases are used for org.xenproject.bootspec.v1 compatibility +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct XenExtension { + #[serde(alias = "xen")] + pub efi_path: String, + #[serde(alias = "xenParams")] + pub params: Vec, +} + +impl TryFrom for XenExtension { + type Error = (); + fn try_from(spec: bootspec::Specialisation) -> Result { + spec.extensions + .get("org.xenproject.bootspec.v2") + .and_then(|v| serde_json::from_value::(v.clone()).ok()) + .ok_or(()) + } +} + +impl TryFrom for XenExtension { + type Error = (); + fn try_from(spec: bootspec::BootJson) -> Result { + spec.extensions + .get("org.xenproject.bootspec.v2") + .and_then(|v| serde_json::from_value::(v.clone()).ok()) + .ok_or(()) + } +} + /// A system configuration. /// /// Can be built from a GenerationLink. @@ -98,6 +133,7 @@ impl Generation { spec: ExtendedBootJson { bootspec: specialisation.clone().generation, lanzaboote_extension: specialisation.clone().into(), + xen_extension: specialisation.clone().try_into().ok(), }, specialisations: Self::parse_specialisations( link, @@ -132,7 +168,8 @@ impl Generation { specialisation_name, spec: ExtendedBootJson { bootspec: bootspec.clone(), - lanzaboote_extension: boot_json.into(), + lanzaboote_extension: boot_json.clone().into(), + xen_extension: boot_json.try_into().ok(), }, specialisations: Self::parse_specialisations(link, bootspec.specialisations)?, }) diff --git a/rust/tool/shared/src/os_release.rs b/rust/tool/shared/src/os_release.rs index 1c3281ae..95df946c 100644 --- a/rust/tool/shared/src/os_release.rs +++ b/rust/tool/shared/src/os_release.rs @@ -48,6 +48,19 @@ impl OsRelease { Ok(Self(map)) } + + pub fn pretty_name(&self) -> &str { + self.0 + .get("PRETTY_NAME") + .map(|v| v.as_str()) + .unwrap_or_default() + } + pub fn version_id(&self) -> &str { + self.0 + .get("VERSION_ID") + .map(|v| v.as_str()) + .unwrap_or_default() + } } impl FromStr for OsRelease { diff --git a/rust/tool/shared/src/pe.rs b/rust/tool/shared/src/pe.rs index 23b05dd7..9afe0977 100644 --- a/rust/tool/shared/src/pe.rs +++ b/rust/tool/shared/src/pe.rs @@ -4,7 +4,7 @@ use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; use std::process::Command; -use anyhow::{Context, Result}; +use anyhow::{ensure, Context, Result}; use goblin::pe::PE; use serde::{Deserialize, Serialize}; use tempfile::TempDir; @@ -106,21 +106,19 @@ pub fn lanzaboote_image( tempdir.write_secure_file(file_hash(&stub_parameters.initrd_store_path)?.as_slice())?; let os_release = tempdir.write_secure_file(&stub_parameters.os_release_contents)?; - let os_release_offs = stub_offset(&stub_parameters.lanzaboote_store_path)?; - let kernel_cmdline_offs = os_release_offs + file_size(&os_release)?; - let initrd_path_offs = kernel_cmdline_offs + file_size(&kernel_cmdline_file)?; - let kernel_path_offs = initrd_path_offs + file_size(&initrd_path_file)?; - let initrd_hash_offs = kernel_path_offs + file_size(&kernel_path_file)?; - let kernel_hash_offs = initrd_hash_offs + file_size(&initrd_hash_file)?; - - let sections = vec![ - s(".osrel", os_release, os_release_offs), - s(".cmdline", kernel_cmdline_file, kernel_cmdline_offs), - s(".initrd", initrd_path_file, initrd_path_offs), - s(".linux", kernel_path_file, kernel_path_offs), - s(".initrdh", initrd_hash_file, initrd_hash_offs), - s(".linuxh", kernel_hash_file, kernel_hash_offs), + + let mut sections = vec![ + s(".osrel", os_release), + s(".cmdline", kernel_cmdline_file), + s(".initrd", initrd_path_file), + s(".linux", kernel_path_file), + s(".initrdh", initrd_hash_file), + s(".linuxh", kernel_hash_file), ]; + calculate_offsets( + stub_offset(&stub_parameters.lanzaboote_store_path)?, + &mut sections, + )?; let image_path = tempdir.path().join(tmpname()); wrap_in_pe( @@ -131,6 +129,47 @@ pub fn lanzaboote_image( Ok(image_path) } +// FIXME: This function generates huge images, as it copies kernel & initrd inside, +// lanzaboote stub needs to be extended to support xen images. +#[allow(clippy::too_many_arguments)] +pub fn xen_image( + tempdir: &TempDir, + xen_stub: &Path, + xen_options: &[String], + kernel_cmdline: &[String], + kernel: &Path, + initrd: &Path, +) -> Result { + use std::fmt::Write; + let mut xen_config = String::new(); + writeln!(xen_config, "[global]")?; + writeln!(xen_config, "default=xen")?; + writeln!(xen_config)?; + writeln!(xen_config, "[xen]")?; + writeln!( + xen_config, + "kernel=nope_this_is_uxi {}", + kernel_cmdline.join(" ") + )?; + writeln!(xen_config, "ramdisk=nope_this_is_uxi")?; + writeln!(xen_config, "options={}", xen_options.join(" "),)?; + let xen_config_file = tempdir.write_secure_file(xen_config)?; + + ensure!(kernel.ends_with("bzImage"), "kernel is not a bzImgae"); + + let mut sections = vec![ + s(".config", xen_config_file), + s(".kernel", kernel), + s(".ramdisk", initrd), + ]; + + calculate_offsets(xen_offset(xen_stub)?, &mut sections)?; + + let image_path = tempdir.path().join(tmpname()); + wrap_in_pe(xen_stub, sections, &image_path)?; + Ok(image_path) +} + /// Take a PE binary stub and attach sections to it. /// /// The resulting binary is then written to a newly created file at the provided output path. @@ -162,9 +201,13 @@ struct Section { } impl Section { + fn resolved_offset(&self) -> bool { + self.offset != u64::MAX + } /// Create objcopy `-add-section` command line parameters that /// attach the section to a PE file. fn to_objcopy(&self) -> Vec { + assert!(self.resolved_offset(), "section offset is not resolved!"); // There is unfortunately no format! for OsString, so we cannot // just format a path. let mut map_str: OsString = format!("{}=", self.name).into(); @@ -179,13 +222,26 @@ impl Section { } } -fn s(name: &'static str, file_path: impl AsRef, offset: u64) -> Section { +fn s(name: &'static str, file_path: impl AsRef) -> Section { Section { name, file_path: file_path.as_ref().into(), - offset, + offset: u64::MAX, } } +// EFI sections needs to be 4k aligned +fn align_to_4k(num: u64) -> u64 { + (num + 0xFFF) & !0xFFF +} +fn calculate_offsets(mut current: u64, sections: &mut [Section]) -> Result<()> { + for section in sections.iter_mut() { + current = align_to_4k(current); + assert!(!section.resolved_offset(), "section offset is known"); + section.offset = current; + current += file_size(§ion.file_path)?; + } + Ok(()) +} /// Convert a path to an UEFI path relative to the specified ESP. fn esp_relative_uefi_path(esp: &Path, path: &Path) -> Result { @@ -222,6 +278,20 @@ fn stub_offset(binary: &Path) -> Result { .expect("Failed to calculate offset"), ) + image_base) } +fn xen_offset(binary: &Path) -> Result { + let pe_binary = fs::read(binary).context("Failed to read PE binary file")?; + let pe = PE::parse(&pe_binary).context("Failed to parse PE binary file")?; + + let image_base = image_base(&pe); + + let pad_section = pe + .sections + .iter() + .find(|s| s.name().ok() == Some(".pad")) + .expect("failed to discover .pad section"); + let offset = pad_section.virtual_size + pad_section.virtual_address; + Ok(u64::from(offset) + image_base) +} fn image_base(pe: &PE) -> u64 { pe.header diff --git a/rust/tool/systemd/src/esp.rs b/rust/tool/systemd/src/esp.rs index 7227367f..b97a2b8e 100644 --- a/rust/tool/systemd/src/esp.rs +++ b/rust/tool/systemd/src/esp.rs @@ -16,10 +16,11 @@ pub struct SystemdEspPaths { pub systemd: PathBuf, pub systemd_boot: PathBuf, pub loader: PathBuf, + pub entries: PathBuf, pub systemd_boot_loader_config: PathBuf, } -impl EspPaths<10> for SystemdEspPaths { +impl EspPaths<11> for SystemdEspPaths { fn new(esp: impl AsRef, architecture: Architecture) -> Self { let esp = esp.as_ref(); let efi = esp.join("EFI"); @@ -28,6 +29,7 @@ impl EspPaths<10> for SystemdEspPaths { let efi_systemd = efi.join("systemd"); let efi_efi_fallback_dir = efi.join("BOOT"); let loader = esp.join("loader"); + let entries = loader.join("entries"); let systemd_boot_loader_config = loader.join("loader.conf"); Self { @@ -40,6 +42,7 @@ impl EspPaths<10> for SystemdEspPaths { systemd: efi_systemd.clone(), systemd_boot: efi_systemd.join(architecture.systemd_filename()), loader, + entries, systemd_boot_loader_config, } } @@ -52,7 +55,7 @@ impl EspPaths<10> for SystemdEspPaths { &self.linux } - fn iter(&self) -> std::array::IntoIter<&PathBuf, 10> { + fn iter(&self) -> std::array::IntoIter<&PathBuf, 11> { [ &self.esp, &self.efi, @@ -63,6 +66,7 @@ impl EspPaths<10> for SystemdEspPaths { &self.systemd, &self.systemd_boot, &self.loader, + &self.entries, &self.systemd_boot_loader_config, ] .into_iter() diff --git a/rust/tool/systemd/src/install.rs b/rust/tool/systemd/src/install.rs index 55c370cb..972e5e0e 100644 --- a/rust/tool/systemd/src/install.rs +++ b/rust/tool/systemd/src/install.rs @@ -119,12 +119,12 @@ impl Installer { if self.broken_gens.is_empty() { log::info!("Collecting garbage..."); - // Only collect garbage in these two directories. This way, no files that do not belong to + // Only collect garbage in these directories. This way, no files that do not belong to // the NixOS installation are deleted. Lanzatool takes full control over the esp/EFI/nixos // directory and deletes ALL files that it doesn't know about. Dual- or multiboot setups // that need files in this directory will NOT work. self.gc_roots.collect_garbage(&self.esp_paths.nixos)?; - // The esp/EFI/Linux directory is assumed to be potentially shared with other distros. + // The esp/EFI/Linux and loader/entries directories are assumed to be potentially shared with other distros. // Thus, only files that start with "nixos-" are garbage collected (i.e. potentially // deleted). self.gc_roots @@ -133,6 +133,12 @@ impl Installer { .and_then(|n| n.to_str()) .is_some_and(|n| n.starts_with("nixos-")) })?; + self.gc_roots + .collect_garbage_with_filter(&self.esp_paths.entries, |p| { + p.file_name() + .and_then(|n| n.to_str()) + .map_or(false, |n| n.starts_with("nixos-")) + })?; } else { // This might produce a ridiculous message if you have a lot of malformed generations. let warning = indoc::formatdoc! {" @@ -287,11 +293,65 @@ impl Installer { let stub_target = self .esp_paths .linux - .join(stub_name(generation, &self.signer).context("Get stub name")?); + .join(stub_name(generation, &self.signer, "nixos").context("Get stub name")?); self.gc_roots.extend([&stub_target]); install_signed(&self.signer, &lanzaboote_image_path, &stub_target) .context("Failed to install the Lanzaboote stub.")?; + if let Some(xen) = &generation.spec.xen_extension { + use std::fmt::Write; + let xen_image = pe::xen_image( + &tempdir, + &PathBuf::from(&xen.efi_path), + &xen.params, + &kernel_cmdline, + &bootspec.kernel, + &initrd_location, + ) + .context("Failed to assemble xen image")?; + + let stub_name = stub_name(generation, &self.signer, "nixos-xen")?; + let stub_target = self.esp_paths.linux.join(&stub_name); + self.gc_roots.extend([&stub_target]); + install_signed(&self.signer, &xen_image, &stub_target) + .context("Failed to install the Lanzaboote image.")?; + + // Entry name works as a sort key (?), reusing stub_name to make + // it compatible with UKI entries. + let entry_path = self + .esp_paths + .entries + .join(format!("{}.conf", stub_name.display())); + self.gc_roots.extend([&entry_path]); + + let mut entry = String::new(); + writeln!( + entry, + "title {} (with Xen Hypervisor)", + os_release.pretty_name() + )?; + writeln!(entry, "version {}", os_release.version_id())?; + // stub_target should be utf-8, .display() is ok here. + // TODO: but better cleanup this. Use esp_relative_uefi_path + // for this calculation. + writeln!( + entry, + "efi {}", + stub_target.strip_prefix(&self.esp_paths.esp)?.display() + )?; + writeln!( + entry, + "sort-key {}", + generation.spec.lanzaboote_extension.sort_key, + )?; + + let entry_tmp = tempdir.write_secure_file(entry)?; + + // Entry name is unique (with regards to comment about + // specialisations above). + install(&entry_tmp, &entry_path)?; + } + Ok(()) } @@ -302,7 +362,7 @@ impl Installer { let stub_target = self .esp_paths .linux - .join(stub_name(generation, &self.signer).context("While getting stub name")?); + .join(stub_name(generation, &self.signer, "nixos").context("While getting stub name")?); let stub = fs::read(&stub_target) .with_context(|| format!("Failed to read the stub: {}", stub_target.display()))?; let kernel_path = resolve_efi_path( @@ -320,6 +380,20 @@ impl Installer { self.gc_roots .extend([&stub_target, &kernel_path, &initrd_path]); + if generation.spec.xen_extension.is_some() { + let stub_name = stub_name(generation, &self.signer, "nixos-xen") + .context("While getting stub name")?; + let stub_target = self.esp_paths.linux.join(&stub_name); + let entry_path = self + .esp_paths + .linux + .join(format!("{}.conf", stub_name.display())); + if !stub_target.exists() || !entry_path.exists() { + anyhow::bail!("Missing xen efi or entry.") + } + self.gc_roots.extend([&stub_target, &entry_path]); + } + Ok(()) } @@ -397,7 +471,7 @@ fn resolve_efi_path(esp: &Path, efi_path: &[u8]) -> Result { /// Compute the file name to be used for the stub of a certain generation, signed with the given key. /// /// The generated name is input-addressed by the toplevel corresponding to the generation and the public part of the signing key. -fn stub_name(generation: &Generation, signer: &S) -> Result { +fn stub_name(generation: &Generation, signer: &S, prefix: &str) -> Result { let bootspec = &generation.spec.bootspec.bootspec; let public_key = signer.get_public_key()?; let stub_inputs = [ @@ -413,12 +487,12 @@ fn stub_name(generation: &Generation, signer: &S) -> Result )); if let Some(specialisation_name) = &generation.specialisation_name { Ok(PathBuf::from(format!( - "nixos-generation-{}-specialisation-{}-{}.efi", + "{prefix}-generation-{}-specialisation-{}-{}.efi", generation, specialisation_name, stub_input_hash ))) } else { Ok(PathBuf::from(format!( - "nixos-generation-{}-{}.efi", + "{prefix}-generation-{}-{}.efi", generation, stub_input_hash ))) }