Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6543,6 +6543,7 @@ dependencies = [
"guid",
"hypervisor_resources",
"libc",
"loader",
"mesh",
"openvmm_defs",
"pal_async",
Expand Down
13 changes: 13 additions & 0 deletions openvmm/openvmm_entry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1672,6 +1672,19 @@ async fn vm_config_from_command_line(
},
force_dma_bounce_enabled: uefi_options.force_dma_bounce,
smbios: ged_smbios,
uefi_overload_images: cli_args::default_uefi_firmware()
.map(|path| {
openvmm_helpers::uefi_overload::uefi_overload_images_from_file(&path)
.unwrap_or_else(|err| {
tracing::warn!(
error = err.as_ref() as &dyn std::error::Error,
path = %path.display(),
"failed to read the default uefi firmware; firmware overload will be rejected"
);
Vec::new()
})
})
.unwrap_or_default(),
}
.into_resource(),
),
Expand Down
1 change: 1 addition & 0 deletions openvmm/openvmm_helpers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ disklayer_vhdx.workspace = true
get_resources.workspace = true
guid = { workspace = true, features = ["mesh"] }
hypervisor_resources.workspace = true
loader.workspace = true
openvmm_defs.workspace = true
vm_resource.workspace = true

Expand Down
1 change: 1 addition & 0 deletions openvmm/openvmm_helpers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ pub mod disk;
pub mod hypervisor;
pub mod shared_memory;
pub mod snapshot;
pub mod uefi_overload;
pub mod underhill;
83 changes: 83 additions & 0 deletions openvmm/openvmm_helpers/src/uefi_overload.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Helpers for building the UEFI images an OpenHCL VM's GED can load in
//! response to a `LoadFirmware` (firmware overload) request.

use anyhow::Context;
use get_resources::ged::UefiOverloadImage;
use std::path::Path;

/// Returns the hibernate firmware token a mu_msvm UEFI image identifies
/// itself with: its `major.minor`, with major in the high byte.
///
/// Returns `None` if the image has no version record.
pub fn uefi_firmware_token(image: &[u8]) -> anyhow::Result<Option<u64>> {
let Some(version) = loader::uefi::firmware_version::find_in_firmware_image(image)
.context("invalid uefi firmware version record")?
else {
return Ok(None);
};
token_from_base_version(version.base_version)
.with_context(|| format!("unrecognized uefi version {:?}", version.base_version))
.map(Some)
}

/// Builds the GED overload image that loads `image` for `token`.
pub fn uefi_overload_image(token: u64, image: Vec<u8>) -> anyhow::Result<UefiOverloadImage> {
let layout = loader::uefi::image_layout(&image).context("invalid uefi image")?;
Ok(UefiOverloadImage {
token,
base_gpa: layout.base_gpa,
entry_offset: layout.entry_offset,
image,
})
}

/// The legacy `LoadFirmware` token of OpenHCL 1.8, from before tokens tracked
/// the UEFI version.
pub const OPENHCL_1_8_TOKEN: u64 = 0x0108;

/// Reads the UEFI image at `path` and builds the GED overload catalog it
/// serves: the token it identifies itself with (if it has a version record)
/// and [`OPENHCL_1_8_TOKEN`].
pub fn uefi_overload_images_from_file(path: &Path) -> anyhow::Result<Vec<UefiOverloadImage>> {
let image = fs_err::read(path)?;
let mut tokens = Vec::new();
if let Some(token) = uefi_firmware_token(&image)? {
tokens.push(token);
}
// OpenHCL 1.8 shipped a 26.0 UEFI, which this image supersedes.
tokens.push(OPENHCL_1_8_TOKEN);
tokens
.into_iter()
.map(|token| uefi_overload_image(token, image.clone()))
.collect()
}

fn token_from_base_version(base_version: &str) -> Option<u64> {
let mut parts = base_version.split('.');
let major: u8 = parts.next()?.parse().ok()?;
let minor: u8 = parts.next()?.parse().ok()?;
Some((u64::from(major) << 8) | u64::from(minor))
}

