1#![expect(missing_docs)]
8#![forbid(unsafe_code)]
9
10use igvm_defs::PAGE_SIZE_4K;
11use page_table::IdentityMapSize;
12use page_table::x64::X64_PTE_ADDRESS_BIT_RANGE;
13use product_policy::ProductPolicy;
14use serde::Deserialize;
15use serde::Serialize;
16use std::collections::HashMap;
17use std::ffi::CString;
18use std::path::PathBuf;
19
20#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
22#[serde(rename_all = "snake_case")]
23pub enum UefiConfigType {
24 None,
26 Igvm,
28}
29
30#[derive(Serialize, Deserialize, Debug)]
32#[serde(rename_all = "snake_case")]
33pub enum SnpInjectionType {
34 Normal,
36 Restricted,
38}
39
40#[derive(Serialize, Default, Deserialize, Debug)]
42#[serde(rename_all = "snake_case")]
43pub enum SecureAvicType {
44 Enabled,
46 #[default]
48 Disabled,
49}
50
51#[derive(Serialize, Deserialize, Debug)]
53#[serde(rename_all = "snake_case")]
54pub enum ConfigIsolationType {
55 None,
57 Vbs {
59 enable_debug: bool,
61 },
62 Snp {
64 shared_gpa_boundary_bits: Option<u8>,
68 policy: u64,
70 enable_debug: bool,
72 injection_type: SnpInjectionType,
74 #[serde(default)]
76 secure_avic: SecureAvicType,
77 },
78 Tdx {
80 enable_debug: bool,
82 sept_ve_disable: bool,
85 },
86}
87
88#[derive(Serialize, Deserialize, Debug)]
90#[serde(rename_all = "snake_case")]
91pub enum Image {
92 None,
94 Uefi { config_type: UefiConfigType },
96 Openhcl {
98 #[serde(default)]
100 command_line: String,
101 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
104 static_command_line: bool,
105 #[serde(skip_serializing_if = "Option::is_none")]
107 memory_page_base: Option<u64>,
108 memory_page_count: u64,
110 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
112 uefi: bool,
113 #[serde(skip_serializing_if = "Option::is_none")]
115 linux: Option<LinuxImage>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
122 product_policy: Option<ProductPolicy>,
123 },
124 Linux(LinuxImage),
127 SnpLinuxDirect {
129 linux: LinuxImage,
131 processor_count: u32,
133 memory_page_count: u64,
135 c_bit_position: u8,
137 },
138}
139
140#[derive(Serialize, Deserialize, Debug)]
141#[serde(rename_all = "snake_case")]
142pub struct LinuxImage {
143 pub use_initrd: bool,
145 pub command_line: CString,
147}
148
149impl Image {
150 pub fn required_resources(&self) -> Vec<ResourceType> {
152 match *self {
153 Image::None => vec![],
154 Image::Uefi { .. } => vec![ResourceType::Uefi],
155 Image::Openhcl {
156 uefi, ref linux, ..
157 } => [
158 ResourceType::UnderhillKernel,
159 ResourceType::OpenhclBoot,
160 ResourceType::UnderhillInitrd,
161 ]
162 .into_iter()
163 .chain(if uefi { Some(ResourceType::Uefi) } else { None })
164 .chain(linux.iter().flat_map(|linux| linux.required_resources()))
165 .collect(),
166 Image::Linux(ref linux) => linux.required_resources(),
167 Image::SnpLinuxDirect { ref linux, .. } => linux
168 .required_resources()
169 .into_iter()
170 .chain([ResourceType::SnpBootshim])
171 .collect(),
172 }
173 }
174
175 pub fn validate(&self) -> Result<(), ImageValidationError> {
177 if let Image::SnpLinuxDirect {
178 processor_count,
179 memory_page_count,
180 c_bit_position,
181 ..
182 } = *self
183 {
184 if processor_count == 0 {
185 return Err(ImageValidationError::ZeroProcessorCount);
186 }
187 if memory_page_count == 0 {
188 return Err(ImageValidationError::ZeroMemoryPageCount);
189 }
190
191 let memory_byte_count = memory_page_count
192 .checked_mul(PAGE_SIZE_4K)
193 .ok_or(ImageValidationError::MemoryByteCountTooLarge { memory_page_count })?;
194 if u32::try_from(memory_byte_count).is_err() {
195 return Err(ImageValidationError::MemoryByteCountTooLarge { memory_page_count });
196 }
197
198 let valid_c_bit = X64_PTE_ADDRESS_BIT_RANGE.contains(&c_bit_position)
203 && (1u64 << c_bit_position) >= IdentityMapSize::Size4Gb.address_space_size();
204 if !valid_c_bit {
205 return Err(ImageValidationError::InvalidCBitPosition { c_bit_position });
206 }
207 }
208
209 Ok(())
210 }
211}
212
213#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)]
215pub enum ImageValidationError {
216 #[error("processor_count must be nonzero")]
218 ZeroProcessorCount,
219 #[error("memory_page_count must be nonzero")]
221 ZeroMemoryPageCount,
222 #[error("memory_page_count {memory_page_count} has a byte count that does not fit in u32")]
225 MemoryByteCountTooLarge {
226 memory_page_count: u64,
228 },
229 #[error(
232 "c_bit_position {c_bit_position} overlaps the 4-GiB identity map or lies outside the x64 PTE address field"
233 )]
234 InvalidCBitPosition {
235 c_bit_position: u8,
237 },
238}
239
240impl LinuxImage {
241 fn required_resources(&self) -> Vec<ResourceType> {
242 [ResourceType::LinuxKernel]
243 .into_iter()
244 .chain(if self.use_initrd {
245 Some(ResourceType::LinuxInitrd)
246 } else {
247 None
248 })
249 .collect()
250 }
251}
252
253#[derive(Serialize, Deserialize, Debug)]
256pub struct GuestConfig {
257 pub guest_svn: u32,
259 pub max_vtl: u8,
261 pub isolation_type: ConfigIsolationType,
263 pub image: Image,
265}
266
267#[derive(Serialize, Deserialize, Debug)]
269#[serde(rename_all = "snake_case")]
270pub enum GuestArch {
271 X64,
273 Aarch64,
275}
276
277#[derive(Serialize, Deserialize, Debug)]
280#[serde(rename_all = "snake_case")]
281pub struct Config {
282 pub guest_arch: GuestArch,
284 pub guest_configs: Vec<GuestConfig>,
286}
287
288impl Config {
289 pub fn required_resources(&self) -> Vec<ResourceType> {
291 let mut resources = vec![];
292 for guest_config in &self.guest_configs {
293 resources.extend(guest_config.image.required_resources());
294 }
295 resources
296 }
297}
298
299#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
300#[serde(rename_all = "snake_case")]
301pub enum ResourceType {
302 Uefi,
303 UnderhillKernel,
304 OpenhclBoot,
305 UnderhillInitrd,
306 UnderhillSidecar,
307 LinuxKernel,
308 LinuxInitrd,
309 SnpBootshim,
310}
311
312#[derive(Serialize, Deserialize, Debug)]
315#[serde(rename_all = "snake_case")]
316pub struct Resources {
317 #[serde(deserialize_with = "parse::resources")]
320 resources: HashMap<ResourceType, PathBuf>,
321}
322
323mod parse {
324 use super::*;
325 use serde::Deserialize;
326 use serde::Deserializer;
327 use std::collections::HashMap;
328
329 pub fn resources<'de, D: Deserializer<'de>>(
330 d: D,
331 ) -> Result<HashMap<ResourceType, PathBuf>, D::Error> {
332 let resources: HashMap<ResourceType, PathBuf> = Deserialize::deserialize(d)?;
333
334 for (resource, path) in &resources {
335 if !path.is_absolute() {
336 return Err(serde::de::Error::custom(AbsolutePathError(
337 *resource,
338 path.clone(),
339 )));
340 }
341 }
342
343 Ok(resources)
344 }
345}
346
347#[derive(Debug)]
349pub struct MissingResourcesError(pub Vec<ResourceType>);
350
351impl std::fmt::Display for MissingResourcesError {
352 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353 write!(f, "missing resources: {:?}", self.0)
354 }
355}
356
357impl std::error::Error for MissingResourcesError {}
358
359#[derive(Debug)]
361pub struct AbsolutePathError(ResourceType, PathBuf);
362
363impl std::fmt::Display for AbsolutePathError {
364 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
365 write!(
366 f,
367 "resource {:?} path is not absolute: {:?}",
368 self.0, self.1
369 )
370 }
371}
372
373impl std::error::Error for AbsolutePathError {}
374
375impl Resources {
376 pub fn new(resources: HashMap<ResourceType, PathBuf>) -> Result<Self, AbsolutePathError> {
379 for (resource, path) in &resources {
380 if !path.is_absolute() {
381 return Err(AbsolutePathError(*resource, path.clone()));
382 }
383 }
384
385 Ok(Resources { resources })
386 }
387
388 pub fn resources(&self) -> &HashMap<ResourceType, PathBuf> {
390 &self.resources
391 }
392
393 pub fn get(&self, resource: ResourceType) -> Option<&PathBuf> {
395 self.resources.get(&resource)
396 }
397
398 pub fn check_required(&self, required: &[ResourceType]) -> Result<(), MissingResourcesError> {
401 let mut missing = vec![];
402 for resource in required {
403 if !self.resources.contains_key(resource) {
404 missing.push(*resource);
405 }
406 }
407
408 if missing.is_empty() {
409 Ok(())
410 } else {
411 Err(MissingResourcesError(missing))
412 }
413 }
414}
415
416#[cfg(test)]
417mod test {
418 use super::*;
419
420 fn snp_linux_direct_image(
421 use_initrd: bool,
422 processor_count: u32,
423 memory_page_count: u64,
424 c_bit_position: u8,
425 ) -> Image {
426 Image::SnpLinuxDirect {
427 linux: LinuxImage {
428 use_initrd,
429 command_line: CString::new("console=ttyS0").unwrap(),
430 },
431 processor_count,
432 memory_page_count,
433 c_bit_position,
434 }
435 }
436
437 #[test]
438 fn parse_snp_linux_direct_manifest() {
439 let config: Config =
440 serde_json::from_str(include_str!("../../manifests/snp-linux-direct.json")).unwrap();
441
442 assert!(matches!(config.guest_arch, GuestArch::X64));
443 let [guest] = config.guest_configs.as_slice() else {
444 panic!("expected one guest config");
445 };
446 assert_eq!(guest.guest_svn, 1);
447 assert_eq!(guest.max_vtl, 0);
448 match &guest.isolation_type {
449 ConfigIsolationType::Snp {
450 shared_gpa_boundary_bits,
451 policy,
452 enable_debug,
453 injection_type,
454 secure_avic,
455 } => {
456 assert_eq!(*shared_gpa_boundary_bits, None);
457 assert_eq!(*policy, 196608);
458 assert!(*enable_debug);
459 assert!(matches!(injection_type, SnpInjectionType::Normal));
460 assert!(matches!(secure_avic, SecureAvicType::Disabled));
461 }
462
463 isolation_type => panic!("unexpected isolation type: {isolation_type:?}"),
464 }
465 match &guest.image {
466 Image::SnpLinuxDirect {
467 linux,
468 processor_count,
469 memory_page_count,
470 c_bit_position,
471 } => {
472 assert!(linux.use_initrd);
473 assert_eq!(
474 linux.command_line.as_bytes(),
475 b"console=ttyS0 earlyprintk=serial earlycon panic=-1"
476 );
477 assert_eq!(*processor_count, 1);
478 assert_eq!(*memory_page_count, 40960);
479 assert_eq!(*c_bit_position, 51);
480 guest.image.validate().unwrap();
481 }
482 image => panic!("unexpected image: {image:?}"),
483 }
484 }
485
486 #[test]
487 fn parse_multi_vp_snp_linux_direct_manifest() {
488 let config: Config = serde_json::from_str(include_str!(
489 "../../manifests/snp-linux-direct-multi-vp.json"
490 ))
491 .unwrap();
492 let [guest] = config.guest_configs.as_slice() else {
493 panic!("expected one guest config");
494 };
495 let Image::SnpLinuxDirect {
496 processor_count, ..
497 } = &guest.image
498 else {
499 panic!("expected SNP Linux-direct image");
500 };
501 assert_eq!(*processor_count, 2);
502 guest.image.validate().unwrap();
503 }
504
505 #[test]
506 fn parse_restricted_snp_linux_direct_manifest() {
507 let config: Config = serde_json::from_str(include_str!(
508 "../../manifests/snp-linux-direct-restricted.json"
509 ))
510 .unwrap();
511 let [guest] = config.guest_configs.as_slice() else {
512 panic!("expected one guest config");
513 };
514 assert!(matches!(
515 guest.isolation_type,
516 ConfigIsolationType::Snp {
517 injection_type: SnpInjectionType::Restricted,
518 ..
519 }
520 ));
521 guest.image.validate().unwrap();
522 }
523
524 #[test]
525 fn snp_linux_direct_required_resources_with_initrd() {
526 let image = snp_linux_direct_image(true, 1, 40960, 51);
527
528 assert_eq!(
529 image.required_resources(),
530 vec![
531 ResourceType::LinuxKernel,
532 ResourceType::LinuxInitrd,
533 ResourceType::SnpBootshim,
534 ]
535 );
536 }
537
538 #[test]
539 fn snp_linux_direct_required_resources_without_initrd() {
540 let image = snp_linux_direct_image(false, 1, 40960, 51);
541
542 assert_eq!(
543 image.required_resources(),
544 vec![ResourceType::LinuxKernel, ResourceType::SnpBootshim]
545 );
546 }
547
548 #[test]
549 fn snp_linux_direct_requires_bootshim_resource() {
550 let image = snp_linux_direct_image(false, 1, 40960, 51);
551 let resources = Resources {
552 resources: [(ResourceType::LinuxKernel, PathBuf::from("/kernel"))]
553 .into_iter()
554 .collect(),
555 };
556
557 assert_eq!(
558 resources
559 .check_required(&image.required_resources())
560 .unwrap_err()
561 .0,
562 [ResourceType::SnpBootshim]
563 );
564 }
565
566 #[test]
567 fn snp_linux_direct_rejects_zero_memory() {
568 let image = snp_linux_direct_image(false, 1, 0, 51);
569
570 assert_eq!(
571 image.validate(),
572 Err(ImageValidationError::ZeroMemoryPageCount)
573 );
574 }
575
576 #[test]
577 fn snp_linux_direct_rejects_oversized_memory() {
578 let memory_page_count = u64::from(u32::MAX) / PAGE_SIZE_4K + 1;
579 let image = snp_linux_direct_image(false, 1, memory_page_count, 51);
580
581 assert_eq!(
582 image.validate(),
583 Err(ImageValidationError::MemoryByteCountTooLarge { memory_page_count })
584 );
585 }
586
587 #[test]
588 fn snp_linux_direct_rejects_invalid_c_bit_positions() {
589 for c_bit_position in [11, 31, 52] {
590 let image = snp_linux_direct_image(false, 1, 40960, c_bit_position);
591
592 assert_eq!(
593 image.validate(),
594 Err(ImageValidationError::InvalidCBitPosition { c_bit_position })
595 );
596 }
597
598 snp_linux_direct_image(false, 1, 40960, 32)
599 .validate()
600 .unwrap();
601 }
602
603 #[test]
604 fn snp_linux_direct_rejects_zero_processors() {
605 let image = snp_linux_direct_image(false, 0, 40960, 51);
606
607 assert_eq!(
608 image.validate(),
609 Err(ImageValidationError::ZeroProcessorCount)
610 );
611 }
612
613 #[test]
614 fn non_absolute_path_new() {
615 let mut resources = HashMap::new();
616 resources.insert(ResourceType::Uefi, PathBuf::from("./uefi"));
617 let result = Resources::new(resources);
618 assert!(result.is_err());
619 }
620
621 #[test]
622 fn parse_non_absolute_path() {
623 let resources = r#"{"uefi":"./uefi"}"#;
624 let result: Result<Resources, _> = serde_json::from_str(resources);
625 assert!(result.is_err());
626 }
627
628 #[test]
629 fn missing_resources() {
630 let resources = Resources {
631 resources: HashMap::new(),
632 };
633 let required = vec![ResourceType::Uefi];
634 let result = resources.check_required(&required);
635 assert!(result.is_err());
636 }
637
638 #[test]
639 fn openhcl_image_without_product_policy_round_trips() {
640 let json = r#"{"openhcl":{"command_line":"","memory_page_count":10,"uefi":true}}"#;
642 let parsed: Image = serde_json::from_str(json).unwrap();
643 match &parsed {
644 Image::Openhcl { product_policy, .. } => assert!(product_policy.is_none()),
645 other => panic!("unexpected parse: {other:?}"),
646 }
647 let reserialized = serde_json::to_string(&parsed).unwrap();
648 assert!(
649 !reserialized.contains("product_policy"),
650 "policy field should be omitted when None: {reserialized}"
651 );
652 }
653
654 #[test]
655 fn openhcl_image_with_sivm_product_policy_deserializes() {
656 let json = r#"{
657 "openhcl": {
658 "command_line": "",
659 "memory_page_count": 10,
660 "uefi": true,
661 "product_policy": {
662 "sivm": {
663 "require_ephemeral_vmgs": true,
664 "require_secure_boot": true,
665 "require_secure_boot_vars": true,
666 "require_bcd_integrity": true,
667 "custom_uefi_json": "ZGVhZGJlZWY="
668 }
669 }
670 }
671 }"#;
672 let parsed: Image = serde_json::from_str(json).unwrap();
673 match parsed {
674 Image::Openhcl {
675 product_policy: Some(policy),
676 ..
677 } => match policy {
678 ProductPolicy::Sivm(p) => {
679 assert!(p.require_ephemeral_vmgs);
680 assert!(p.require_secure_boot);
681 assert!(p.require_secure_boot_vars);
682 assert!(p.require_bcd_integrity);
683 assert_eq!(p.custom_uefi_json, b"deadbeef");
684 }
685 ProductPolicy::Cwcow(p) => panic!("unexpected Cwcow policy: {p:?}"),
686 },
687 other => panic!("unexpected parse: {other:?}"),
688 }
689 }
690
691 #[test]
692 fn openhcl_image_with_null_product_policy_is_absent() {
693 let json = r#"{
694 "openhcl": {
695 "command_line": "",
696 "memory_page_count": 10,
697 "uefi": true,
698 "product_policy": null
699 }
700 }"#;
701 let parsed: Image = serde_json::from_str(json).unwrap();
702 match parsed {
703 Image::Openhcl { product_policy, .. } => assert!(product_policy.is_none()),
704 other => panic!("unexpected parse: {other:?}"),
705 }
706 }
707}