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