From cddf4246dd2c2ce9729218edcc8b079c5818df64 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 5 Dec 2025 16:36:00 -0500 Subject: [PATCH] libvirt: Add QEMU firmware interop JSON descriptor support Parse the QEMU firmware interop specification JSON descriptors to find firmware instead of doing it manually. This fixes OVMF firmware detection on Debian/Ubuntu derivatives. Add a hidden `bcvk libvirt print-firmware` which is a cheap way to test our parser. Assisted-by: Claude Code (Sonnet 4.5) Signed-off-by: Colin Walters --- .../src/tests/libvirt_verb.rs | 60 +++ crates/kit/src/libvirt/domain.rs | 148 ++++++- crates/kit/src/libvirt/mod.rs | 5 + crates/kit/src/libvirt/print_firmware.rs | 123 ++++++ crates/kit/src/libvirt/run.rs | 19 +- crates/kit/src/libvirt/secureboot.rs | 377 +++++++++++++++--- crates/kit/src/main.rs | 3 + 7 files changed, 670 insertions(+), 65 deletions(-) create mode 100644 crates/kit/src/libvirt/print_firmware.rs diff --git a/crates/integration-tests/src/tests/libvirt_verb.rs b/crates/integration-tests/src/tests/libvirt_verb.rs index 66815e8..95ba0fc 100644 --- a/crates/integration-tests/src/tests/libvirt_verb.rs +++ b/crates/integration-tests/src/tests/libvirt_verb.rs @@ -959,6 +959,66 @@ fn test_libvirt_run_no_storage_opts_without_bind_storage() -> Result<()> { } integration_test!(test_libvirt_run_no_storage_opts_without_bind_storage); +/// Test print-firmware command (hidden debugging command) +fn test_libvirt_print_firmware() -> Result<()> { + let bck = get_bck_command()?; + + // Test YAML output (default) + let yaml_output = Command::new(&bck) + .args(["libvirt", "print-firmware"]) + .output() + .expect("Failed to run libvirt print-firmware"); + + let stdout = String::from_utf8_lossy(&yaml_output.stdout); + let stderr = String::from_utf8_lossy(&yaml_output.stderr); + + // Should succeed and produce YAML output + assert!( + yaml_output.status.success(), + "libvirt print-firmware should succeed. stderr: {}", + stderr + ); + + // Verify YAML output contains expected fields + assert!( + stdout.contains("architecture:"), + "YAML output should contain architecture field" + ); + + println!("libvirt print-firmware YAML output:\n{}", stdout); + + // Test JSON output + let json_output = Command::new(&bck) + .args(["libvirt", "print-firmware", "--format", "json"]) + .output() + .expect("Failed to run libvirt print-firmware --format json"); + + let json_stdout = String::from_utf8_lossy(&json_output.stdout); + + if json_output.status.success() { + // Verify it's valid JSON + let json_result: std::result::Result = + serde_json::from_str(&json_stdout); + assert!( + json_result.is_ok(), + "libvirt print-firmware --format json should produce valid JSON: {}", + json_stdout + ); + + let json_value = json_result.unwrap(); + assert!( + json_value.get("architecture").is_some(), + "JSON output should contain architecture field" + ); + + println!("libvirt print-firmware JSON output:\n{}", json_stdout); + } + + println!("libvirt print-firmware test passed"); + Ok(()) +} +integration_test!(test_libvirt_print_firmware); + /// Test error handling for invalid configurations fn test_libvirt_error_handling() -> Result<()> { let bck = get_bck_command()?; diff --git a/crates/kit/src/libvirt/domain.rs b/crates/kit/src/libvirt/domain.rs index 392b1ee..dad2b1c 100644 --- a/crates/kit/src/libvirt/domain.rs +++ b/crates/kit/src/libvirt/domain.rs @@ -23,6 +23,16 @@ pub struct VirtiofsFilesystem { pub readonly: bool, } +/// Configuration for firmware debug log output +#[derive(Debug, Clone)] +pub enum FirmwareLogOutput { + /// Write firmware log to a file on the host + #[allow(dead_code)] + File(String), + /// Make firmware log available via virsh console (pty) + Console, +} + /// Builder for creating libvirt domain XML configurations #[derive(Debug)] pub struct DomainBuilder { @@ -41,7 +51,10 @@ pub struct DomainBuilder { firmware: Option, tpm: bool, ovmf_code_path: Option, // Custom OVMF_CODE path for secure boot + ovmf_code_format: Option, // Format of OVMF_CODE (raw, qcow2) nvram_template: Option, // Custom NVRAM template with enrolled keys + nvram_format: Option, // Format of NVRAM template (raw, qcow2) + firmware_log: Option, // OVMF debug log output via isa-debugcon } impl Default for DomainBuilder { @@ -69,7 +82,10 @@ impl DomainBuilder { firmware: None, // Defaults to UEFI tpm: true, // Default to enabled ovmf_code_path: None, + ovmf_code_format: None, nvram_template: None, + nvram_format: None, + firmware_log: Some(FirmwareLogOutput::Console), // Default to pty for virsh console access } } @@ -153,15 +169,38 @@ impl DomainBuilder { self } - /// Set custom OVMF_CODE path for secure boot - pub fn with_ovmf_code_path(mut self, path: &str) -> Self { + /// Set custom OVMF_CODE path and format for secure boot + /// + /// Format must be specified (either "raw" or "qcow2") and should come from + /// the QEMU firmware interop JSON descriptors. + pub fn with_ovmf_code_path(mut self, path: &str, format: &str) -> Self { self.ovmf_code_path = Some(path.to_string()); + self.ovmf_code_format = Some(format.to_string()); self } - /// Set custom NVRAM template path with enrolled secure boot keys - pub fn with_nvram_template(mut self, path: &str) -> Self { + /// Set custom NVRAM template path and format with enrolled secure boot keys + /// + /// Format must be specified (either "raw" or "qcow2") and should come from + /// the QEMU firmware interop JSON descriptors. + pub fn with_nvram_template(mut self, path: &str, format: &str) -> Self { self.nvram_template = Some(path.to_string()); + self.nvram_format = Some(format.to_string()); + self + } + + /// Enable firmware debug log output via isa-debugcon (x86_64 only) + /// + /// This captures OVMF/EDK2 DEBUG() output which is useful for debugging + /// Secure Boot failures and other firmware issues. The log is available + /// on IO port 0x402. + /// + /// Options: + /// - `FirmwareLogOutput::File(path)` - Write to a file on the host + /// - `FirmwareLogOutput::Console` - Access via `virsh console serial1` + #[allow(dead_code)] + pub fn with_firmware_log(mut self, output: FirmwareLogOutput) -> Self { + self.firmware_log = Some(output); self } @@ -232,8 +271,16 @@ impl DomainBuilder { if use_uefi { if let Some(ref ovmf_code) = self.ovmf_code_path { // Use custom OVMF_CODE path for secure boot - let mut loader_attrs = - vec![("readonly", "yes"), ("type", "pflash"), ("format", "raw")]; + // Format is required and comes from QEMU firmware interop JSON descriptors + let code_format = self + .ovmf_code_format + .as_deref() + .expect("ovmf_code_format must be set when ovmf_code_path is set"); + let mut loader_attrs = vec![ + ("readonly", "yes"), + ("type", "pflash"), + ("format", code_format), + ]; if secure_boot { loader_attrs.push(("secure", "yes")); } @@ -241,13 +288,18 @@ impl DomainBuilder { // Add NVRAM element if template is specified if let Some(ref nvram_template) = self.nvram_template { + // Format is required and comes from QEMU firmware interop JSON descriptors + let nvram_fmt = self + .nvram_format + .as_deref() + .expect("nvram_format must be set when nvram_template is set"); writer.write_text_element_with_attrs( "nvram", "", // Empty content, template attr provides the source &[ ("template", nvram_template), - ("templateFormat", "raw"), - ("format", "raw"), + ("templateFormat", nvram_fmt), + ("format", nvram_fmt), ], )?; } @@ -369,6 +421,29 @@ impl DomainBuilder { writer.write_empty_element("target", &[("type", "virtio")])?; writer.end_element("console")?; + // Firmware debug log via isa-debugcon (x86_64 only) + // This captures OVMF/EDK2 DEBUG() output on IO port 0x402, useful for + // debugging Secure Boot failures. Access via: virsh console serial0 + // See: https://libvirt.org/formatdomain.html#serial-port (isa-debug target type) + if arch_config.arch == "x86_64" { + if let Some(ref firmware_log) = self.firmware_log { + let (serial_type, source_path) = match firmware_log { + FirmwareLogOutput::Console => ("pty", None), + FirmwareLogOutput::File(path) => ("file", Some(path.as_str())), + }; + + writer.start_element("serial", &[("type", serial_type)])?; + if let Some(path) = source_path { + writer.write_empty_element("source", &[("path", path)])?; + } + writer.start_element("target", &[("type", "isa-debug"), ("port", "0")])?; + writer.write_empty_element("model", &[("name", "isa-debugcon")])?; + writer.end_element("target")?; + writer.write_empty_element("address", &[("type", "isa"), ("iobase", "0x402")])?; + writer.end_element("serial")?; + } + } + // VNC graphics if enabled if let Some(vnc_port) = self.vnc_port { writer.write_empty_element( @@ -638,8 +713,8 @@ mod tests { let xml = DomainBuilder::new() .with_name("test-custom-secboot") .with_firmware(FirmwareType::UefiSecure) - .with_ovmf_code_path("/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd") - .with_nvram_template("/var/lib/libvirt/qemu/nvram/custom_VARS.fd") + .with_ovmf_code_path("/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd", "raw") + .with_nvram_template("/var/lib/libvirt/qemu/nvram/custom_VARS.fd", "raw") .build_xml() .unwrap(); @@ -701,4 +776,57 @@ mod tests { assert!(xml_ro.contains("source dir=\"/host/storage\"")); assert!(xml_ro.contains("target dir=\"hoststorage\"")); } + + #[test] + fn test_firmware_log_default() { + // By default, firmware log should be enabled (pty/console mode) + let xml = DomainBuilder::new() + .with_name("test-firmware-log-default") + .build_xml() + .unwrap(); + + // On x86_64, should have isa-debugcon serial device + if std::env::consts::ARCH == "x86_64" { + assert!(xml.contains("serial type=\"pty\"")); + assert!(xml.contains("target type=\"isa-debug\"")); + assert!(xml.contains("model name=\"isa-debugcon\"")); + assert!(xml.contains("address type=\"isa\" iobase=\"0x402\"")); + } + } + + #[test] + fn test_firmware_log_file() { + // Test firmware log to file + let xml = DomainBuilder::new() + .with_name("test-firmware-log-file") + .with_firmware_log(FirmwareLogOutput::File("/tmp/ovmf-debug.log".to_string())) + .build_xml() + .unwrap(); + + // On x86_64, should have isa-debugcon with file output + if std::env::consts::ARCH == "x86_64" { + assert!(xml.contains("serial type=\"file\"")); + assert!(xml.contains("source path=\"/tmp/ovmf-debug.log\"")); + assert!(xml.contains("target type=\"isa-debug\"")); + assert!(xml.contains("model name=\"isa-debugcon\"")); + assert!(xml.contains("address type=\"isa\" iobase=\"0x402\"")); + } + } + + #[test] + fn test_firmware_log_disabled() { + // Test disabling firmware log by setting firmware_log to None after construction + // Note: There's no public API to disable it once set, but we can test the XML + // generation doesn't include it on non-x86 architectures + let xml = DomainBuilder::new() + .with_name("test-firmware-log") + .build_xml() + .unwrap(); + + // On non-x86_64, should NOT have isa-debugcon (it's x86-only) + if std::env::consts::ARCH != "x86_64" { + assert!(!xml.contains("isa-debugcon")); + assert!(!xml.contains("isa-debug")); + } + } } diff --git a/crates/kit/src/libvirt/mod.rs b/crates/kit/src/libvirt/mod.rs index 1f72b0f..7e1bc17 100644 --- a/crates/kit/src/libvirt/mod.rs +++ b/crates/kit/src/libvirt/mod.rs @@ -30,6 +30,7 @@ pub mod domain; pub mod inspect; pub mod list; pub mod list_volumes; +pub mod print_firmware; pub mod rm; pub mod rm_all; pub mod run; @@ -220,4 +221,8 @@ pub enum LibvirtSubcommands { /// Manage base disk images used for VM cloning #[clap(name = "base-disks")] BaseDisks(base_disks_cli::LibvirtBaseDisksOpts), + + /// Print detected firmware paths and configuration + #[clap(name = "print-firmware", hide = true)] + PrintFirmware(print_firmware::LibvirtPrintFirmwareOpts), } diff --git a/crates/kit/src/libvirt/print_firmware.rs b/crates/kit/src/libvirt/print_firmware.rs new file mode 100644 index 0000000..d9fe9b2 --- /dev/null +++ b/crates/kit/src/libvirt/print_firmware.rs @@ -0,0 +1,123 @@ +//! Print firmware information command +//! +//! This module provides a command to display detected OVMF firmware paths +//! and configuration for debugging firmware detection issues. + +use clap::Parser; +use color_eyre::{eyre::Context, Result}; +use serde::{Deserialize, Serialize}; + +use super::secureboot::{find_ovmf_vars, find_secure_boot_firmware}; + +/// Options for the print-firmware command +#[derive(Debug, Parser)] +pub struct LibvirtPrintFirmwareOpts { + /// Output format (yaml or json) + #[clap(long, default_value = "yaml", value_enum)] + pub format: OutputFormat, +} + +/// Output format for print-firmware command +#[derive(Debug, Clone, clap::ValueEnum)] +pub enum OutputFormat { + /// YAML format (default, human-readable) + Yaml, + /// JSON format (machine-readable) + Json, +} + +/// Firmware information for display +#[derive(Debug, Serialize, Deserialize)] +pub struct PrintFirmwareInfo { + /// Path to OVMF_VARS.fd (or equivalent) + pub vars_path: Option, + /// Path to OVMF_CODE.secboot.fd (or equivalent) + pub code_secboot_path: Option, + /// Format of OVMF_CODE file (raw or qcow2) + pub code_format: Option, + /// Format of OVMF_VARS file (raw or qcow2) + pub vars_format: Option, + /// Current architecture + pub architecture: String, +} + +/// Execute the print-firmware command +pub fn run(opts: LibvirtPrintFirmwareOpts) -> Result<()> { + // Try to find OVMF_VARS (non-secboot variant) + let vars_path = match find_ovmf_vars() { + Ok(path) => Some(path.to_string()), + Err(e) => { + tracing::debug!("Failed to find OVMF_VARS: {}", e); + None + } + }; + + // Try to find secure boot firmware (CODE and VARS with formats) + let (code_secboot_path, code_format, vars_format) = match find_secure_boot_firmware() { + Ok(fw_info) => ( + Some(fw_info.code_path.to_string()), + Some(fw_info.code_format), + Some(fw_info.vars_format), + ), + Err(e) => { + tracing::debug!("Failed to find secure boot firmware: {}", e); + (None, None, None) + } + }; + + let info = PrintFirmwareInfo { + vars_path, + code_secboot_path, + code_format, + vars_format, + architecture: std::env::consts::ARCH.to_string(), + }; + + // Output in requested format + match opts.format { + OutputFormat::Yaml => { + println!( + "{}", + serde_yaml::to_string(&info) + .with_context(|| "Failed to serialize firmware info as YAML")? + ); + } + OutputFormat::Json => { + println!( + "{}", + serde_json::to_string_pretty(&info) + .with_context(|| "Failed to serialize firmware info as JSON")? + ); + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_firmware_info_serialization() { + let info = PrintFirmwareInfo { + vars_path: Some("/usr/share/edk2/ovmf/OVMF_VARS.fd".to_string()), + code_secboot_path: Some("/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd".to_string()), + code_format: Some("raw".to_string()), + vars_format: Some("raw".to_string()), + architecture: "x86_64".to_string(), + }; + + // Test YAML serialization + let yaml = serde_yaml::to_string(&info).unwrap(); + assert!(yaml.contains("vars_path")); + assert!(yaml.contains("OVMF_VARS.fd")); + assert!(yaml.contains("code_format")); + + // Test JSON serialization + let json = serde_json::to_string(&info).unwrap(); + assert!(json.contains("vars_path")); + assert!(json.contains("OVMF_VARS.fd")); + assert!(json.contains("code_format")); + } +} diff --git a/crates/kit/src/libvirt/run.rs b/crates/kit/src/libvirt/run.rs index aba80d5..2353cf6 100644 --- a/crates/kit/src/libvirt/run.rs +++ b/crates/kit/src/libvirt/run.rs @@ -1077,8 +1077,14 @@ fn create_libvirt_domain_from_disk( eyre::ensure!(opts.firmware == FirmwareType::UefiSecure); + // Place the OVMF vars file in the libvirt storage pool so it's lifecycled with the VM + let pool_path = get_libvirt_storage_pool_path(global_opts.connect.as_deref()) + .context("Failed to get libvirt storage pool path for secure boot vars")?; + let vars_output_path = pool_path.join(format!("{}_OVMF_VARS.fd", domain_name)); + info!("Setting up secure boot configuration from {}", keys); - let config = secureboot::setup_secure_boot(&keys).context("Failed to setup secure boot")?; + let config = secureboot::setup_secure_boot(&keys, &vars_output_path) + .context("Failed to setup secure boot")?; Some(config) } else { None @@ -1129,15 +1135,18 @@ fn create_libvirt_domain_from_disk( // Add secure boot configuration if enabled if let Some(ref sb_config) = secure_boot_config { - let ovmf_code = crate::libvirt::secureboot::find_ovmf_code_secboot() - .context("Failed to find OVMF_CODE.secboot.fd")?; + // Get firmware info with paths and formats from QEMU firmware descriptors + let firmware_info = crate::libvirt::secureboot::find_secure_boot_firmware() + .context("Failed to find secure boot firmware")?; let sb_vars_path = sb_config .vars_template .canonicalize_utf8() .context("Canonicalizing secureboot vars path")?; + + // Use the formats from the firmware descriptors domain_builder = domain_builder - .with_ovmf_code_path(ovmf_code.as_str()) - .with_nvram_template(sb_vars_path.as_str()); + .with_ovmf_code_path(firmware_info.code_path.as_str(), &firmware_info.code_format) + .with_nvram_template(sb_vars_path.as_str(), &sb_config.vars_format); // Add secure boot keys path to metadata for reference domain_builder = diff --git a/crates/kit/src/libvirt/secureboot.rs b/crates/kit/src/libvirt/secureboot.rs index 01c9f2f..a0b7c10 100644 --- a/crates/kit/src/libvirt/secureboot.rs +++ b/crates/kit/src/libvirt/secureboot.rs @@ -4,7 +4,124 @@ //! keys (PK, KEK, db) and customizing OVMF firmware variables for VMs. use camino::{Utf8Path, Utf8PathBuf}; +use cap_std_ext::cap_std::fs::Dir; +use cap_std_ext::dirext::{CapStdExtDirExt, WalkConfiguration}; use color_eyre::{eyre::eyre, Result}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::ops::ControlFlow; + +/// System-wide QEMU firmware descriptor search directories +const QEMU_FIRMWARE_DIRS: &[&str] = &["/etc/qemu/firmware", "/usr/share/qemu/firmware"]; + +/// QEMU firmware descriptor executable section +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct FirmwareExecutable { + /// Path to the firmware file + pub(crate) filename: String, + /// Format of the firmware file (e.g., "raw") + pub(crate) format: String, +} + +/// QEMU firmware descriptor nvram template section +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct FirmwareNvramTemplate { + /// Path to the NVRAM template file + pub(crate) filename: String, + /// Format of the NVRAM template file (e.g., "raw") + pub(crate) format: String, +} + +/// QEMU firmware descriptor mapping section +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct FirmwareMapping { + /// Device type + pub(crate) device: String, + /// Executable (firmware) configuration (for flash device type) + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) executable: Option, + /// NVRAM template configuration (for flash device type) + #[serde(rename = "nvram-template", skip_serializing_if = "Option::is_none")] + pub(crate) nvram_template: Option, + /// Single firmware filename (for memory device type) + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) filename: Option, +} + +impl FirmwareMapping { + /// QEMU firmware device type: memory-mapped + pub(crate) const DEVICE_TYPE_MEMORY: &'static str = "memory"; + + /// QEMU firmware device type: flash + #[allow(dead_code)] + pub(crate) const DEVICE_TYPE_FLASH: &'static str = "flash"; +} + +/// QEMU firmware descriptor target architecture +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct FirmwareTarget { + /// Architecture name (e.g., "x86_64", "aarch64") + pub(crate) architecture: String, + /// Supported machines + pub(crate) machines: Vec, +} + +/// QEMU firmware descriptor (QEMU firmware interop specification) +/// See: https://qemu.readthedocs.io/en/latest/interop/firmware.json.html +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct FirmwareDescriptor { + /// Human-readable description + pub(crate) description: String, + /// Interface types (e.g., "uefi") + #[serde(rename = "interface-types")] + pub(crate) interface_types: Vec, + /// Firmware mapping + pub(crate) mapping: FirmwareMapping, + /// Target architectures + pub(crate) targets: Vec, + /// Features (e.g., "secure-boot", "enrolled-keys") + pub(crate) features: Vec, + /// Tags + pub(crate) tags: Vec, +} + +impl FirmwareDescriptor { + /// QEMU firmware feature: secure boot support + pub(crate) const FEATURE_SECURE_BOOT: &'static str = "secure-boot"; + + /// QEMU firmware feature: enrolled keys + pub(crate) const FEATURE_ENROLLED_KEYS: &'static str = "enrolled-keys"; + + /// Check if this firmware supports secure boot + pub(crate) fn supports_secure_boot(&self) -> bool { + self.features + .contains(&Self::FEATURE_SECURE_BOOT.to_string()) + } + + /// Check if this firmware has enrolled keys + pub(crate) fn has_enrolled_keys(&self) -> bool { + self.features + .contains(&Self::FEATURE_ENROLLED_KEYS.to_string()) + } + + /// Check if this firmware supports the given architecture + pub(crate) fn supports_architecture(&self, arch: &str) -> bool { + self.targets.iter().any(|t| t.architecture == arch) + } +} + +/// UEFI firmware paths and formats from QEMU firmware interop descriptors +#[derive(Debug, Clone)] +pub struct FirmwareInfo { + /// Path to OVMF_CODE firmware file + pub code_path: Utf8PathBuf, + /// Format of the OVMF_CODE file (raw, qcow2) + pub code_format: String, + /// Path to OVMF_VARS template file + pub vars_path: Utf8PathBuf, + /// Format of the OVMF_VARS file (raw, qcow2) + pub vars_format: String, +} /// Secure Boot key configuration #[derive(Debug, Clone)] @@ -13,6 +130,8 @@ pub struct SecureBootConfig { pub key_dir: Utf8PathBuf, /// Path to custom OVMF_VARS template with enrolled keys pub vars_template: Utf8PathBuf, + /// Format of the NVRAM template file (raw, qcow2) + pub vars_format: String, /// GUID for the key owner #[allow(dead_code)] pub guid: String, @@ -107,9 +226,7 @@ pub fn customize_ovmf_vars( let check_output = check.output()?; if !check_output.status.success() { - return Err(eyre!( - "virt-fw-vars not found. Install it with: dnf install -y python3-virt-firmware" - )); + return Err(eyre!("virt-fw-vars tool not found")); } // Use virt-fw-vars to inject keys into OVMF_VARS @@ -144,80 +261,240 @@ pub fn customize_ovmf_vars( } /// Load and setup secure boot configuration from existing keys -pub fn setup_secure_boot(key_dir: &Utf8Path) -> Result { +/// +/// The `vars_output_path` should be in the libvirt storage pool so that +/// the OVMF vars file is lifecycled with the VM (e.g., deleted with `--nvram`). +pub fn setup_secure_boot( + key_dir: &Utf8Path, + vars_output_path: &Utf8Path, +) -> Result { tracing::info!("Loading secure boot keys from {}", key_dir); let keys = SecureBootKeys::load(key_dir)?; - // Path for the customized OVMF_VARS template - let vars_template = key_dir.join("OVMF_VARS_CUSTOM.fd"); + // Find the system firmware (includes format info) + let firmware_info = find_firmware_from_descriptors(true)?; - // Find the system OVMF_VARS.fd - let ovmf_vars = find_ovmf_vars()?; - - // Check if custom vars template already exists - let mut test_template = std::process::Command::new("test"); - test_template.args(["-f", vars_template.as_str()]); - - if !test_template.status()?.success() { - tracing::info!("Creating custom OVMF_VARS template with enrolled keys"); - customize_ovmf_vars(&keys, &ovmf_vars, &vars_template)?; + // Check if custom vars template already exists at the output path + if !vars_output_path.exists() { + tracing::info!( + "Creating custom OVMF_VARS template with enrolled keys at {}", + vars_output_path + ); + customize_ovmf_vars(&keys, &firmware_info.vars_path, vars_output_path)?; } + // virt-fw-vars preserves the input format, so the output has the same format as the input Ok(SecureBootConfig { key_dir: key_dir.to_owned(), - vars_template, + vars_template: vars_output_path.to_owned(), + vars_format: firmware_info.vars_format, guid: keys.guid, }) } -/// Find the system OVMF_VARS.fd file -fn find_ovmf_vars() -> Result { - // Common locations for OVMF_VARS.fd - let locations = [ - "/usr/share/edk2/ovmf/OVMF_VARS.fd", - "/usr/share/OVMF/OVMF_VARS.fd", - "/usr/share/qemu/OVMF_VARS.fd", - "/usr/share/edk2-ovmf/OVMF_VARS.fd", - ]; +/// Get firmware search directories following QEMU firmware interop specification +fn get_firmware_search_dirs() -> Vec { + let mut dirs = Vec::new(); - for path in &locations { - let mut test_file = std::process::Command::new("test"); - test_file.args(["-f", path]); - - if test_file.status()?.success() { - return Ok(Utf8PathBuf::from(path)); + // $XDG_CONFIG_HOME/qemu/firmware or $HOME/.config/qemu/firmware + if let Some(config_home) = std::env::var_os("XDG_CONFIG_HOME") { + if let Ok(mut path) = Utf8PathBuf::try_from(config_home) { + path.push("qemu/firmware"); + dirs.push(path); + } + } else if let Some(home) = std::env::var_os("HOME") { + if let Ok(mut path) = Utf8PathBuf::try_from(home) { + path.push(".config/qemu/firmware"); + dirs.push(path); } } - Err(eyre!( - "Could not find OVMF_VARS.fd. Please install edk2-ovmf package." - )) + // System-wide directories + dirs.extend(QEMU_FIRMWARE_DIRS.iter().map(|d| Utf8PathBuf::from(d))); + + dirs } -/// Find the secure boot OVMF_CODE file -pub fn find_ovmf_code_secboot() -> Result { - // Common locations for OVMF_CODE.secboot.fd - let locations = [ - "/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd", - "/usr/share/OVMF/OVMF_CODE.secboot.fd", - "/usr/share/qemu/OVMF_CODE.secboot.fd", - "/usr/share/edk2-ovmf/OVMF_CODE.secboot.fd", - ]; +/// List all firmware descriptor JSON files +pub(crate) fn list_firmware_descriptors() -> Result> { + let search_dirs = get_firmware_search_dirs(); + let mut descriptors = Vec::new(); + + let root = Dir::open_ambient_dir("/", cap_std_ext::cap_std::ambient_authority())?; + + for dir_path in search_dirs { + // Strip leading slash to make path relative to root Dir + let relative_path = dir_path + .as_str() + .strip_prefix('/') + .unwrap_or(dir_path.as_str()); + // Use open_dir_optional to handle non-existent directories gracefully + let Some(dir) = root.open_dir_optional(relative_path)? else { + continue; + }; - for path in &locations { - let mut test_file = std::process::Command::new("test"); - test_file.args(["-f", path]); + // Use walk with sort_by_file_name to get entries in lexical order per QEMU firmware interop spec + let config = WalkConfiguration::default().sort_by_file_name(); + dir.walk::<_, color_eyre::eyre::Error>(&config, |component| { + // Only process regular files with .json extension at the top level + if component.file_type.is_file() { + if let Some(ext) = component.path.extension() { + if ext == "json" { + let full_path = dir_path.join( + Utf8Path::from_path(component.path) + .ok_or_else(|| eyre!("Non-UTF8 path: {:?}", component.path))?, + ); + descriptors.push(full_path); + } + } + } + Ok(ControlFlow::Continue(())) + })?; + } + + Ok(descriptors) +} + +/// Load and parse a firmware descriptor JSON file +pub(crate) fn load_firmware_descriptor(path: &Utf8Path) -> Result { + let content = fs::read_to_string(path) + .map_err(|e| eyre!("Failed to read firmware descriptor {}: {}", path, e))?; - if test_file.status()?.success() { - return Ok(Utf8PathBuf::from(path)); + serde_json::from_str(&content) + .map_err(|e| eyre!("Failed to parse firmware descriptor {}: {}", path, e)) +} + +/// Get the current architecture in QEMU format +pub(crate) fn get_qemu_architecture() -> &'static str { + match std::env::consts::ARCH { + "powerpc64" => "ppc64", + "powerpc64le" => "ppc64le", + arch => arch, + } +} + +/// Find firmware using QEMU firmware interop JSON descriptors +/// +/// This follows the same approach as systemd-vmspawn: +/// - Searches in $XDG_CONFIG_HOME/qemu/firmware, /etc/qemu/firmware, /usr/share/qemu/firmware +/// - Filters by architecture and secure boot support +/// - Skips firmware with enrolled keys (known to cause issues) +fn find_firmware_from_descriptors(require_secure_boot: bool) -> Result { + let descriptors = list_firmware_descriptors()?; + let arch = get_qemu_architecture(); + + for descriptor_path in descriptors { + let descriptor = load_firmware_descriptor(&descriptor_path)?; + + // Skip firmware with enrolled keys (known to cause issues) + if descriptor.has_enrolled_keys() { + tracing::debug!( + "Skipping {}, firmware has enrolled keys which has been known to cause issues", + descriptor_path + ); + continue; + } + + // Check architecture support + if !descriptor.supports_architecture(arch) { + tracing::debug!( + "Skipping {}, firmware doesn't support architecture {}", + descriptor_path, + arch + ); + continue; + } + + // Check secure boot requirement + if require_secure_boot && !descriptor.supports_secure_boot() { + tracing::debug!( + "Skipping {}, firmware doesn't support secure boot", + descriptor_path + ); + continue; + } + + // Skip memory-mapped firmware (we need separate code and vars files) + if descriptor.mapping.device == FirmwareMapping::DEVICE_TYPE_MEMORY { + tracing::debug!( + "Skipping {}, memory-mapped firmware not supported", + descriptor_path + ); + continue; } + + // Extract code and vars paths and formats from flash device firmware + let firmware_info = match ( + &descriptor.mapping.executable, + &descriptor.mapping.nvram_template, + ) { + (Some(executable), Some(nvram_template)) => FirmwareInfo { + code_path: Utf8PathBuf::from(&executable.filename), + code_format: executable.format.clone(), + vars_path: Utf8PathBuf::from(&nvram_template.filename), + vars_format: nvram_template.format.clone(), + }, + _ => { + tracing::debug!( + "Skipping {}, missing executable or nvram-template fields", + descriptor_path + ); + continue; + } + }; + + tracing::debug!("Selected firmware definition {}", descriptor_path); + return Ok(firmware_info); } Err(eyre!( - "Could not find OVMF_CODE.secboot.fd. Please install edk2-ovmf package." + "No suitable firmware descriptor found for architecture {} with secure_boot={}", + arch, + require_secure_boot )) } +/// Find the system OVMF_VARS.fd file using QEMU firmware interop JSON descriptors +pub(crate) fn find_ovmf_vars() -> Result { + let firmware_info = find_firmware_from_descriptors(false)?; + + if !firmware_info.vars_path.exists() { + return Err(eyre!( + "Firmware descriptor returned non-existent path: {}. Please verify your QEMU firmware installation.", + firmware_info.vars_path + )); + } + + tracing::debug!( + "Found OVMF_VARS via firmware descriptor: {}", + firmware_info.vars_path + ); + Ok(firmware_info.vars_path) +} + +/// Find secure boot firmware using QEMU firmware interop JSON descriptors +/// +/// Returns full firmware info including paths and formats for both CODE and VARS +pub fn find_secure_boot_firmware() -> Result { + let firmware_info = find_firmware_from_descriptors(true)?; + + if !firmware_info.code_path.exists() { + return Err(eyre!( + "Firmware descriptor returned non-existent path: {}. Please verify your QEMU firmware installation.", + firmware_info.code_path + )); + } + + tracing::debug!( + "Found secure boot firmware: code={} ({}), vars={} ({})", + firmware_info.code_path, + firmware_info.code_format, + firmware_info.vars_path, + firmware_info.vars_format + ); + Ok(firmware_info) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/kit/src/main.rs b/crates/kit/src/main.rs index b630093..772e5fc 100644 --- a/crates/kit/src/main.rs +++ b/crates/kit/src/main.rs @@ -186,6 +186,9 @@ fn main() -> Result<(), Report> { libvirt::LibvirtSubcommands::BaseDisks(opts) => { libvirt::base_disks_cli::run(&options, opts)? } + libvirt::LibvirtSubcommands::PrintFirmware(opts) => { + libvirt::print_firmware::run(opts)? + } } } Commands::LibvirtUploadDisk(opts) => {