From a70c1b38e7681fb2cf9480176401705b1086d9b4 Mon Sep 17 00:00:00 2001 From: Alex Orozco Date: Mon, 19 May 2025 21:41:41 +0000 Subject: [PATCH] devices: Add fw_cfg cli options This allows us to enable/disable the fw_cfg device via the cli We can also now upload files into the guest vm using fw_cfg_items via the cli Signed-off-by: Alex Orozco --- devices/src/legacy/fw_cfg.rs | 28 +++++ src/main.rs | 10 ++ vmm/src/config.rs | 183 ++++++++++++++++++++++++++++++- vmm/src/lib.rs | 2 + vmm/src/vm.rs | 206 ++++++++++++++++++++++------------- vmm/src/vm_config.rs | 75 +++++++++++++ 6 files changed, 429 insertions(+), 75 deletions(-) diff --git a/devices/src/legacy/fw_cfg.rs b/devices/src/legacy/fw_cfg.rs index 4e96d4535..02c52c707 100644 --- a/devices/src/legacy/fw_cfg.rs +++ b/devices/src/legacy/fw_cfg.rs @@ -433,6 +433,34 @@ impl FwCfg { } } + pub fn populate_fw_cfg( + &mut self, + mem_size: Option, + kernel: Option, + initramfs: Option, + cmdline: Option, + fw_cfg_item_list: Option>, + ) -> Result<()> { + if let Some(mem_size) = mem_size { + self.add_e820(mem_size)? + } + if let Some(kernel) = kernel { + self.add_kernel_data(&kernel)?; + } + if let Some(cmdline) = cmdline { + self.add_kernel_cmdline(cmdline); + } + if let Some(initramfs) = initramfs { + self.add_initramfs_data(&initramfs)? + } + if let Some(fw_cfg_item_list) = fw_cfg_item_list { + for item in fw_cfg_item_list { + self.add_item(item)?; + } + } + Ok(()) + } + pub fn add_e820(&mut self, mem_size: usize) -> Result<()> { #[cfg(target_arch = "x86_64")] let mut mem_regions = vec![ diff --git a/src/main.rs b/src/main.rs index bf0d9089d..4ba766e05 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,8 @@ use vmm::api::ApiAction; use vmm::config::{RestoreConfig, VmParams}; use vmm::landlock::{Landlock, LandlockError}; use vmm::vm_config; +#[cfg(feature = "fw_cfg")] +use vmm::vm_config::FwCfgConfig; #[cfg(target_arch = "x86_64")] use vmm::vm_config::SgxEpcConfig; use vmm::vm_config::{ @@ -269,6 +271,12 @@ fn get_cli_options_sorted( .help(FsConfig::SYNTAX) .num_args(1..) .group("vm-config"), + #[cfg(feature = "fw_cfg")] + Arg::new("fw-cfg-config") + .long("fw-cfg-config") + .help(FwCfgConfig::SYNTAX) + .num_args(1) + .group("vm-payload"), #[cfg(feature = "guest_debug")] Arg::new("gdb") .long("gdb") @@ -979,6 +987,8 @@ mod unit_tests { igvm: None, #[cfg(feature = "sev_snp")] host_data: None, + #[cfg(feature = "fw_cfg")] + fw_cfg_config: None, }), rate_limit_groups: None, disks: None, diff --git a/vmm/src/config.rs b/vmm/src/config.rs index b2d940a66..28d8cc6d8 100644 --- a/vmm/src/config.rs +++ b/vmm/src/config.rs @@ -163,6 +163,10 @@ pub enum Error { /// Missing fields in Landlock rules #[error("Error parsing --landlock-rules: path/access field missing")] ParseLandlockMissingFields, + #[cfg(feature = "fw_cfg")] + /// Failed Parsing FwCfgItem config + #[error("Error parsing --fw-cfg-config items")] + ParseFwCfgItem(#[source] OptionParserError), } #[derive(Debug, PartialEq, Eq, Error)] @@ -318,6 +322,18 @@ pub enum ValidationError { /// Invalid block device serial length #[error("Block device serial length ({0}) exceeds maximum allowed length ({1})")] InvalidSerialLength(usize, usize), + #[cfg(feature = "fw_cfg")] + /// FwCfg missing kernel + #[error("Error --fw-cfg-config: missing --kernel")] + FwCfgMissingKernel, + #[cfg(feature = "fw_cfg")] + /// FwCfg missing cmdline + #[error("Error --fw-cfg-config: missing --cmdline")] + FwCfgMissingCmdline, + #[cfg(feature = "fw_cfg")] + /// FwCfg missing initramfs + #[error("Error --fw-cfg-config: missing --initramfs")] + FwCfgMissingInitramfs, } type ValidationResult = std::result::Result; @@ -373,6 +389,8 @@ pub struct VmParams<'a> { pub host_data: Option<&'a str>, pub landlock_enable: bool, pub landlock_rules: Option>, + #[cfg(feature = "fw_cfg")] + pub fw_cfg_config: Option<&'a str>, } impl<'a> VmParams<'a> { @@ -444,7 +462,9 @@ impl<'a> VmParams<'a> { let landlock_rules: Option> = args .get_many::("landlock-rules") .map(|x| x.map(|y| y as &str).collect()); - + #[cfg(feature = "fw_cfg")] + let fw_cfg_config: Option<&str> = + args.get_one::("fw-cfg-config").map(|x| x as &str); VmParams { cpus, memory, @@ -486,6 +506,8 @@ impl<'a> VmParams<'a> { host_data, landlock_enable, landlock_rules, + #[cfg(feature = "fw_cfg")] + fw_cfg_config, } } } @@ -1603,6 +1625,102 @@ impl FsConfig { } } +#[cfg(feature = "fw_cfg")] +impl FwCfgConfig { + pub const SYNTAX: &'static str = "Boot params to pass to FW CFG device \ + \"e820=on|off,kernel=on|off,cmdline=on|off,initramfs=on|off,acpi_table=on|off, \ + items=[name0=,file0=:name1=,file1=]\""; + pub fn parse(fw_cfg_config: &str) -> Result { + let mut parser = OptionParser::new(); + parser + .add("e820") + .add("kernel") + .add("cmdline") + .add("initramfs") + .add("acpi_table") + .add("items"); + parser.parse(fw_cfg_config).map_err(Error::ParseFwCfgItem)?; + let e820 = parser + .convert::("e820") + .map_err(Error::ParseFwCfgItem)? + .unwrap_or(Toggle(true)) + .0; + let kernel = parser + .convert::("kernel") + .map_err(Error::ParseFwCfgItem)? + .unwrap_or(Toggle(true)) + .0; + let cmdline = parser + .convert::("cmdline") + .map_err(Error::ParseFwCfgItem)? + .unwrap_or(Toggle(true)) + .0; + let initramfs = parser + .convert::("initramfs") + .map_err(Error::ParseFwCfgItem)? + .unwrap_or(Toggle(true)) + .0; + let acpi_tables = parser + .convert::("acpi_table") + .map_err(Error::ParseFwCfgItem)? + .unwrap_or(Toggle(true)) + .0; + let items = if parser.is_set("items") { + Some( + parser + .convert::("items") + .map_err(Error::ParseFwCfgItem)? + .unwrap(), + ) + } else { + None + }; + + Ok(FwCfgConfig { + e820, + kernel, + cmdline, + initramfs, + acpi_tables, + items, + }) + } + pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> { + let payload = vm_config.payload.as_ref().unwrap(); + if self.kernel && payload.kernel.is_none() { + return Err(ValidationError::FwCfgMissingKernel); + } else if self.cmdline && payload.cmdline.is_none() { + return Err(ValidationError::FwCfgMissingCmdline); + } else if self.initramfs && payload.initramfs.is_none() { + return Err(ValidationError::FwCfgMissingInitramfs); + } + Ok(()) + } +} + +#[cfg(feature = "fw_cfg")] +impl FwCfgItem { + pub fn parse(fw_cfg: &str) -> Result { + let mut parser = OptionParser::new(); + parser.add("name").add("file"); + parser.parse(fw_cfg).map_err(Error::ParseFwCfgItem)?; + + let name = + parser + .get("name") + .ok_or(Error::ParseFwCfgItem(OptionParserError::InvalidValue( + "missing FwCfgItem name".to_string(), + )))?; + let file = parser + .get("file") + .map(PathBuf::from) + .ok_or(Error::ParseFwCfgItem(OptionParserError::InvalidValue( + "missing FwCfgItem file path".to_string(), + )))?; + Ok(FwCfgItem { name, file }) + } +} + impl PmemConfig { pub const SYNTAX: &'static str = "Persistent memory parameters \ \"file=,size=,iommu=on|off,\ @@ -2661,6 +2779,14 @@ impl VmConfig { disks = Some(disk_config_list); } + #[cfg(feature = "fw_cfg")] + let fw_cfg_config = if let Some(fw_cfg_config_str) = vm_params.fw_cfg_config { + let fw_cfg_config = FwCfgConfig::parse(fw_cfg_config_str)?; + Some(fw_cfg_config) + } else { + None + }; + let mut net: Option> = None; if let Some(net_list) = &vm_params.net { let mut net_config_list = Vec::new(); @@ -2797,6 +2923,8 @@ impl VmConfig { igvm: vm_params.igvm.map(PathBuf::from), #[cfg(feature = "sev_snp")] host_data: vm_params.host_data.map(|s| s.to_string()), + #[cfg(feature = "fw_cfg")] + fw_cfg_config, }) } else { None @@ -3939,6 +4067,8 @@ mod tests { host_data: Some( "243eb7dc1a21129caa91dcbb794922b933baecb5823a377eb431188673288c07".to_string(), ), + #[cfg(feature = "fw_cfg")] + fw_cfg_config: None, }), rate_limit_groups: None, disks: None, @@ -4556,6 +4686,8 @@ mod tests { igvm: None, #[cfg(feature = "sev_snp")] host_data: Some("".to_string()), + #[cfg(feature = "fw_cfg")] + fw_cfg_config: None, }); config_with_no_host_data.validate().unwrap_err(); @@ -4570,6 +4702,8 @@ mod tests { igvm: None, #[cfg(feature = "sev_snp")] host_data: None, + #[cfg(feature = "fw_cfg")] + fw_cfg_config: None, }); valid_config_with_no_host_data.validate().unwrap(); @@ -4586,6 +4720,8 @@ mod tests { host_data: Some( "243eb7dc1a21129caa91dcbb794922b933baecb5823a377eb43118867328".to_string(), ), + #[cfg(feature = "fw_cfg")] + fw_cfg_config: None, }); config_with_invalid_host_data.validate().unwrap_err(); } @@ -4617,4 +4753,49 @@ mod tests { ); Ok(()) } + #[test] + #[cfg(feature = "fw_cfg")] + fn test_fw_cfg_config_item_list_parsing() -> Result<()> { + // Empty list + FwCfgConfig::parse("items=[]").unwrap_err(); + // Missing closing bracket + FwCfgConfig::parse("items=[name=opt/org.test/fw_cfg_test_item,file=/tmp/fw_cfg_test_item") + .unwrap_err(); + // Single Item + assert_eq!( + FwCfgConfig::parse( + "items=[name=opt/org.test/fw_cfg_test_item,file=/tmp/fw_cfg_test_item]" + )?, + FwCfgConfig { + items: Some(FwCfgItemList { + item_list: vec![FwCfgItem { + name: "opt/org.test/fw_cfg_test_item".to_string(), + file: PathBuf::from("/tmp/fw_cfg_test_item"), + }] + }), + ..Default::default() + }, + ); + // Multiple Items + assert_eq!( + FwCfgConfig::parse( + "items=[name=opt/org.test/fw_cfg_test_item,file=/tmp/fw_cfg_test_item:name=opt/org.test/fw_cfg_test_item2,file=/tmp/fw_cfg_test_item2]" + )?, + FwCfgConfig { + items: Some(FwCfgItemList { + item_list: vec![FwCfgItem { + name: "opt/org.test/fw_cfg_test_item".to_string(), + file: PathBuf::from("/tmp/fw_cfg_test_item"), + }, + FwCfgItem { + name: "opt/org.test/fw_cfg_test_item2".to_string(), + file: PathBuf::from("/tmp/fw_cfg_test_item2"), + }] + }), + ..Default::default() + }, + + ); + Ok(()) + } } diff --git a/vmm/src/lib.rs b/vmm/src/lib.rs index ef99fb31a..e51573159 100644 --- a/vmm/src/lib.rs +++ b/vmm/src/lib.rs @@ -2391,6 +2391,8 @@ mod unit_tests { igvm: None, #[cfg(feature = "sev_snp")] host_data: None, + #[cfg(feature = "fw_cfg")] + fw_cfg_config: None, }), rate_limit_groups: None, disks: None, diff --git a/vmm/src/vm.rs b/vmm/src/vm.rs index bed0a4363..4935bc2e9 100644 --- a/vmm/src/vm.rs +++ b/vmm/src/vm.rs @@ -34,6 +34,8 @@ use arch::PciSpaceInfo; use arch::{get_host_cpu_phys_bits, EntryPoint, NumaNode, NumaNodes}; #[cfg(target_arch = "aarch64")] use devices::interrupt_controller; +#[cfg(feature = "fw_cfg")] +use devices::legacy::fw_cfg::FwCfgItem; use devices::AcpiNotificationFlags; #[cfg(all(target_arch = "aarch64", feature = "guest_debug"))] use gdbstub_arch::aarch64::reg::AArch64CoreRegs as CoreRegs; @@ -91,6 +93,8 @@ use crate::migration::get_vm_snapshot; #[cfg(all(target_arch = "x86_64", feature = "guest_debug"))] use crate::migration::url_to_file; use crate::migration::{url_to_path, SNAPSHOT_CONFIG_FILE, SNAPSHOT_STATE_FILE}; +#[cfg(feature = "fw_cfg")] +use crate::vm_config::FwCfgConfig; use crate::vm_config::{ DeviceConfig, DiskConfig, FsConfig, HotplugMethod, NetConfig, NumaConfig, PayloadConfig, PmemConfig, UserDeviceConfig, VdpaConfig, VmConfig, VsockConfig, @@ -359,6 +363,18 @@ pub enum Error { #[cfg(feature = "fw_cfg")] #[error("Error creating acpi tables")] CreatingAcpiTables(#[source] io::Error), + + #[cfg(feature = "fw_cfg")] + #[error("Error adding fw_cfg item")] + AddingFwCfgItem(#[source] io::Error), + + #[cfg(feature = "fw_cfg")] + #[error("Error populating fw_cfg")] + ErrorPopulatingFwCfg(#[source] io::Error), + + #[cfg(feature = "fw_cfg")] + #[error("Error using fw_cfg while disabled")] + FwCfgDisabled, } pub type Result = result::Result; @@ -741,11 +757,22 @@ impl Vm { } #[cfg(feature = "fw_cfg")] - device_manager - .lock() - .unwrap() - .create_fw_cfg_device() - .map_err(Error::DeviceManager)?; + { + let fw_cfg_config = config + .lock() + .unwrap() + .payload + .as_ref() + .map(|p| p.fw_cfg_config.is_some()) + .unwrap_or(false); + if fw_cfg_config { + device_manager + .lock() + .unwrap() + .create_fw_cfg_device() + .map_err(Error::DeviceManager)?; + } + } #[cfg(feature = "tdx")] let kernel = config @@ -806,76 +833,85 @@ impl Vm { #[cfg(feature = "fw_cfg")] fn populate_fw_cfg( + fw_cfg_config: &FwCfgConfig, device_manager: &Arc>, config: &Arc>, ) -> Result<()> { - device_manager - .lock() - .unwrap() - .fw_cfg() - .expect("fw_cfg device must be present") - .lock() - .unwrap() - .add_e820(config.lock().unwrap().memory.size as usize) - .map_err(Error::CreatingE820Map)?; - let kernel = config - .lock() - .unwrap() - .payload - .as_ref() - .map(|p| p.kernel.as_ref().map(File::open)) - .unwrap_or_default() - .transpose() - .map_err(Error::MissingFwCfgKernelFile)?; - if let Some(kernel_file) = kernel { - device_manager - .lock() - .unwrap() - .fw_cfg() - .expect("fw_cfg device must be present") - .lock() - .unwrap() - .add_kernel_data(&kernel_file) - .map_err(Error::MissingFwCfgKernelFile)? + let mut e820_option: Option = None; + if fw_cfg_config.e820 { + e820_option = Some(config.lock().unwrap().memory.size as usize); } - let cmdline = Vm::generate_cmdline( - config.lock().unwrap().payload.as_ref().unwrap(), - #[cfg(target_arch = "aarch64")] - device_manager, - ) - .map_err(|_| Error::MissingFwCfgCmdline)? - .as_cstring() - .map_err(|_| Error::MissingFwCfgCmdline)?; - device_manager - .lock() - .unwrap() - .fw_cfg() - .expect("fw_cfg device must be present") - .lock() - .unwrap() - .add_kernel_cmdline(cmdline); - let initramfs = config - .lock() - .unwrap() - .payload - .as_ref() - .map(|p| p.initramfs.as_ref().map(File::open)) - .unwrap_or_default() - .transpose() - .map_err(Error::MissingFwCfgInitramfs)?; - // We measure the initramfs when running Oak Containers in SNP mode (initramfs = Stage1) - // o/w use Stage0 to launch cloud disk images - if let Some(initramfs_file) = initramfs { - device_manager + let mut kernel_option: Option = None; + if fw_cfg_config.kernel { + let kernel = config .lock() .unwrap() - .fw_cfg() - .expect("fw_cfg device must be present") + .payload + .as_ref() + .map(|p| p.kernel.as_ref().map(File::open)) + .unwrap_or_default() + .transpose() + .map_err(Error::MissingFwCfgKernelFile)?; + kernel_option = kernel; + } + let mut cmdline_option: Option = None; + if fw_cfg_config.cmdline { + let cmdline = Vm::generate_cmdline( + config.lock().unwrap().payload.as_ref().unwrap(), + #[cfg(target_arch = "aarch64")] + device_manager, + ) + .map_err(|_| Error::MissingFwCfgCmdline)? + .as_cstring() + .map_err(|_| Error::MissingFwCfgCmdline)?; + cmdline_option = Some(cmdline); + } + let mut initramfs_option: Option = None; + if fw_cfg_config.initramfs { + let initramfs = config .lock() .unwrap() - .add_initramfs_data(&initramfs_file) + .payload + .as_ref() + .map(|p| p.initramfs.as_ref().map(File::open)) + .unwrap_or_default() + .transpose() .map_err(Error::MissingFwCfgInitramfs)?; + // We measure the initramfs when running Oak Containers in SNP mode (initramfs = Stage1) + // o/w use Stage0 to launch cloud disk images + initramfs_option = initramfs; } + let mut fw_cfg_item_list_option: Option> = None; + if let Some(fw_cfg_files) = &fw_cfg_config.items { + let mut fw_cfg_item_list = vec![]; + for fw_cfg_file in fw_cfg_files.item_list.clone() { + fw_cfg_item_list.push(FwCfgItem { + name: fw_cfg_file.name, + content: devices::legacy::fw_cfg::FwCfgContent::File( + 0, + File::open(fw_cfg_file.file).map_err(Error::AddingFwCfgItem)?, + ), + }); + } + fw_cfg_item_list_option = Some(fw_cfg_item_list); + } + + let device_manager_binding = device_manager.lock().unwrap(); + let Some(fw_cfg) = device_manager_binding.fw_cfg() else { + return Err(Error::FwCfgDisabled); + }; + + fw_cfg + .lock() + .unwrap() + .populate_fw_cfg( + e820_option, + kernel_option, + initramfs_option, + cmdline_option, + fw_cfg_item_list_option, + ) + .map_err(Error::ErrorPopulatingFwCfg)?; Ok(()) } @@ -2370,15 +2406,37 @@ impl Vm { #[cfg(feature = "fw_cfg")] { - Self::populate_fw_cfg(&self.device_manager, &self.config)?; - let tpm_enabled = self.config.lock().unwrap().tpm.is_some(); - crate::acpi::create_acpi_tables_for_fw_cfg( - &self.device_manager, - &self.cpu_manager, - &self.memory_manager, - &self.numa_nodes, - tpm_enabled, - )? + let fw_cfg_enabled = self + .config + .lock() + .unwrap() + .payload + .as_ref() + .map(|p| p.fw_cfg_config.is_some()) + .unwrap_or(false); + if fw_cfg_enabled { + let fw_cfg_config = self + .config + .lock() + .unwrap() + .payload + .as_ref() + .map(|p| p.fw_cfg_config.clone()) + .unwrap_or_default() + .ok_or(Error::VmMissingConfig)?; + Self::populate_fw_cfg(&fw_cfg_config, &self.device_manager, &self.config)?; + + if fw_cfg_config.acpi_tables { + let tpm_enabled = self.config.lock().unwrap().tpm.is_some(); + crate::acpi::create_acpi_tables_for_fw_cfg( + &self.device_manager, + &self.cpu_manager, + &self.memory_manager, + &self.numa_nodes, + tpm_enabled, + )? + } + } } // Do earlier to parallelise with loading kernel diff --git a/vmm/src/vm_config.rs b/vmm/src/vm_config.rs index 3e6cf447d..2d829a678 100644 --- a/vmm/src/vm_config.rs +++ b/vmm/src/vm_config.rs @@ -4,6 +4,8 @@ // use std::net::{IpAddr, Ipv4Addr}; use std::path::PathBuf; +#[cfg(feature = "fw_cfg")] +use std::str::FromStr; use std::{fs, result}; use net_util::MacAddr; @@ -699,6 +701,79 @@ pub struct PayloadConfig { #[cfg(feature = "sev_snp")] #[serde(default)] pub host_data: Option, + #[cfg(feature = "fw_cfg")] + pub fw_cfg_config: Option, +} + +#[cfg(feature = "fw_cfg")] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct FwCfgConfig { + pub e820: bool, + pub kernel: bool, + pub cmdline: bool, + pub initramfs: bool, + pub acpi_tables: bool, + pub items: Option, +} + +#[cfg(feature = "fw_cfg")] +impl Default for FwCfgConfig { + fn default() -> Self { + FwCfgConfig { + e820: true, + kernel: true, + cmdline: true, + initramfs: true, + acpi_tables: true, + items: None, + } + } +} + +#[cfg(feature = "fw_cfg")] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct FwCfgItemList { + #[serde(default)] + pub item_list: Vec, +} + +#[cfg(feature = "fw_cfg")] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct FwCfgItem { + #[serde(default)] + pub name: String, + #[serde(default)] + pub file: PathBuf, +} + +#[cfg(feature = "fw_cfg")] +pub enum FwCfgItemError { + InvalidValue(String), +} + +#[cfg(feature = "fw_cfg")] +impl FromStr for FwCfgItemList { + type Err = FwCfgItemError; + + fn from_str(s: &str) -> Result { + let body = s + .trim() + .strip_prefix('[') + .and_then(|s| s.strip_suffix(']')) + .ok_or_else(|| FwCfgItemError::InvalidValue(s.to_string()))?; + + let mut fw_cfg_items: Vec = vec![]; + let items: Vec<&str> = body.split(':').collect(); + for item in items { + fw_cfg_items.push( + FwCfgItem::parse(item) + .map_err(|_| FwCfgItemError::InvalidValue(item.to_string()))?, + ); + } + Ok(FwCfgItemList { + item_list: fw_cfg_items, + }) + } } impl ApplyLandlock for PayloadConfig {