#[cfg(test)]
mod tests {
use super::token_from_base_version;
use super::uefi_overload_image;

#[test]
fn overload_image_rejects_wrong_size() {
assert!(uefi_overload_image(0x1A00, vec![0; 0x1000]).is_err());
}

#[test]
fn base_version_to_token() {
assert_eq!(token_from_base_version("26.0.44"), Some(0x1A00));
assert_eq!(token_from_base_version("26.1"), Some(0x1A01));
assert_eq!(token_from_base_version("26"), None);
assert_eq!(token_from_base_version("v26.0.44"), None);
assert_eq!(token_from_base_version("256.0.0"), None);
}
}
18 changes: 18 additions & 0 deletions petri/src/vm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1725,6 +1725,20 @@ impl<T: PetriVmmBackend> PetriVmBuilder<T> {
self
}

/// Serve `firmware` for OpenHCL's `LoadFirmware` overload requests, under
/// the token from its version record and OpenHCL 1.8's legacy token.
pub fn with_uefi_overload_firmware(
mut self,
firmware: ResolvedArtifact<impl IsLoadable>,
) -> Self {
self.config
.firmware
.openhcl_config_mut()
.expect("UEFI overload firmware is only supported for OpenHCL firmware.")
.uefi_overload_firmware = Some(firmware.erase());
self
}

/// Enable confidential filtering, even if the VM is not confidential.
pub fn with_confidential_filtering(self) -> Self {
if !self.config.firmware.is_openhcl() {
Expand Down Expand Up @@ -3134,6 +3148,9 @@ pub struct OpenHclConfig {
pub vtl2_base_address_type: Option<Vtl2BaseAddressType>,
/// VTL2 settings
pub vtl2_settings: Option<Vtl2Settings>,
/// UEFI image the host serves for `LoadFirmware` overload requests
/// (OpenVMM only).
pub uefi_overload_firmware: Option<ResolvedArtifact>,
}

impl OpenHclConfig {
Expand Down Expand Up @@ -3189,6 +3206,7 @@ impl Default for OpenHclConfig {
log_levels: OpenvmmLogConfig::TestDefault,
vtl2_base_address_type: None,
vtl2_settings: None,
uefi_overload_firmware: None,
}
}
}
Expand Down
16 changes: 14 additions & 2 deletions petri/src/vm/openvmm/construct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1081,7 +1081,8 @@ impl PetriVmConfigSetupCore<'_> {
custom_command_line: _,
log_levels: _,
vtl2_base_address_type,
vtl2_settings: _, // run_core
vtl2_settings: _, // run_core
uefi_overload_firmware: _, // config_openhcl_vmbus_devices
} = openhcl_config;

let mut cmdline = Some(openhcl_config.command_line());
Expand Down Expand Up @@ -1224,7 +1225,11 @@ impl PetriVmConfigSetupCore<'_> {
efi_diagnostics_log_level,
efi_diagnostics_rate_limit: _, // applied device-side via UefiManifest::new
},
OpenHclConfig { vmbus_redirect, .. },
OpenHclConfig {
vmbus_redirect,
uefi_overload_firmware,
..
},
) = match self.firmware {
Firmware::OpenhclUefi {
uefi_config,
Expand Down Expand Up @@ -1292,6 +1297,13 @@ impl PetriVmConfigSetupCore<'_> {
},
force_dma_bounce_enabled: *force_dma_bounce,
smbios: Default::default(),
uefi_overload_images: match uefi_overload_firmware {
Some(firmware) => {
openvmm_helpers::uefi_overload::uefi_overload_images_from_file(firmware.get())
.context("failed to read the uefi overload firmware")?
}
None => Vec::new(),
},
};

