xref: /cloud-hypervisor/vmm/src/igvm/igvm_loader.rs (revision 88a9f799449c04180c6b9a21d3b9c0c4b57e2bd6)
1 // SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
2 //
3 // Copyright © 2023, Microsoft Corporation
4 //
5 use std::collections::HashMap;
6 use std::ffi::CString;
7 use std::io::Read;
8 use std::io::Seek;
9 use std::io::SeekFrom;
10 use std::mem::size_of;
11 use std::sync::{Arc, Mutex};
12 
13 use igvm::{snp_defs::SevVmsa, IgvmDirectiveHeader, IgvmFile, IgvmPlatformHeader, IsolationType};
14 use igvm_defs::{
15     IgvmPageDataType, IgvmPlatformType, IGVM_VHS_PARAMETER, IGVM_VHS_PARAMETER_INSERT,
16 };
17 #[cfg(feature = "sev_snp")]
18 use igvm_defs::{MemoryMapEntryType, IGVM_VHS_MEMORY_MAP_ENTRY};
19 use mshv_bindings::*;
20 use thiserror::Error;
21 use zerocopy::AsBytes;
22 
23 use crate::cpu::CpuManager;
24 use crate::igvm::{
25     loader::Loader, BootPageAcceptance, IgvmLoadedInfo, StartupMemoryType, HV_PAGE_SIZE,
26 };
27 use crate::memory_manager::MemoryManager;
28 #[cfg(feature = "sev_snp")]
29 use crate::GuestMemoryMmap;
30 
31 #[derive(Debug, Error)]
32 pub enum Error {
33     #[error("command line is not a valid C string")]
34     InvalidCommandLine(#[source] std::ffi::NulError),
35     #[error("failed to read igvm file")]
36     Igvm(#[source] std::io::Error),
37     #[error("invalid igvm file")]
38     InvalidIgvmFile(#[source] igvm::Error),
39     #[error("invalid guest memory map")]
40     InvalidGuestMemmap(#[source] arch::Error),
41     #[error("loader error")]
42     Loader(#[source] crate::igvm::loader::Error),
43     #[error("parameter too large for parameter area")]
44     ParameterTooLarge,
45     #[error("Error importing isolated pages: {0}")]
46     ImportIsolatedPages(#[source] hypervisor::HypervisorVmError),
47     #[error("Error completing importing isolated pages: {0}")]
48     CompleteIsolatedImport(#[source] hypervisor::HypervisorVmError),
49     #[error("Error decoding host data: {0}")]
50     FailedToDecodeHostData(#[source] hex::FromHexError),
51 }
52 
53 #[allow(dead_code)]
54 #[derive(Copy, Clone)]
55 struct GpaPages {
56     pub gpa: u64,
57     pub page_type: u32,
58     pub page_size: u32,
59 }
60 
61 #[derive(Debug)]
62 enum ParameterAreaState {
63     /// Parameter area has been declared via a ParameterArea header.
64     Allocated { data: Vec<u8>, max_size: u64 },
65     /// Parameter area inserted and invalid to use.
66     Inserted,
67 }
68 
69 #[cfg(feature = "sev_snp")]
70 fn igvm_memmap_from_ram_range(ram_range: (u64, u64)) -> IGVM_VHS_MEMORY_MAP_ENTRY {
71     assert!(ram_range.0 % HV_PAGE_SIZE == 0);
72     assert!((ram_range.1 - ram_range.0) % HV_PAGE_SIZE == 0);
73 
74     IGVM_VHS_MEMORY_MAP_ENTRY {
75         starting_gpa_page_number: ram_range.0 / HV_PAGE_SIZE,
76         number_of_pages: (ram_range.1 - ram_range.0) / HV_PAGE_SIZE,
77         entry_type: MemoryMapEntryType::MEMORY,
78         flags: 0,
79         reserved: 0,
80     }
81 }
82 
83 #[cfg(feature = "sev_snp")]
84 fn generate_memory_map(
85     guest_mem: &GuestMemoryMmap,
86 ) -> Result<Vec<IGVM_VHS_MEMORY_MAP_ENTRY>, Error> {
87     let mut memory_map = Vec::new();
88 
89     // Get usable physical memory ranges
90     let ram_ranges = arch::generate_ram_ranges(guest_mem).map_err(Error::InvalidGuestMemmap)?;
91 
92     for ram_range in ram_ranges {
93         memory_map.push(igvm_memmap_from_ram_range(ram_range));
94     }
95 
96     Ok(memory_map)
97 }
98 
99 // Import a parameter to the given parameter area.
100 fn import_parameter(
101     parameter_areas: &mut HashMap<u32, ParameterAreaState>,
102     info: &IGVM_VHS_PARAMETER,
103     parameter: &[u8],
104 ) -> Result<(), Error> {
105     let (parameter_area, max_size) = match parameter_areas
106         .get_mut(&info.parameter_area_index)
107         .expect("parameter area should be present")
108     {
109         ParameterAreaState::Allocated { data, max_size } => (data, max_size),
110         ParameterAreaState::Inserted => panic!("igvmfile is not valid"),
111     };
112     let offset = info.byte_offset as usize;
113     let end_of_parameter = offset + parameter.len();
114 
115     if end_of_parameter > *max_size as usize {
116         // TODO: tracing for which parameter was too big?
117         return Err(Error::ParameterTooLarge);
118     }
119 
120     if parameter_area.len() < end_of_parameter {
121         parameter_area.resize(end_of_parameter, 0);
122     }
123 
124     parameter_area[offset..end_of_parameter].copy_from_slice(parameter);
125     Ok(())
126 }
127 
128 ///
129 /// Load the given IGVM file to guest memory.
130 /// Right now it only supports SNP based isolation.
131 /// We can boot legacy VM with an igvm file without
132 /// any isolation.
133 ///
134 pub fn load_igvm(
135     mut file: &std::fs::File,
136     memory_manager: Arc<Mutex<MemoryManager>>,
137     cpu_manager: Arc<Mutex<CpuManager>>,
138     cmdline: &str,
139     #[cfg(feature = "sev_snp")] host_data: &Option<String>,
140 ) -> Result<Box<IgvmLoadedInfo>, Error> {
141     let mut loaded_info: Box<IgvmLoadedInfo> = Box::default();
142     let command_line = CString::new(cmdline).map_err(Error::InvalidCommandLine)?;
143     let mut file_contents = Vec::new();
144     let memory = memory_manager.lock().as_ref().unwrap().guest_memory();
145     let mut gpas: Vec<GpaPages> = Vec::new();
146     let proc_count = cpu_manager.lock().unwrap().vcpus().len() as u32;
147 
148     #[cfg(feature = "sev_snp")]
149     let mut host_data_contents = [0; 32];
150     #[cfg(feature = "sev_snp")]
151     if let Some(host_data_str) = host_data {
152         hex::decode_to_slice(host_data_str, &mut host_data_contents as &mut [u8])
153             .map_err(Error::FailedToDecodeHostData)?;
154     }
155 
156     file.seek(SeekFrom::Start(0)).map_err(Error::Igvm)?;
157     file.read_to_end(&mut file_contents).map_err(Error::Igvm)?;
158 
159     let igvm_file = IgvmFile::new_from_binary(&file_contents, Some(IsolationType::Snp))
160         .map_err(Error::InvalidIgvmFile)?;
161 
162     let mask = match &igvm_file.platforms()[0] {
163         IgvmPlatformHeader::SupportedPlatform(info) => {
164             debug_assert!(info.platform_type == IgvmPlatformType::SEV_SNP);
165             info.compatibility_mask
166         }
167     };
168 
169     let mut loader = Loader::new(memory);
170 
171     let mut parameter_areas: HashMap<u32, ParameterAreaState> = HashMap::new();
172 
173     for header in igvm_file.directives() {
174         debug_assert!(header.compatibility_mask().unwrap_or(mask) & mask == mask);
175 
176         match header {
177             IgvmDirectiveHeader::PageData {
178                 gpa,
179                 compatibility_mask: _,
180                 flags,
181                 data_type,
182                 data,
183             } => {
184                 debug_assert!(data.len() as u64 % HV_PAGE_SIZE == 0);
185 
186                 // TODO: only 4k or empty page data supported right now
187                 assert!(data.len() as u64 == HV_PAGE_SIZE || data.is_empty());
188 
189                 let acceptance = match *data_type {
190                     IgvmPageDataType::NORMAL => {
191                         if flags.unmeasured() {
192                             gpas.push(GpaPages {
193                                 gpa: *gpa,
194                                 page_type: hv_isolated_page_type_HV_ISOLATED_PAGE_TYPE_UNMEASURED,
195                                 page_size: hv_isolated_page_size_HV_ISOLATED_PAGE_SIZE_4KB,
196                             });
197                             BootPageAcceptance::ExclusiveUnmeasured
198                         } else {
199                             gpas.push(GpaPages {
200                                 gpa: *gpa,
201                                 page_type: hv_isolated_page_type_HV_ISOLATED_PAGE_TYPE_NORMAL,
202                                 page_size: hv_isolated_page_size_HV_ISOLATED_PAGE_SIZE_4KB,
203                             });
204                             BootPageAcceptance::Exclusive
205                         }
206                     }
207                     IgvmPageDataType::SECRETS => {
208                         gpas.push(GpaPages {
209                             gpa: *gpa,
210                             page_type: hv_isolated_page_type_HV_ISOLATED_PAGE_TYPE_SECRETS,
211                             page_size: hv_isolated_page_size_HV_ISOLATED_PAGE_SIZE_4KB,
212                         });
213                         BootPageAcceptance::SecretsPage
214                     }
215                     IgvmPageDataType::CPUID_DATA => {
216                         // SAFETY: CPUID is readonly
217                         unsafe {
218                             let cpuid_page_p: *mut hv_psp_cpuid_page =
219                                 data.as_ptr() as *mut hv_psp_cpuid_page; // as *mut hv_psp_cpuid_page;
220                             let cpuid_page: &mut hv_psp_cpuid_page = &mut *cpuid_page_p;
221                             for i in 0..cpuid_page.count {
222                                 let leaf = cpuid_page.cpuid_leaf_info[i as usize];
223                                 let mut in_leaf = cpu_manager
224                                     .lock()
225                                     .unwrap()
226                                     .get_cpuid_leaf(
227                                         0,
228                                         leaf.eax_in,
229                                         leaf.ecx_in,
230                                         leaf.xfem_in,
231                                         leaf.xss_in,
232                                     )
233                                     .unwrap();
234                                 if leaf.eax_in == 1 {
235                                     in_leaf[2] &= 0x7FFFFFFF;
236                                 }
237                                 cpuid_page.cpuid_leaf_info[i as usize].eax_out = in_leaf[0];
238                                 cpuid_page.cpuid_leaf_info[i as usize].ebx_out = in_leaf[1];
239                                 cpuid_page.cpuid_leaf_info[i as usize].ecx_out = in_leaf[2];
240                                 cpuid_page.cpuid_leaf_info[i as usize].edx_out = in_leaf[3];
241                             }
242                         }
243                         gpas.push(GpaPages {
244                             gpa: *gpa,
245                             page_type: hv_isolated_page_type_HV_ISOLATED_PAGE_TYPE_CPUID,
246                             page_size: hv_isolated_page_size_HV_ISOLATED_PAGE_SIZE_4KB,
247                         });
248                         BootPageAcceptance::CpuidPage
249                     }
250                     // TODO: other data types SNP / TDX only, unsupported
251                     _ => todo!("unsupported IgvmPageDataType"),
252                 };
253 
254                 loader
255                     .import_pages(gpa / HV_PAGE_SIZE, 1, acceptance, data)
256                     .map_err(Error::Loader)?;
257             }
258             IgvmDirectiveHeader::ParameterArea {
259                 number_of_bytes,
260                 parameter_area_index,
261                 initial_data,
262             } => {
263                 debug_assert!(number_of_bytes % HV_PAGE_SIZE == 0);
264                 debug_assert!(
265                     initial_data.is_empty() || initial_data.len() as u64 == *number_of_bytes
266                 );
267 
268                 // Allocate a new parameter area. It must not be already used.
269                 if parameter_areas
270                     .insert(
271                         *parameter_area_index,
272                         ParameterAreaState::Allocated {
273                             data: initial_data.clone(),
274                             max_size: *number_of_bytes,
275                         },
276                     )
277                     .is_some()
278                 {
279                     panic!("IgvmFile is not valid, invalid invariant");
280                 }
281             }
282             IgvmDirectiveHeader::VpCount(info) => {
283                 import_parameter(&mut parameter_areas, info, proc_count.as_bytes())?;
284             }
285             IgvmDirectiveHeader::MmioRanges(_info) => {
286                 todo!("unsupported IgvmPageDataType");
287             }
288             IgvmDirectiveHeader::MemoryMap(_info) => {
289                 #[cfg(feature = "sev_snp")]
290                 {
291                     let guest_mem = memory_manager.lock().unwrap().boot_guest_memory();
292                     let memory_map = generate_memory_map(&guest_mem)?;
293                     import_parameter(&mut parameter_areas, _info, memory_map.as_bytes())?;
294                 }
295 
296                 #[cfg(not(feature = "sev_snp"))]
297                 todo!("Not implemented");
298             }
299             IgvmDirectiveHeader::CommandLine(info) => {
300                 import_parameter(&mut parameter_areas, info, command_line.as_bytes_with_nul())?;
301             }
302             IgvmDirectiveHeader::RequiredMemory {
303                 gpa,
304                 compatibility_mask: _,
305                 number_of_bytes,
306                 vtl2_protectable: _,
307             } => {
308                 let memory_type = StartupMemoryType::Ram;
309                 loaded_info.gpas.push(*gpa);
310                 loader
311                     .verify_startup_memory_available(
312                         gpa / HV_PAGE_SIZE,
313                         *number_of_bytes as u64 / HV_PAGE_SIZE,
314                         memory_type,
315                     )
316                     .map_err(Error::Loader)?;
317             }
318             IgvmDirectiveHeader::SnpVpContext {
319                 gpa,
320                 compatibility_mask: _,
321                 vp_index,
322                 vmsa,
323             } => {
324                 assert_eq!(gpa % HV_PAGE_SIZE, 0);
325                 let mut data: [u8; 4096] = [0; 4096];
326                 let len = size_of::<SevVmsa>();
327                 loaded_info.vmsa_gpa = *gpa;
328                 loaded_info.vmsa = **vmsa;
329                 // Only supported for index zero
330                 if *vp_index == 0 {
331                     data[..len].copy_from_slice(vmsa.as_bytes());
332                     loader
333                         .import_pages(gpa / HV_PAGE_SIZE, 1, BootPageAcceptance::VpContext, &data)
334                         .map_err(Error::Loader)?;
335                 }
336 
337                 gpas.push(GpaPages {
338                     gpa: *gpa,
339                     page_type: hv_isolated_page_type_HV_ISOLATED_PAGE_TYPE_VMSA,
340                     page_size: hv_isolated_page_size_HV_ISOLATED_PAGE_SIZE_4KB,
341                 });
342             }
343             IgvmDirectiveHeader::SnpIdBlock {
344                 compatibility_mask,
345                 author_key_enabled,
346                 reserved,
347                 ld,
348                 family_id,
349                 image_id,
350                 version,
351                 guest_svn,
352                 id_key_algorithm,
353                 author_key_algorithm,
354                 id_key_signature,
355                 id_public_key,
356                 author_key_signature,
357                 author_public_key,
358             } => {
359                 loaded_info.snp_id_block.compatibility_mask = *compatibility_mask;
360                 loaded_info.snp_id_block.author_key_enabled = *author_key_enabled;
361                 loaded_info.snp_id_block.reserved = *reserved;
362                 loaded_info.snp_id_block.ld = *ld;
363                 loaded_info.snp_id_block.family_id = *family_id;
364                 loaded_info.snp_id_block.image_id = *image_id;
365                 loaded_info.snp_id_block.version = *version;
366                 loaded_info.snp_id_block.guest_svn = *guest_svn;
367                 loaded_info.snp_id_block.id_key_algorithm = *id_key_algorithm;
368                 loaded_info.snp_id_block.author_key_algorithm = *author_key_algorithm;
369                 loaded_info.snp_id_block.id_key_signature = **id_key_signature;
370                 loaded_info.snp_id_block.id_public_key = **id_public_key;
371                 loaded_info.snp_id_block.author_key_signature = **author_key_signature;
372                 loaded_info.snp_id_block.author_public_key = **author_public_key;
373             }
374             IgvmDirectiveHeader::X64VbsVpContext {
375                 vtl: _,
376                 registers: _,
377                 compatibility_mask: _,
378             } => {
379                 todo!("VbsVpContext not supported");
380             }
381             IgvmDirectiveHeader::VbsMeasurement { .. } => {
382                 todo!("VbsMeasurement not supported")
383             }
384             IgvmDirectiveHeader::ParameterInsert(IGVM_VHS_PARAMETER_INSERT {
385                 gpa,
386                 compatibility_mask: _,
387                 parameter_area_index,
388             }) => {
389                 debug_assert!(gpa % HV_PAGE_SIZE == 0);
390 
391                 let area = parameter_areas
392                     .get_mut(parameter_area_index)
393                     .expect("igvmfile should be valid");
394                 match area {
395                     ParameterAreaState::Allocated { data, max_size } => loader
396                         .import_pages(
397                             gpa / HV_PAGE_SIZE,
398                             *max_size / HV_PAGE_SIZE,
399                             BootPageAcceptance::ExclusiveUnmeasured,
400                             data,
401                         )
402                         .map_err(Error::Loader)?,
403                     ParameterAreaState::Inserted => panic!("igvmfile is invalid, multiple insert"),
404                 }
405                 *area = ParameterAreaState::Inserted;
406                 gpas.push(GpaPages {
407                     gpa: *gpa,
408                     page_type: hv_isolated_page_type_HV_ISOLATED_PAGE_TYPE_UNMEASURED,
409                     page_size: hv_isolated_page_size_HV_ISOLATED_PAGE_SIZE_4KB,
410                 });
411             }
412             IgvmDirectiveHeader::ErrorRange { .. } => {
413                 todo!("Error Range not supported")
414             }
415             _ => {
416                 todo!("Header not supported!!")
417             }
418         }
419     }
420 
421     #[cfg(feature = "sev_snp")]
422     {
423         use std::time::Instant;
424 
425         let mut now = Instant::now();
426 
427         // Sort the gpas to group them by the page type
428         gpas.sort_by(|a, b| a.gpa.cmp(&b.gpa));
429 
430         let gpas_grouped = gpas
431             .iter()
432             .fold(Vec::<Vec<GpaPages>>::new(), |mut acc, gpa| {
433                 if let Some(last_vec) = acc.last_mut() {
434                     if last_vec[0].page_type == gpa.page_type {
435                         last_vec.push(*gpa);
436                         return acc;
437                     }
438                 }
439                 acc.push(vec![*gpa]);
440                 acc
441             });
442 
443         // Import the pages as a group(by page type) of PFNs to reduce the
444         // hypercall.
445         for group in gpas_grouped.iter() {
446             info!(
447                 "Importing {} page{}",
448                 group.len(),
449                 if group.len() > 1 { "s" } else { "" }
450             );
451             // Convert the gpa into PFN as MSHV hypercall takes an array
452             // of PFN for importing the isolated pages
453             let pfns: Vec<u64> = group
454                 .iter()
455                 .map(|gpa| gpa.gpa >> HV_HYP_PAGE_SHIFT)
456                 .collect();
457             memory_manager
458                 .lock()
459                 .unwrap()
460                 .vm
461                 .import_isolated_pages(
462                     group[0].page_type,
463                     hv_isolated_page_size_HV_ISOLATED_PAGE_SIZE_4KB,
464                     &pfns,
465                 )
466                 .map_err(Error::ImportIsolatedPages)?;
467         }
468 
469         info!(
470             "Time it took to for hashing pages {:.2?} and page_count {:?}",
471             now.elapsed(),
472             gpas.len()
473         );
474 
475         now = Instant::now();
476         // Call Complete Isolated Import since we are done importing isolated pages
477         memory_manager
478             .lock()
479             .unwrap()
480             .vm
481             .complete_isolated_import(loaded_info.snp_id_block, host_data_contents, 1)
482             .map_err(Error::CompleteIsolatedImport)?;
483 
484         info!(
485             "Time it took to for launch complete command  {:.2?}",
486             now.elapsed()
487         );
488     }
489 
490     debug!("Dumping the contents of VMSA page: {:x?}", loaded_info.vmsa);
491     Ok(loaded_info)
492 }
493