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")]
igvm_memmap_from_ram_range(ram_range: (u64, u64)) -> IGVM_VHS_MEMORY_MAP_ENTRY68 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")]
generate_memory_map( guest_mem: &GuestMemoryMmap, ) -> Result<Vec<IGVM_VHS_MEMORY_MAP_ENTRY>, Error>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.
import_parameter( parameter_areas: &mut HashMap<u32, ParameterAreaState>, info: &IGVM_VHS_PARAMETER, parameter: &[u8], ) -> Result<(), Error>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 ///
load_igvm( mut file: &std::fs::File, memory_manager: Arc<Mutex<MemoryManager>>, cpu_manager: Arc<Mutex<CpuManager>>, cmdline: &str, #[cfg(feature = "sev_snp")] host_data: &Option<String>, ) -> Result<Box<IgvmLoadedInfo>, Error>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