Ok((ged, guest_request_send))
Expand Down
7 changes: 4 additions & 3 deletions vm/devices/get/get_protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1967,9 +1967,10 @@ pub struct LoadFirmwareResponse {
pub message_header: HeaderHostResponse,
pub status: LoadFirmwareStatus,
/// Offset from the firmware image base to the firmware entry point (SEC
/// entry on x64), computed by the host. Valid only when `status` is
/// [`LoadFirmwareStatus::SUCCESS`]; the guest/paravisor programs VTL0's RIP
/// to `image_base + entry_point_image_offset`.
/// entry on x64, `0x1000` on aarch64), computed by the host. Valid only
/// when `status` is [`LoadFirmwareStatus::SUCCESS`]; the guest/paravisor
/// programs VTL0's entry point to `image_base + entry_point_image_offset`.
/// Zero leaves the entry point unchanged.
Comment on lines +1970 to +1973
pub entry_point_image_offset: u64,
}

Expand Down
16 changes: 16 additions & 0 deletions vm/devices/get/get_resources/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,22 @@ pub mod ged {
pub enable_hibernation: bool,
/// SMBIOS identity overrides delivered to the guest firmware.
pub smbios: smbios_defs::SmbiosConfig,
/// VTL0 UEFI images the host can load in response to `LoadFirmware`.
pub uefi_overload_images: Vec<UefiOverloadImage>,
}

/// A VTL0 UEFI image the GED writes into guest memory in response to a
/// `LoadFirmware` request.
#[derive(MeshPayload)]
pub struct UefiOverloadImage {
/// The `LoadFirmware` token that selects this image.
pub token: u64,
/// The guest physical address to write the image at.
pub base_gpa: u64,
/// The entry point's offset from `base_gpa`.
pub entry_offset: u64,
/// The image bytes.
pub image: Vec<u8>,
}

/// An IPMI SEL notification received from OpenHCL.
Expand Down
71 changes: 63 additions & 8 deletions vm/devices/get/guest_emulation_device/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,38 @@ pub struct GuestConfig {
/// SMBIOS identity overrides.
#[inspect(skip)]
pub smbios: smbios_defs::SmbiosConfig,
/// VTL0 UEFI images the host can load in response to `LoadFirmware`.
#[inspect(iter_by_index)]
pub uefi_overload_images: Vec<UefiOverloadImage>,
}

/// A VTL0 UEFI image the host writes into guest memory in response to a
/// `LoadFirmware` request.
#[derive(Clone, Inspect)]
pub struct UefiOverloadImage {
/// The `LoadFirmware` token that selects this image.
#[inspect(hex)]
pub token: u64,
/// The guest physical address to write the image at.
#[inspect(hex)]
pub base_gpa: u64,
/// The entry point's offset from `base_gpa`.
#[inspect(hex)]
pub entry_offset: u64,
/// The image bytes.
#[inspect(with = "|x| x.len()")]
pub image: std::sync::Arc<[u8]>,
}

impl std::fmt::Debug for UefiOverloadImage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UefiOverloadImage")
.field("token", &format_args!("{:#x}", self.token))
.field("base_gpa", &format_args!("{:#x}", self.base_gpa))
.field("entry_offset", &format_args!("{:#x}", self.entry_offset))
.field("image_len", &self.image.len())
.finish()
}
}

