vm_manifest_builder/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Crate to help build a VM manifest.
5//!
6//! The VM's _manifest_ is a list of device handles (and, for now, legacy device
7//! configuration for [`vmotherboard`]) for devices that are present in a VM.
8//!
9//! This crate helps build the manifest via the [`VmManifestBuilder`] type. This
10//! can be used to construct common manifests for different VM types, such as
11//! Hyper-V generation 1 and 2 VMs, unenlightened Linux VMs, and Underhill VMs.
12//!
13//! For now, this crate only builds handles and configuration for "chipset"
14//! devices. In the future, it will also build handles for PCI and VMBus
15//! devices.
16
17#![forbid(unsafe_code)]
18
19use chipset_resources::battery::BatteryDeviceHandleAArch64;
20use chipset_resources::battery::BatteryDeviceHandleX64;
21use chipset_resources::battery::HostBatteryUpdate;
22use chipset_resources::i8042::I8042DeviceHandle;
23use input_core::MultiplexedInputHandle;
24use missing_dev_resources::MissingDevHandle;
25use serial_16550_resources::Serial16550DeviceHandle;
26use serial_core::resources::DisconnectedSerialBackendHandle;
27use serial_debugcon_resources::SerialDebugconDeviceHandle;
28use serial_pl011_resources::SerialPl011DeviceHandle;
29use std::iter::zip;
30use thiserror::Error;
31use vm_resource::IntoResource;
32use vm_resource::Resource;
33use vm_resource::kind::SerialBackendHandle;
34use vmotherboard::ChipsetDeviceHandle;
35use vmotherboard::options::BaseChipsetManifest;
36
37/// Builder for a VM manifest.
38pub struct VmManifestBuilder {
39    ty: BaseChipsetType,
40    arch: MachineArch,
41    serial: Option<[Option<Resource<SerialBackendHandle>>; 4]>,
42    serial_wait_for_rts: bool,
43    proxy_vga: bool,
44    stub_floppy: bool,
45    battery_status_recv: Option<mesh::Receiver<HostBatteryUpdate>>,
46    framebuffer: bool,
47    guest_watchdog: bool,
48    psp: bool,
49    debugcon: Option<(Resource<SerialBackendHandle>, u16)>,
50}
51
52/// The VM's base chipset type, which determines the set of core devices (such
53/// as timers, interrupt controllers, and buses) that are present in the VM.
54pub enum BaseChipsetType {
55    /// Hyper-V generation 1 VM, with a PCAT firmware and PIIX4 chipset.
56    HypervGen1,
57    /// Hyper-V generation 2 VM, with a UEFI firmware and no legacy devices.
58    HypervGen2Uefi,
59    /// Hyper-V generation 2 VM, booting directly from Linux with no legacy
60    /// devices.
61    HyperVGen2LinuxDirect,
62    /// VM hosting an HCL (Underhill) instance, with no architectural devices at
63    /// all.
64    ///
65    /// The HCL will determine the actual devices presented to the guest OS;
66    /// this VMM just needs to present the devices needed by the HCL.
67    HclHost,
68    /// Unenlightened Linux VM, with a PCI bus and basic architectural devices.
69    UnenlightenedLinuxDirect,
70}
71
72/// The machine architecture of the VM.
73#[derive(Debug, Copy, Clone, PartialEq, Eq)]
74pub enum MachineArch {
75    /// x86_64 (AMD64) architecture.
76    X86_64,
77    /// AArch64 (ARM64) architecture.
78    Aarch64,
79}
80
81/// The result of building a VM manifest.
82pub struct VmChipsetResult {
83    /// The base chipset manifest for the VM.
84    pub chipset: BaseChipsetManifest,
85    /// The list of chipset devices present in the VM.
86    pub chipset_devices: Vec<ChipsetDeviceHandle>,
87}
88
89/// Error type for building a VM manifest.
90#[derive(Debug, Error)]
91#[error(transparent)]
92pub struct Error(#[from] ErrorInner);
93
94#[derive(Debug, Error)]
95enum ErrorInner {
96    #[error("unsupported architecture")]
97    UnsupportedArch,
98    #[error("unsupported serial port count")]
99    UnsupportedSerialCount,
100    #[error("unsupported debugcon architecture")]
101    UnsupportedDebugconArch,
102    #[error("wait for RTS not supported with this serial type")]
103    WaitForRtsNotSupported,
104}
105
106impl VmManifestBuilder {
107    /// Create a new VM manifest builder for the given chipset type and
108    /// architecture.
109    pub fn new(ty: BaseChipsetType, arch: MachineArch) -> Self {
110        VmManifestBuilder {
111            ty,
112            arch,
113            serial: None,
114            serial_wait_for_rts: false,
115            proxy_vga: false,
116            stub_floppy: false,
117            battery_status_recv: None,
118            framebuffer: false,
119            guest_watchdog: false,
120            psp: false,
121            debugcon: None,
122        }
123    }
124
125    /// Enable serial ports (of a type determined by the chipset type), backed
126    /// by the given serial backends.
127    ///
128    /// For Hyper-V generation 1 VMs, serial ports are always present but are
129    /// disconnected unless this method is called. For other VMs, this method
130    /// must be called to add serial ports.
131    ///
132    /// For ARM64 VMs, only two serial ports are supported.
133    pub fn with_serial(mut self, serial: [Option<Resource<SerialBackendHandle>>; 4]) -> Self {
134        self.serial = Some(serial);
135        self
136    }
137
138    /// Enable wait-for-RTS mode for serial ports.
139    ///
140    /// This ensures that the VMM will not push data into the serial port's FIFO
141    /// until the guest has raised the RTS line.
142    pub fn with_serial_wait_for_rts(mut self) -> Self {
143        self.serial_wait_for_rts = true;
144        self
145    }
146
147    /// Enable the debugcon output-only serial device at the specified port,
148    /// backed by the given serial backend.
149    ///
150    /// Only supported on x86
151    pub fn with_debugcon(mut self, serial: Resource<SerialBackendHandle>, port: u16) -> Self {
152        self.debugcon = Some((serial, port));
153        self
154    }
155
156    /// Enable the proxy VGA device.
157    ///
158    /// This is used for Underhill VMs that are emulating Hyper-V generation 1
159    /// VMs.
160    pub fn with_proxy_vga(mut self) -> Self {
161        assert!(matches!(self.ty, BaseChipsetType::HypervGen1));
162        self.proxy_vga = true;
163        self
164    }
165
166    /// Enable the battery device.
167    pub fn with_battery(mut self, battery_status_recv: mesh::Receiver<HostBatteryUpdate>) -> Self {
168        self.battery_status_recv = Some(battery_status_recv);
169        self
170    }
171
172    /// Enable the stub floppy device instead of the full floppy device
173    /// implementation.
174    ///
175    /// This is used to support the saved states for VMs that used the stub
176    /// floppy device.
177    ///
178    /// This is only supported for Hyper-V generation 1 VMs. Panics otherwise.
179    pub fn with_stub_floppy(mut self) -> Self {
180        assert!(matches!(self.ty, BaseChipsetType::HypervGen1));
181        self.stub_floppy = true;
182        self
183    }
184
185    /// Enable the framebuffer device.
186    ///
187    /// This is implicit for Hyper-V generation 1 VMs.
188    ///
189    /// This method will be removed once all devices depending on the
190    /// framebuffer are managed through this builder type.
191    pub fn with_framebuffer(mut self) -> Self {
192        self.framebuffer = true;
193        self
194    }
195
196    /// Enable the guest watchdog device.
197    pub fn with_guest_watchdog(mut self) -> Self {
198        self.guest_watchdog = true;
199        self
200    }
201
202    /// Enable the AMD64 PSP device.
203    pub fn with_psp(mut self) -> Self {
204        self.psp = true;
205        self
206    }
207
208    /// Build the VM manifest.
209    pub fn build(self) -> Result<VmChipsetResult, Error> {
210        let mut result = VmChipsetResult {
211            chipset_devices: Vec::new(),
212            chipset: BaseChipsetManifest::empty(),
213        };
214
215        if let Some((backend, port)) = self.debugcon {
216            if matches!(self.arch, MachineArch::X86_64) {
217                result.attach_debugcon(port, backend);
218            } else {
219                return Err(ErrorInner::UnsupportedDebugconArch.into());
220            }
221        }
222
223        match self.ty {
224            BaseChipsetType::HypervGen1 => {
225                if self.arch != MachineArch::X86_64 {
226                    return Err(Error(ErrorInner::UnsupportedArch));
227                }
228                result.attach_i8042();
229                // This chipset always has a serial port even if not requested.
230                result.attach_serial_16550(
231                    self.serial_wait_for_rts,
232                    self.serial.unwrap_or_else(|| [(); 4].map(|_| None)),
233                );
234                result.chipset = BaseChipsetManifest {
235                    with_generic_cmos_rtc: false,
236                    with_generic_ioapic: true,
237                    with_generic_isa_dma: true,
238                    with_generic_isa_floppy: false,
239                    with_generic_pci_bus: false,
240                    with_generic_pic: true,
241                    with_generic_pit: true,
242                    with_generic_psp: false,
243                    with_hyperv_firmware_pcat: true,
244                    with_hyperv_firmware_uefi: false,
245                    with_hyperv_framebuffer: !self.proxy_vga,
246                    with_hyperv_guest_watchdog: false,
247                    with_hyperv_ide: true,
248                    with_hyperv_power_management: false,
249                    with_hyperv_vga: !self.proxy_vga,
250                    with_i440bx_host_pci_bridge: true,
251                    with_piix4_cmos_rtc: true,
252                    with_piix4_pci_bus: true,
253                    with_piix4_pci_isa_bridge: true,
254                    with_piix4_pci_usb_uhci_stub: true,
255                    with_piix4_power_management: true,
256                    with_underhill_vga_proxy: self.proxy_vga,
257                    with_winbond_super_io_and_floppy_stub: self.stub_floppy,
258                    with_winbond_super_io_and_floppy_full: !self.stub_floppy,
259                };
260                result.attach_missing_arch_ports(self.arch, false);
261                if let Some(recv) = self.battery_status_recv {
262                    result.attach_battery(self.arch, recv);
263                }
264            }
265            BaseChipsetType::UnenlightenedLinuxDirect => {
266                let is_x86 = matches!(self.arch, MachineArch::X86_64);
267                result.chipset = BaseChipsetManifest {
268                    with_generic_cmos_rtc: is_x86,
269                    with_generic_ioapic: is_x86,
270                    with_generic_isa_dma: false,
271                    with_generic_isa_floppy: false,
272                    with_generic_pci_bus: is_x86,
273                    with_generic_pic: is_x86,
274                    with_generic_pit: is_x86,
275                    with_generic_psp: self.psp,
276                    with_hyperv_firmware_pcat: false,
277                    with_hyperv_firmware_uefi: false,
278                    with_hyperv_framebuffer: self.framebuffer,
279                    with_hyperv_guest_watchdog: self.guest_watchdog,
280                    with_hyperv_ide: false,
281                    with_hyperv_power_management: is_x86,
282                    with_hyperv_vga: false,
283                    with_i440bx_host_pci_bridge: false,
284                    with_piix4_cmos_rtc: false,
285                    with_piix4_pci_bus: false,
286                    with_piix4_pci_isa_bridge: false,
287                    with_piix4_pci_usb_uhci_stub: false,
288                    with_piix4_power_management: false,
289                    with_underhill_vga_proxy: false,
290                    with_winbond_super_io_and_floppy_stub: false,
291                    with_winbond_super_io_and_floppy_full: false,
292                };
293                result
294                    .maybe_attach_arch_serial(
295                        self.arch,
296                        self.serial_wait_for_rts,
297                        true,
298                        self.serial,
299                    )?
300                    .attach_missing_arch_ports(self.arch, false);
301                if let Some(recv) = self.battery_status_recv {
302                    result.attach_battery(self.arch, recv);
303                }
304            }
305            BaseChipsetType::HypervGen2Uefi | BaseChipsetType::HyperVGen2LinuxDirect => {
306                let is_x86 = matches!(self.arch, MachineArch::X86_64);
307                result.chipset = BaseChipsetManifest {
308                    with_generic_cmos_rtc: is_x86,
309                    with_generic_ioapic: is_x86,
310                    with_generic_isa_dma: false,
311                    with_generic_isa_floppy: false,
312                    with_generic_pci_bus: false,
313                    with_generic_pic: false,
314                    with_generic_pit: false,
315                    with_generic_psp: self.psp,
316                    with_hyperv_firmware_pcat: false,
317                    with_hyperv_firmware_uefi: matches!(self.ty, BaseChipsetType::HypervGen2Uefi),
318                    with_hyperv_framebuffer: self.framebuffer,
319                    with_hyperv_guest_watchdog: self.guest_watchdog,
320                    with_hyperv_ide: false,
321                    with_hyperv_power_management: is_x86,
322                    with_hyperv_vga: false,
323                    with_i440bx_host_pci_bridge: false,
324                    with_piix4_cmos_rtc: false,
325                    with_piix4_pci_bus: false,
326                    with_piix4_pci_isa_bridge: false,
327                    with_piix4_pci_usb_uhci_stub: false,
328                    with_piix4_power_management: false,
329                    with_underhill_vga_proxy: false,
330                    with_winbond_super_io_and_floppy_stub: false,
331                    with_winbond_super_io_and_floppy_full: false,
332                };
333                result
334                    .maybe_attach_arch_serial(
335                        self.arch,
336                        self.serial_wait_for_rts,
337                        true,
338                        self.serial,
339                    )?
340                    .attach_missing_arch_ports(self.arch, true);
341                if let Some(recv) = self.battery_status_recv {
342                    result.attach_battery(self.arch, recv);
343                }
344            }
345            BaseChipsetType::HclHost => {
346                result.chipset = BaseChipsetManifest {
347                    with_hyperv_framebuffer: self.framebuffer,
348                    ..BaseChipsetManifest::empty()
349                };
350                result.maybe_attach_arch_serial(
351                    self.arch,
352                    self.serial_wait_for_rts,
353                    false,
354                    self.serial,
355                )?;
356                if let Some(recv) = self.battery_status_recv {
357                    result.attach_battery(self.arch, recv);
358                }
359            }
360        }
361        Ok(result)
362    }
363}
364
365impl VmChipsetResult {
366    fn attach_i8042(&mut self) -> &mut Self {
367        self.chipset_devices.push(ChipsetDeviceHandle {
368            name: "i8042".to_owned(),
369            resource: I8042DeviceHandle {
370                keyboard_input: MultiplexedInputHandle { elevation: 0 }.into_resource(),
371            }
372            .into_resource(),
373        });
374        self
375    }
376
377    fn attach_battery(
378        &mut self,
379        arch: MachineArch,
380        battery_status_recv: mesh::Receiver<HostBatteryUpdate>,
381    ) -> &mut Self {
382        self.chipset_devices.push(ChipsetDeviceHandle {
383            name: "battery".to_owned(),
384            resource: match arch {
385                MachineArch::X86_64 => BatteryDeviceHandleX64 {
386                    battery_status_recv,
387                }
388                .into_resource(),
389                MachineArch::Aarch64 => BatteryDeviceHandleAArch64 {
390                    battery_status_recv,
391                }
392                .into_resource(),
393            },
394        });
395
396        self
397    }
398
399    fn maybe_attach_arch_serial(
400        &mut self,
401        arch: MachineArch,
402        wait_for_rts: bool,
403        register_missing: bool,
404        serial: Option<[Option<Resource<SerialBackendHandle>>; 4]>,
405    ) -> Result<&mut Self, ErrorInner> {
406        if let Some(serial) = serial {
407            match arch {
408                MachineArch::X86_64 => {
409                    self.attach_serial_16550(wait_for_rts, serial);
410                }
411                MachineArch::Aarch64 => {
412                    if wait_for_rts {
413                        return Err(ErrorInner::WaitForRtsNotSupported);
414                    }
415                    self.attach_serial_pl011(serial)?;
416                }
417            }
418        } else if register_missing && arch == MachineArch::X86_64 {
419            self.chipset_devices.push(ChipsetDeviceHandle {
420                name: "missing-serial".to_owned(),
421                resource: MissingDevHandle::new()
422                    .claim_pio("com1", 0x3f8..=0x3ff)
423                    .claim_pio("com2", 0x2f8..=0x2ff)
424                    .claim_pio("com3", 0x3e8..=0x3ef)
425                    .claim_pio("com4", 0x2e8..=0x2ef)
426                    .into_resource(),
427            });
428        }
429        Ok(self)
430    }
431
432    fn attach_debugcon(&mut self, port: u16, backend: Resource<SerialBackendHandle>) -> &mut Self {
433        self.chipset_devices.push(ChipsetDeviceHandle {
434            name: format!("debugcon-{port:#x?}"),
435            resource: SerialDebugconDeviceHandle { port, io: backend }.into_resource(),
436        });
437        self
438    }
439
440    fn attach_serial_16550(
441        &mut self,
442        wait_for_rts: bool,
443        backends: [Option<Resource<SerialBackendHandle>>; 4],
444    ) -> &mut Self {
445        let mut devices = Serial16550DeviceHandle::com_ports(
446            backends.map(|r| r.unwrap_or_else(|| DisconnectedSerialBackendHandle.into_resource())),
447        );
448
449        if wait_for_rts {
450            devices = devices.map(|d| Serial16550DeviceHandle {
451                wait_for_rts: true,
452                ..d
453            });
454        }
455
456        self.chipset_devices.extend(
457            zip(
458                ["serial-com1", "serial-com2", "serial-com3", "serial-com4"],
459                devices,
460            )
461            .map(|(name, device)| ChipsetDeviceHandle {
462                name: name.to_string(),
463                resource: device.into_resource(),
464            }),
465        );
466        self
467    }
468
469    fn attach_serial_pl011(
470        &mut self,
471        backends: [Option<Resource<SerialBackendHandle>>; 4],
472    ) -> Result<&mut Self, ErrorInner> {
473        const PL011_SERIAL0_BASE: u64 = 0xEFFEC000;
474        const PL011_SERIAL0_IRQ: u32 = 1;
475        const PL011_SERIAL1_BASE: u64 = 0xEFFEB000;
476        const PL011_SERIAL1_IRQ: u32 = 2;
477
478        let [backend0, backend1, backend2, backend3] = backends;
479        if backend2.is_some() || backend3.is_some() {
480            return Err(ErrorInner::UnsupportedSerialCount);
481        }
482        self.chipset_devices.extend([
483            ChipsetDeviceHandle {
484                name: "com1".to_string(),
485                resource: SerialPl011DeviceHandle {
486                    base: PL011_SERIAL0_BASE,
487                    irq: PL011_SERIAL0_IRQ,
488                    io: backend0.unwrap_or_else(|| DisconnectedSerialBackendHandle.into_resource()),
489                }
490                .into_resource(),
491            },
492            ChipsetDeviceHandle {
493                name: "com2".to_string(),
494                resource: SerialPl011DeviceHandle {
495                    base: PL011_SERIAL1_BASE,
496                    irq: PL011_SERIAL1_IRQ,
497                    io: backend1.unwrap_or_else(|| DisconnectedSerialBackendHandle.into_resource()),
498                }
499                .into_resource(),
500            },
501        ]);
502        Ok(self)
503    }
504
505    fn attach_missing_arch_ports(&mut self, arch: MachineArch, pcat_missing: bool) -> &mut Self {
506        if arch != MachineArch::X86_64 {
507            return self;
508        }
509
510        self.chipset_devices.extend([
511            // Some linux versions write to port 0xED as an IO delay mechanims.
512            ChipsetDeviceHandle {
513                name: "io-delay-0xed".to_owned(),
514                resource: MissingDevHandle::new()
515                    .claim_pio("delay", 0xed..=0xed)
516                    .into_resource(),
517            },
518            // some windows versions try to unconditionally access these IO ports.
519            ChipsetDeviceHandle {
520                name: "missing-vmware-backdoor".to_owned(),
521                resource: MissingDevHandle::new()
522                    .claim_pio("backdoor", 0x5658..=0x5659)
523                    .into_resource(),
524            },
525            // DOS games often unconditionally poll the gameport (e.g: Duke Nukem 1)
526            ChipsetDeviceHandle {
527                name: "missing-gameport".to_owned(),
528                resource: MissingDevHandle::new()
529                    .claim_pio("gameport", 0x201..=0x201)
530                    .into_resource(),
531            },
532        ]);
533
534        if pcat_missing {
535            self.chipset_devices.extend([
536                ChipsetDeviceHandle {
537                    name: "missing-pic".to_owned(),
538                    resource: MissingDevHandle::new()
539                        .claim_pio("primary", 0x20..=0x21)
540                        .claim_pio("secondary", 0xa0..=0xa1)
541                        .into_resource(),
542                },
543                ChipsetDeviceHandle {
544                    name: "missing-pit".to_owned(),
545                    resource: MissingDevHandle::new()
546                        .claim_pio("main", 0x40..=0x43)
547                        .claim_pio("port61", 0x61..=0x61)
548                        .into_resource(),
549                },
550                ChipsetDeviceHandle {
551                    name: "missing-pci".to_owned(),
552                    resource: MissingDevHandle::new()
553                        .claim_pio("address", 0xcf8..=0xcfb)
554                        .claim_pio("data", 0xcfc..=0xcff)
555                        .into_resource(),
556                },
557                // Linux will probe 0x87 during boot to determine if there the DMA
558                // device is present
559                ChipsetDeviceHandle {
560                    name: "missing-dma".to_owned(),
561                    resource: MissingDevHandle::new()
562                        .claim_pio("io", 0x87..=0x87)
563                        .into_resource(),
564                },
565            ]);
566        }
567        self
568    }
569}