igvmfilegen_config/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Configuration for generating IGVM files. These are deserialized from a JSON
5//! manifest file used by the file builder.
6
7#![expect(missing_docs)]
8#![forbid(unsafe_code)]
9
10use serde::Deserialize;
11use serde::Serialize;
12use std::collections::HashMap;
13use std::ffi::CString;
14use std::path::PathBuf;
15
16/// The UEFI config type to pass to the UEFI loader.
17#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
18#[serde(rename_all = "snake_case")]
19pub enum UefiConfigType {
20    /// No UEFI config set at load time.
21    None,
22    /// UEFI config is specified via IGVM parameters.
23    Igvm,
24}
25
26/// The interrupt injection type that should be used for VMPL0 on SNP.
27#[derive(Serialize, Deserialize, Debug)]
28#[serde(rename_all = "snake_case")]
29pub enum SnpInjectionType {
30    /// Normal injection.
31    Normal,
32    /// Restricted injection.
33    Restricted,
34}
35
36/// The isolation type that should be used for the loader.
37#[derive(Serialize, Deserialize, Debug)]
38#[serde(rename_all = "snake_case")]
39pub enum ConfigIsolationType {
40    /// No isolation is present.
41    None,
42    /// Hypervisor based isolation (VBS) is present.
43    Vbs {
44        /// Boolean representing if the guest allows debugging
45        enable_debug: bool,
46    },
47    /// AMD SEV-SNP.
48    Snp {
49        /// The optional shared GPA boundary to configure for the guest. A
50        /// `None` value represents a guest that no shared GPA boundary is to be
51        /// configured.
52        shared_gpa_boundary_bits: Option<u8>,
53        /// The SEV-SNP policy for the guest.
54        policy: u64,
55        /// Boolean representing if the guest allows debugging
56        enable_debug: bool,
57        /// The interrupt injection type to use for the highest vmpl.
58        injection_type: SnpInjectionType,
59    },
60    /// Intel TDX.
61    Tdx {
62        /// Boolean representing if the guest allows debugging
63        enable_debug: bool,
64        /// Boolean representing if the guest is disallowed from handling
65        /// virtualization exceptions
66        sept_ve_disable: bool,
67    },
68}
69
70/// Configuration on what to load.
71#[derive(Serialize, Deserialize, Debug)]
72#[serde(rename_all = "snake_case")]
73pub enum Image {
74    /// Load nothing.
75    None,
76    /// Load UEFI.
77    Uefi { config_type: UefiConfigType },
78    /// Load the OpenHCL paravisor.
79    Openhcl {
80        /// The paravisor kernel command line.
81        #[serde(default)]
82        command_line: String,
83        /// If false, the host may provide additional kernel command line
84        /// parameters at runtime.
85        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
86        static_command_line: bool,
87        /// The base page number for paravisor memory. None means relocation is used.
88        #[serde(skip_serializing_if = "Option::is_none")]
89        memory_page_base: Option<u64>,
90        /// The number of pages for paravisor memory.
91        memory_page_count: u64,
92        /// Include the UEFI firmware for loading into the guest.
93        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
94        uefi: bool,
95        /// Include the Linux kernel for loading into the guest.
96        #[serde(skip_serializing_if = "Option::is_none")]
97        linux: Option<LinuxImage>,
98    },
99    /// Load the Linux kernel.
100    /// TODO: Currently, this only works with underhill.
101    Linux(LinuxImage),
102}
103
104#[derive(Serialize, Deserialize, Debug)]
105#[serde(rename_all = "snake_case")]
106pub struct LinuxImage {
107    /// Load with an initrd.
108    pub use_initrd: bool,
109    /// The command line to boot the kernel with.
110    pub command_line: CString,
111}
112
113impl Image {
114    /// Get the required resources for this image config.
115    pub fn required_resources(&self) -> Vec<ResourceType> {
116        match *self {
117            Image::None => vec![],
118            Image::Uefi { .. } => vec![ResourceType::Uefi],
119            Image::Openhcl {
120                uefi, ref linux, ..
121            } => [
122                ResourceType::UnderhillKernel,
123                ResourceType::OpenhclBoot,
124                ResourceType::UnderhillInitrd,
125            ]
126            .into_iter()
127            .chain(if uefi { Some(ResourceType::Uefi) } else { None })
128            .chain(linux.iter().flat_map(|linux| linux.required_resources()))
129            .collect(),
130            Image::Linux(ref linux) => linux.required_resources(),
131        }
132    }
133}
134
135impl LinuxImage {
136    fn required_resources(&self) -> Vec<ResourceType> {
137        [ResourceType::LinuxKernel]
138            .into_iter()
139            .chain(if self.use_initrd {
140                Some(ResourceType::LinuxInitrd)
141            } else {
142                None
143            })
144            .collect()
145    }
146}
147
148/// The config used to describe an initial guest context to be generated by the
149/// tool.
150#[derive(Serialize, Deserialize, Debug)]
151pub struct GuestConfig {
152    /// The SVN of this guest.
153    pub guest_svn: u32,
154    /// The maximum VTL to be enabled for the guest.
155    pub max_vtl: u8,
156    /// The isolation type to be used for the guest.
157    pub isolation_type: ConfigIsolationType,
158    /// The image to load into the guest.
159    pub image: Image,
160}
161
162/// The architecture of the igvm file.
163#[derive(Serialize, Deserialize, Debug)]
164#[serde(rename_all = "snake_case")]
165pub enum GuestArch {
166    /// x64
167    X64,
168    /// AArch64 aka ARM64
169    Aarch64,
170}
171
172/// The config used to describe a multi-architecture IGVM file containing
173/// multiple guests.
174#[derive(Serialize, Deserialize, Debug)]
175#[serde(rename_all = "snake_case")]
176pub struct Config {
177    /// The architecture of the igvm file.
178    pub guest_arch: GuestArch,
179    /// The array of guest configs to be used to generate a single IGVM file.
180    pub guest_configs: Vec<GuestConfig>,
181}
182
183impl Config {
184    /// Get a vec representing the required resources for this config.
185    pub fn required_resources(&self) -> Vec<ResourceType> {
186        let mut resources = vec![];
187        for guest_config in &self.guest_configs {
188            resources.extend(guest_config.image.required_resources());
189        }
190        resources
191    }
192}
193
194#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
195#[serde(rename_all = "snake_case")]
196pub enum ResourceType {
197    Uefi,
198    UnderhillKernel,
199    OpenhclBoot,
200    UnderhillInitrd,
201    UnderhillSidecar,
202    LinuxKernel,
203    LinuxInitrd,
204}
205
206/// Resources used by igvmfilegen to generate IGVM files. These are generated by
207/// build tooling and not checked into the repo.
208#[derive(Serialize, Deserialize, Debug)]
209#[serde(rename_all = "snake_case")]
210pub struct Resources {
211    /// The set of resources to use to generate IGVM files. These paths must be
212    /// absolute.
213    #[serde(deserialize_with = "parse::resources")]
214    resources: HashMap<ResourceType, PathBuf>,
215}
216
217mod parse {
218    use super::*;
219    use serde::Deserialize;
220    use serde::Deserializer;
221    use std::collections::HashMap;
222
223    pub fn resources<'de, D: Deserializer<'de>>(
224        d: D,
225    ) -> Result<HashMap<ResourceType, PathBuf>, D::Error> {
226        let resources: HashMap<ResourceType, PathBuf> = Deserialize::deserialize(d)?;
227
228        for (resource, path) in &resources {
229            if !path.is_absolute() {
230                return Err(serde::de::Error::custom(AbsolutePathError(
231                    *resource,
232                    path.clone(),
233                )));
234            }
235        }
236
237        Ok(resources)
238    }
239}
240
241/// Error returned when required resources are missing.
242#[derive(Debug)]
243pub struct MissingResourcesError(pub Vec<ResourceType>);
244
245impl std::fmt::Display for MissingResourcesError {
246    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247        write!(f, "missing resources: {:?}", self.0)
248    }
249}
250
251impl std::error::Error for MissingResourcesError {}
252
253/// Error returned when a resource is not an absolute path.
254#[derive(Debug)]
255pub struct AbsolutePathError(ResourceType, PathBuf);
256
257impl std::fmt::Display for AbsolutePathError {
258    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259        write!(
260            f,
261            "resource {:?} path is not absolute: {:?}",
262            self.0, self.1
263        )
264    }
265}
266
267impl std::error::Error for AbsolutePathError {}
268
269impl Resources {
270    /// Create a new set of resources. Returns an error if any of the paths are
271    /// not absolute.
272    pub fn new(resources: HashMap<ResourceType, PathBuf>) -> Result<Self, AbsolutePathError> {
273        for (resource, path) in &resources {
274            if !path.is_absolute() {
275                return Err(AbsolutePathError(*resource, path.clone()));
276            }
277        }
278
279        Ok(Resources { resources })
280    }
281
282    /// Get the resources for this set.
283    pub fn resources(&self) -> &HashMap<ResourceType, PathBuf> {
284        &self.resources
285    }
286
287    /// Get the resource path for a given resource type.
288    pub fn get(&self, resource: ResourceType) -> Option<&PathBuf> {
289        self.resources.get(&resource)
290    }
291
292    /// Check that the required resources are present. On error, returns which
293    /// resources are missing.
294    pub fn check_required(&self, required: &[ResourceType]) -> Result<(), MissingResourcesError> {
295        let mut missing = vec![];
296        for resource in required {
297            if !self.resources.contains_key(resource) {
298                missing.push(*resource);
299            }
300        }
301
302        if missing.is_empty() {
303            Ok(())
304        } else {
305            Err(MissingResourcesError(missing))
306        }
307    }
308}
309
310#[cfg(test)]
311mod test {
312    use super::*;
313
314    #[test]
315    fn non_absolute_path_new() {
316        let mut resources = HashMap::new();
317        resources.insert(ResourceType::Uefi, PathBuf::from("./uefi"));
318        let result = Resources::new(resources);
319        assert!(result.is_err());
320    }
321
322    #[test]
323    fn parse_non_absolute_path() {
324        let resources = r#"{"uefi":"./uefi"}"#;
325        let result: Result<Resources, _> = serde_json::from_str(resources);
326        assert!(result.is_err());
327    }
328
329    #[test]
330    fn missing_resources() {
331        let resources = Resources {
332            resources: HashMap::new(),
333        };
334        let required = vec![ResourceType::Uefi];
335        let result = resources.check_required(&required);
336        assert!(result.is_err());
337    }
338}