#[derive(Debug, Clone, Inspect)]
Expand Down Expand Up @@ -666,7 +698,7 @@ impl<T: RingMem + Unpin> GedChannel<T> {
HostRequests::UNMAP_FRAMEBUFFER => self.handle_unmap_framebuffer(state).await?,
HostRequests::CREATE_RAM_GPA_RANGE => self.handle_create_ram_gpa_range(message_buf)?,
HostRequests::RESET_RAM_GPA_RANGE => self.handle_reset_ram_gpa_range(message_buf)?,
HostRequests::LOAD_FIRMWARE => self.handle_load_firmware(message_buf)?,
HostRequests::LOAD_FIRMWARE => self.handle_load_firmware(message_buf, state)?,
_ => {
tracing::error!(message_id = ?header.message_id(), "unexpected message");
return Err(Error::InvalidSequence);
Expand Down Expand Up @@ -1086,20 +1118,43 @@ impl<T: RingMem + Unpin> GedChannel<T> {
Ok(())
}

fn handle_load_firmware(&mut self, message_buf: &[u8]) -> Result<(), Error> {
fn handle_load_firmware(
&mut self,
message_buf: &[u8],
state: &GuestEmulationDevice,
) -> Result<(), Error> {
let request = get_protocol::LoadFirmwareRequest::read_from_prefix(message_buf)
.map_err(|_| Error::MessageTooSmall)?
.0; // TODO: zerocopy: map_err (https://github.com/microsoft/openvmm/issues/759)

let firmware_token = request.firmware_token;
tracing::info!(firmware_token, "load firmware request");

// This emulated host does not have a firmware image to write into guest
// RAM, so the request is simply acknowledged with a zero entry-point
// offset (leave VTL0's RIP unchanged). A real host loads the image
// identified by the token and returns the computed SEC entry offset.
let response =
get_protocol::LoadFirmwareResponse::new(get_protocol::LoadFirmwareStatus::SUCCESS, 0);
// Write the image the token selects over VTL0's firmware and return
// its entry point offset.
let image = state
.config
.uefi_overload_images
.iter()
.find(|image| image.token == firmware_token);
let (status, entry_offset) = match image {
None => (get_protocol::LoadFirmwareStatus::INVALID_TOKEN, 0),
Some(image) => match self.gm.write_at(image.base_gpa, &image.image) {
Ok(()) => (
get_protocol::LoadFirmwareStatus::SUCCESS,
image.entry_offset,
),
Err(err) => {
tracelimit::error_ratelimited!(
error = &err as &dyn std::error::Error,
firmware_token,
"failed to write the firmware image into guest memory"
);
(get_protocol::LoadFirmwareStatus::FAILED, 0)
}
},
};
let response = get_protocol::LoadFirmwareResponse::new(status, entry_offset);
self.channel
.try_send(response.as_bytes())
.map_err(Error::Vmbus)?;
Expand Down
10 changes: 10 additions & 0 deletions vm/devices/get/guest_emulation_device/src/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,16 @@ impl AsyncResolveResource<VmbusDeviceHandleKind, GuestEmulationDeviceHandle>
},
force_dma_bounce_enabled: resource.force_dma_bounce_enabled,
smbios: resource.smbios,
uefi_overload_images: resource
.uefi_overload_images
.into_iter()
.map(|image| crate::UefiOverloadImage {
token: image.token,
base_gpa: image.base_gpa,
entry_offset: image.entry_offset,
image: image.image.into(),
})
.collect(),
},
halt,
resource.firmware_event_send,
Expand Down
15 changes: 15 additions & 0 deletions vm/devices/get/guest_emulation_device/src/test_utilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,15 @@ impl<T: RingMem + Unpin> TestGedChannel<T> {
}
}

/// The `LoadFirmware` token the test GED accepts.
pub const TEST_UEFI_FIRMWARE_TOKEN: u64 = 0x1A00;
/// Where the test GED writes the image for [`TEST_UEFI_FIRMWARE_TOKEN`].
pub const TEST_UEFI_FIRMWARE_GPA: u64 = 0x1000;
/// The entry point offset the test GED returns for [`TEST_UEFI_FIRMWARE_TOKEN`].
pub const TEST_UEFI_FIRMWARE_ENTRY_OFFSET: u64 = 0x10;
/// The image the test GED writes for [`TEST_UEFI_FIRMWARE_TOKEN`].
pub const TEST_UEFI_FIRMWARE_IMAGE: &[u8] = b"test uefi image";

/// Create the host Guest Emulation Device and corresponding I/O.
///
/// If `ged_responses` is Some(), then TestGedChannel will be used to
Expand Down Expand Up @@ -277,6 +286,12 @@ pub fn create_host_channel(
efi_diagnostics_log_level: Default::default(),
force_dma_bounce_enabled: false,
smbios: Default::default(),
uefi_overload_images: vec![crate::UefiOverloadImage {
token: TEST_UEFI_FIRMWARE_TOKEN,
base_gpa: TEST_UEFI_FIRMWARE_GPA,
entry_offset: TEST_UEFI_FIRMWARE_ENTRY_OFFSET,
image: TEST_UEFI_FIRMWARE_IMAGE.into(),
}],
};

let halt_reason = Arc::new(Mutex::new(None));
Expand Down
Loading
Loading