Skip to main content

firmware_pcat/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! PCAT BIOS helper device.
5//!
6//! A bespoke virtual device that works in-tandem with the custom Hyper-V PCAT
7//! BIOS running within the guest.
8//!
9//! Provides interfaces to fetch various bits of VM machine topology and
10//! configuration, along with hooks into various VMM runtime services (e.g:
11//! event logging, efficient busy-waiting, generation ID, etc...).
12
13#![forbid(unsafe_code)]
14
15mod bios_boot_order;
16mod default_cmos_values;
17mod root_cpu_data;
18
19pub use default_cmos_values::default_cmos_values;
20
21use self::bios_boot_order::bios_boot_order;
22use chipset_device::ChipsetDevice;
23use chipset_device::io::IoError;
24use chipset_device::io::IoResult;
25use chipset_device::io::deferred::DeferredToken;
26use chipset_device::io::deferred::DeferredWrite;
27use chipset_device::io::deferred::defer_write;
28use chipset_device::mmio::MmioIntercept;
29use chipset_device::pio::ControlPortIoIntercept;
30use chipset_device::pio::PortIoIntercept;
31use chipset_device::pio::RegisterPortIoIntercept;
32use chipset_device::poll_device::PollDevice;
33use guestmem::GuestMemory;
34use guestmem::MapRom;
35use guestmem::UnmapRom;
36use inspect::Inspect;
37use inspect::InspectMut;
38use std::fmt::Debug;
39use std::ops::RangeInclusive;
40use std::task::Context;
41use std::time::Duration;
42use thiserror::Error;
43use vm_topology::memory::MemoryLayout;
44use vm_topology::processor::VpIndex;
45use vmcore::device_state::ChangeDeviceState;
46use vmcore::vmtime::VmTimeAccess;
47use vmcore::vmtime::VmTimeSource;
48use zerocopy::IntoBytes;
49
50/// Static config info which gets queried by the PCAT BIOS.
51pub mod config {
52    use guid::Guid;
53    use inspect::Inspect;
54    use memory_range::MemoryRange;
55    use vm_topology::memory::MemoryLayout;
56    use vm_topology::processor::ProcessorTopology;
57    use vm_topology::processor::x86::X86Topology;
58
59    /// Maximum number of bytes of a variable-length SMBIOS string (e.g. the
60    /// system serial number) that the config port can deliver to the BIOS ROM.
61    ///
62    /// The port returns each string in eight 4-byte chunks (`read_count % 8`),
63    /// so any bytes beyond this are never read by the guest and would be
64    /// silently truncated.
65    pub const SMBIOS_STRING_MAX_LEN: usize = 8 * 4;
66
67    /// Subset of SMBIOS v2.4 CPU Information structure.
68    #[derive(Debug, Inspect)]
69    #[expect(missing_docs)] // self-explanatory fields
70    pub struct SmbiosProcessorInfoBundle {
71        pub processor_family: u8,
72        pub voltage: u8,
73        pub external_clock: u16,
74        pub max_speed: u16,
75        pub current_speed: u16,
76    }
77
78    /// A collection of SMBIOS constants that get reflected into the guest.
79    ///
80    /// There is a lot of info here, but empirically, it's not _super_ important
81    /// to make these values 100% accurate...
82    #[expect(missing_docs)] // self-explanatory fields
83    #[derive(Debug, Inspect)]
84    pub struct SmbiosConstants {
85        pub bios_guid: Guid,
86        #[inspect(with = "String::from_utf8_lossy")]
87        pub system_serial_number: Vec<u8>,
88        #[inspect(with = "String::from_utf8_lossy")]
89        pub base_board_serial_number: Vec<u8>,
90        #[inspect(with = "String::from_utf8_lossy")]
91        pub chassis_serial_number: Vec<u8>,
92        #[inspect(with = "String::from_utf8_lossy")]
93        pub chassis_asset_tag: Vec<u8>,
94        #[inspect(with = "String::from_utf8_lossy")]
95        pub bios_lock_string: Vec<u8>,
96        #[inspect(with = "String::from_utf8_lossy")]
97        pub processor_manufacturer: Vec<u8>,
98        #[inspect(with = "String::from_utf8_lossy")]
99        pub processor_version: Vec<u8>,
100        /// If set to `None`, default UNKNOWN values are used
101        pub cpu_info_bundle: Option<SmbiosProcessorInfoBundle>,
102    }
103
104    /// A particular kind of boot device PCAT understands.
105    #[derive(Debug, Clone, Copy, Inspect)]
106    #[expect(missing_docs)] // self-explanatory variants
107    pub enum BootDevice {
108        Floppy = 0,
109        Optical = 1,
110        HardDrive = 2,
111        Network = 3,
112    }
113
114    /// Determines if a boot device is connected or not.
115    #[derive(Debug, Clone, Copy, Inspect)]
116    pub struct BootDeviceStatus {
117        /// Boot device
118        pub kind: BootDevice,
119        /// Whether it is physically attached to the system
120        pub attached: bool,
121    }
122
123    /// PCAT device static configuration data.
124    #[derive(Debug, Inspect)]
125    pub struct PcatBiosConfig {
126        /// Number of VCPUs
127        pub processor_topology: ProcessorTopology<X86Topology>,
128        /// The VM's memory layout
129        pub mem_layout: MemoryLayout,
130        /// Chipset low MMIO range (below 4 GB).
131        pub chipset_low_mmio: MemoryRange,
132        /// Chipset high MMIO range (above RAM).
133        pub chipset_high_mmio: MemoryRange,
134        /// The SRAT ACPI table reflected into the guest
135        pub srat: Vec<u8>,
136        /// Initial [Generation Id](generation_id) value
137        pub initial_generation_id: [u8; 16],
138        /// Hibernation support
139        pub hibernation_enabled: bool,
140        /// Boot device order
141        #[inspect(iter_by_index)]
142        pub boot_order: [BootDeviceStatus; 4],
143        /// If num-lock is enabled at boot
144        pub num_lock_enabled: bool,
145        /// Bundle of SMBIOS constants
146        pub smbios: SmbiosConstants,
147    }
148}
149
150/// PCAT event
151#[derive(Debug)]
152pub enum PcatEvent {
153    /// Failed to boot via any boot medium
154    BootFailure,
155    /// Attempted to boot (INT19) via BIOS
156    BootAttempt,
157}
158
159/// Platform interface to emit PCAT events.
160pub trait PcatLogger: Send {
161    /// Emit a log corresponding to the provided event.
162    fn log_event(&self, event: PcatEvent);
163}
164
165#[derive(Debug, Inspect)]
166struct PcatBiosState {
167    #[inspect(hex)]
168    address: u32,
169    #[inspect(hex)]
170    read_count: u32,
171    #[inspect(hex)]
172    e820_entry: u8,
173    #[inspect(hex)]
174    srat_offset: u32,
175    #[inspect(hex)]
176    srat_size: u32,
177    #[inspect(hex)]
178    port80: u32,
179    #[inspect(skip)]
180    entropy: [u8; 64],
181    entropy_placed: bool,
182}
183
184impl PcatBiosState {
185    fn new() -> Self {
186        let mut entropy = [0; 64];
187        getrandom::fill(&mut entropy).expect("rng failure");
188        Self {
189            address: 0,
190            read_count: 0,
191            e820_entry: 0,
192            srat_offset: 0,
193            srat_size: 0,
194            port80: 0,
195            entropy,
196            entropy_placed: false,
197        }
198    }
199}
200
201/// PCAT device runtime dependencies.
202#[expect(missing_docs)] // self-explanatory fields
203pub struct PcatBiosRuntimeDeps<'a> {
204    pub gm: GuestMemory,
205    pub logger: Box<dyn PcatLogger>,
206    pub generation_id_deps: generation_id::GenerationIdRuntimeDeps,
207    pub vmtime: &'a VmTimeSource,
208    /// The BIOS ROM.
209    ///
210    /// If missing, then assume the ROM is already in memory.
211    pub rom: Option<Box<dyn MapRom>>,
212    pub register_pio: &'a mut dyn RegisterPortIoIntercept,
213    /// Replays the initial MTRRs on all VPs.
214    pub replay_mtrrs: Box<dyn Send + FnMut()>,
215}
216
217/// PCAT BIOS helper device.
218#[derive(InspectMut)]
219pub struct PcatBiosDevice {
220    // Fixed configuration
221    config: config::PcatBiosConfig,
222
223    // Runtime glue
224    vmtime_wait: VmTimeAccess,
225    gm: GuestMemory,
226    #[inspect(skip)]
227    logger: Box<dyn PcatLogger>,
228    #[inspect(skip)]
229    _rom_mems: Vec<Box<dyn UnmapRom>>,
230    pre_boot_pio: PreBootStubbedPio,
231    #[inspect(skip)]
232    replay_mtrrs: Box<dyn Send + FnMut()>,
233
234    // Sub-emulators
235    #[inspect(mut)]
236    generation_id: generation_id::GenerationId,
237
238    // Runtime book-keeping
239    #[inspect(skip)]
240    deferred_wait: Option<DeferredWrite>,
241
242    // Volatile state
243    state: PcatBiosState,
244}
245
246// Begin and end range are inclusive.
247const IO_PORT_RANGE_BEGIN: u16 = 0x28;
248// The device only decodes dword accesses at IO_PORT_ADDR_OFFSET and
249// IO_PORT_DATA_OFFSET, so the top of the data dword (0x2e/0x2f) is left
250// unclaimed for the "missing-superio" device to absorb guest probes of the
251// legacy SuperIO ports.
252const IO_PORT_RANGE_END: u16 = 0x2d;
253const IO_PORT_ADDR_OFFSET: u16 = 0x0;
254const IO_PORT_DATA_OFFSET: u16 = 0x4;
255
256// Reports BIOS POST status.
257const POST_IO_PORT: u16 = 0x80;
258
259/// Errors which may occur during PCAT BIOS helper device initialization.
260#[derive(Debug, Error)]
261#[expect(missing_docs)] // self-explanatory variants
262pub enum PcatBiosDeviceInitError {
263    #[error("PCAT requires non-empty chipset low and high MMIO ranges")]
264    IncorrectMmioHoles,
265    #[error("invalid ROM size {0:x} bytes, expected 256KB")]
266    InvalidRomSize(u64),
267    #[error("error mapping ROM")]
268    Rom(#[source] std::io::Error),
269    #[error("SMBIOS {field} of {len} bytes exceeds the config port's {max}-byte limit")]
270    SmbiosStringTooLong {
271        field: &'static str,
272        len: usize,
273        max: usize,
274    },
275}
276
277impl PcatBiosDevice {
278    /// Create a new instance of the PCAT BIOS helper device.
279    pub fn new(
280        runtime_deps: PcatBiosRuntimeDeps<'_>,
281        config: config::PcatBiosConfig,
282    ) -> Result<PcatBiosDevice, PcatBiosDeviceInitError> {
283        let PcatBiosRuntimeDeps {
284            gm,
285            logger,
286            generation_id_deps,
287            vmtime,
288            rom,
289            register_pio,
290            replay_mtrrs,
291        } = runtime_deps;
292
293        let initial_generation_id = config.initial_generation_id;
294
295        // The config port delivers each variable-length SMBIOS string in
296        // fixed-size chunks; reject an over-long serial rather than silently
297        // truncating what the guest sees.
298        if config.smbios.system_serial_number.len() > config::SMBIOS_STRING_MAX_LEN {
299            return Err(PcatBiosDeviceInitError::SmbiosStringTooLong {
300                field: "system serial number",
301                len: config.smbios.system_serial_number.len(),
302                max: config::SMBIOS_STRING_MAX_LEN,
303            });
304        }
305
306        if config.chipset_low_mmio.is_empty() || config.chipset_high_mmio.is_empty() {
307            return Err(PcatBiosDeviceInitError::IncorrectMmioHoles);
308        }
309
310        let mut rom_mems = Vec::new();
311        if let Some(rom) = rom {
312            let rom_size = rom.len();
313            if rom_size != 0x40000 {
314                return Err(PcatBiosDeviceInitError::InvalidRomSize(rom_size));
315            }
316
317            // Map the ROM at both high and low memory.
318            for gpa in [0xfffc0000, 0xf0000] {
319                let rom_offset = (gpa + rom_size) & 0xfffff;
320                let len = rom_size - rom_offset;
321                let mem = rom
322                    .map_rom(gpa, rom_offset, len)
323                    .map_err(PcatBiosDeviceInitError::Rom)?;
324                rom_mems.push(mem);
325            }
326        }
327
328        Ok(PcatBiosDevice {
329            gm,
330            logger,
331            config,
332            state: PcatBiosState::new(),
333            generation_id: generation_id::GenerationId::new(
334                initial_generation_id,
335                generation_id_deps,
336            ),
337            vmtime_wait: vmtime.access("pcat-wait"),
338            deferred_wait: None,
339            _rom_mems: rom_mems,
340            pre_boot_pio: PreBootStubbedPio::new(register_pio),
341            replay_mtrrs,
342        })
343    }
344
345    fn index_using_read_count(&self, data: &[u8]) -> u32 {
346        let index = (self.state.read_count % 8) as usize * 4;
347        let mut buffer = [0u8; 4];
348        for i in 0..4_usize {
349            if index + i < data.len() {
350                buffer[i] = data[index + i];
351            } else {
352                buffer[i] = b' ';
353            }
354        }
355        u32::from_ne_bytes(buffer)
356    }
357
358    fn read_data(&mut self, addr: u32) -> u32 {
359        let mut buffer = [0u8; 4];
360        match PcatAddress(addr) {
361            PcatAddress::FIRST_MEMORY_BLOCK_SIZE => {
362                // Consumers: PCAT BIOS in source/bsp/OEM.ASM
363                //
364                // Report only the first memory block here as the BIOS really
365                // isn't structured to deal with gaps between memory blocks.
366                // This will bound where the BIOS puts things, including the
367                // ACPI tables, answers to INT 15 E820, etc.
368                self.config.mem_layout.ram()[0].range.len().to_kb()
369            }
370            PcatAddress::NUM_LOCK_ENABLED => self.config.num_lock_enabled as u32,
371            PcatAddress::BIOS_GUID => {
372                let index = (self.state.read_count % 4) as usize;
373                buffer.copy_from_slice(&self.config.smbios.bios_guid.as_bytes()[index * 4..][..4]);
374                u32::from_ne_bytes(buffer)
375            }
376            PcatAddress::BIOS_SYSTEM_SERIAL_NUMBER => {
377                self.index_using_read_count(self.config.smbios.system_serial_number.as_bytes())
378            }
379            PcatAddress::BIOS_BASE_SERIAL_NUMBER => {
380                self.index_using_read_count(self.config.smbios.base_board_serial_number.as_bytes())
381            }
382            PcatAddress::BIOS_CHASSIS_SERIAL_NUMBER => {
383                self.index_using_read_count(self.config.smbios.chassis_serial_number.as_bytes())
384            }
385            PcatAddress::BIOS_CHASSIS_ASSET_TAG => {
386                self.index_using_read_count(self.config.smbios.chassis_asset_tag.as_bytes())
387            }
388            PcatAddress::BOOT_DEVICE_ORDER => bios_boot_order(&self.config.boot_order),
389            PcatAddress::BIOS_PROCESSOR_COUNT => self.config.processor_topology.vp_count(),
390            PcatAddress::PROCESSOR_LOCAL_APIC_ID => {
391                if self.state.read_count < self.config.processor_topology.vp_count() {
392                    self.config
393                        .processor_topology
394                        .vp_arch(VpIndex::new(self.state.read_count))
395                        .apic_id
396                } else {
397                    !0
398                }
399            }
400            PcatAddress::SRAT_SIZE => self.config.srat.len() as u32,
401            PcatAddress::SRAT_DATA => {
402                let srat_chunk = (self.state.srat_offset + self.state.read_count * 4) as usize;
403                if let Some(data) = self.config.srat.get(srat_chunk..).and_then(|c| c.get(..4)) {
404                    u32::from_ne_bytes(data.try_into().unwrap())
405                } else {
406                    tracelimit::warn_ratelimited!(
407                        "invalid SRAT offset: {} + {} * 4 < {} - 4",
408                        self.state.srat_offset,
409                        self.state.read_count,
410                        self.config.srat.len()
411                    );
412                    0
413                }
414            }
415            PcatAddress::MEMORY_AMOUNT_ABOVE_4GB => {
416                // Consumers:
417                // - vmbios/source/bsp/em/smbios/Smbport.asm,
418                // - core/src/MEM.ASM.
419                self.config
420                    .mem_layout
421                    .ram()
422                    .iter()
423                    .filter(|r| r.range.end() >= 0x1_0000_0000)
424                    .map(|r| r.range.len())
425                    .sum::<u64>()
426                    .to_mb()
427            }
428            PcatAddress::SLEEP_STATES => {
429                // The AMI BIOS wants to read a byte value of flags to determine
430                // what sleep states (S1...S4) are supported. In the original
431                // AMI BIOS code, S4 was enabled as:
432                //
433                //              or      aml_buff.AMLDATA.dSx, 8
434                //
435                // Our data register is 4-bytes wide, we just fill in the low
436                // byte (al) here with the S4 flag if it should be set
437                if self.config.hibernation_enabled {
438                    8
439                } else {
440                    0
441                }
442            }
443            PcatAddress::PCI_IO_GAP_START => {
444                self.config.chipset_low_mmio.start().try_into().unwrap()
445            }
446            PcatAddress::PROCESSOR_STA_ENABLE => {
447                // Read by the ACPI _STA (status) method in the Processor
448                // objects in the PCAT BIOS DSDT. Return zero (not active) for
449                // any processor whose index exceeds the current active
450                // processor count.
451                if self.state.read_count < self.config.processor_topology.vp_count() {
452                    1
453                } else {
454                    0
455                }
456            }
457            PcatAddress::BIOS_LOCK_STRING => {
458                self.index_using_read_count(self.config.smbios.bios_lock_string.as_bytes())
459            }
460            PcatAddress::MEMORY_ABOVE_HIGH_MMIO => {
461                // Consumers:
462                // - vmbios/source/bsp/em/smbios/Smbport.asm,
463                // - core/src/MEM.ASM.
464                self.config
465                    .mem_layout
466                    .ram()
467                    .iter()
468                    .filter(|r| r.range.start() >= self.config.chipset_high_mmio.end())
469                    .map(|r| r.range.len())
470                    .sum::<u64>()
471                    .to_mb()
472            }
473            PcatAddress::HIGH_MMIO_GAP_BASE_IN_MB => {
474                // Consumers:
475                // - vmbios/source/bsp/em/smbios/Smbport.asm,
476                // - core/src/MEM.ASM.
477                self.config.chipset_high_mmio.start().to_mb()
478            }
479            PcatAddress::HIGH_MMIO_GAP_LENGTH_IN_MB => {
480                // Consumers:
481                // - vmbios/source/bsp/em/smbios/Smbport.asm,
482                // - core/src/MEM.ASM.
483                //
484                // In a classic case of "two wrongs make a right", PCAT expects
485                // to get _one less_ than the true MMIO region length , as when
486                // this code was written in Hyper-V, the `end - start`
487                // calculation used an _inclusive_ `start..=end` range from the
488                // MMIO gaps API, which wasn't properly compensated for here.
489                self.config.chipset_high_mmio.len().to_mb() - 1
490            }
491            PcatAddress::E820_ENTRY => handle_int15_e820_query(
492                &self.config.mem_layout,
493                self.state.e820_entry,
494                self.state.read_count,
495            ),
496            PcatAddress::INITIAL_MEGABYTES_BELOW_GAP => {
497                // Consumers: vmbios/source/bsp/em/smbios/smbios/Smbport.asm
498                self.config
499                    .mem_layout
500                    .ram()
501                    .iter()
502                    .filter(|r| r.range.end() < 0x1_0000_0000)
503                    .map(|r| r.range.len())
504                    .sum::<u64>()
505                    .to_mb()
506            }
507            _ => {
508                tracelimit::warn_ratelimited!(?addr, "unknown bios read");
509                0xffffffff
510            }
511        }
512    }
513
514    fn write_data(
515        &mut self,
516        addr: u32,
517        data: u32,
518    ) -> Result<Option<DeferredToken>, guestmem::GuestMemoryError> {
519        match PcatAddress(addr) {
520            PcatAddress::BIOS_PROCESSOR_COUNT => {
521                // gets poked by the bios for some reason...
522            }
523            PcatAddress::SRAT_SIZE => {
524                if self.config.srat.len() > (data as usize) {
525                    tracelimit::warn_ratelimited!(
526                        data,
527                        len = self.config.srat.len(),
528                        "improper SRAT_SIZE write",
529                    );
530                }
531
532                self.state.srat_size = data;
533            }
534            PcatAddress::SRAT_OFFSET => {
535                if (data as usize) >= self.config.srat.len() || data >= self.state.srat_size {
536                    tracelimit::warn_ratelimited!(
537                        data,
538                        len = self.config.srat.len(),
539                        "improper SRAT_OFFSET write",
540                    );
541                }
542
543                self.state.srat_offset = data;
544            }
545            PcatAddress::SRAT_DATA => {
546                if data == 0 || data == 0xffffffff {
547                    tracelimit::warn_ratelimited!(data, "improper SRAT_DATA write");
548                }
549
550                self.gm.write_at(data as u64, &self.config.srat)?;
551            }
552            PcatAddress::BOOT_FINALIZE => {
553                // The BIOS trashes the originally set MTRRs. Reset them.
554                (self.replay_mtrrs)();
555            }
556            PcatAddress::ENTROPY_TABLE => {
557                if data == 0 || data == 0xffffffff {
558                    tracelimit::warn_ratelimited!(data, "improper ENTROPY_TABLE write");
559                }
560
561                if !self.state.entropy_placed {
562                    self.gm.write_plain(data as u64, &self.state.entropy)?;
563                    self.state.entropy_placed = true;
564                }
565            }
566            PcatAddress::PROCESSOR_DMTF_TABLE => {
567                if data == 0 || data == 0xffffffff {
568                    tracelimit::warn_ratelimited!(data, "improper PROCESSOR_DMTF_TABLE write");
569                }
570
571                let cpu_info_legacy = root_cpu_data::get_vp_dmi_info(
572                    self.config.smbios.cpu_info_bundle.as_ref(),
573                    &self.config.smbios.processor_manufacturer,
574                    &self.config.smbios.processor_version,
575                );
576
577                self.gm.write_plain(data as u64, &cpu_info_legacy)?;
578            }
579            PcatAddress::PROCESSOR_STA_ENABLE => {
580                // NOTE: doesn't make a whole lot of sense, but that's what our
581                // old impl did, so better safe than sorry...
582                self.state.read_count = data;
583            }
584            PcatAddress::WAIT_NANO100 => {
585                return Ok(Some(
586                    self.defer_wait(Duration::from_nanos(data as u64 * 100)),
587                ));
588            }
589            PcatAddress::GENERATION_ID_PTR_LOW => self.generation_id.write_generation_id_low(data),
590            PcatAddress::GENERATION_ID_PTR_HIGH => {
591                self.generation_id.write_generation_id_high(data)
592            }
593            PcatAddress::E820_ENTRY => {
594                self.state.e820_entry = data as u8;
595            }
596            _ => tracelimit::warn_ratelimited!(addr, data, "unknown bios write"),
597        }
598
599        Ok(None)
600    }
601
602    fn write_address(&mut self, addr: u32) -> Option<DeferredToken> {
603        // As a side effect of setting the address register, we also reset the
604        // data register read counter.
605        self.state.address = addr;
606        self.state.read_count = 0;
607
608        // Some commands do not write to the data register, only the address
609        // register (so as to save an additional VMEXIT).
610        match PcatAddress(addr) {
611            PcatAddress::WAIT1_MILLISECOND => {
612                return Some(self.defer_wait(Duration::from_millis(1)));
613            }
614            PcatAddress::WAIT10_MILLISECONDS => {
615                return Some(self.defer_wait(Duration::from_millis(10)));
616            }
617            PcatAddress::WAIT2_MILLISECOND => {
618                return Some(self.defer_wait(Duration::from_millis(2)));
619            }
620            PcatAddress::REPORT_BOOT_FAILURE => {
621                tracelimit::info_ratelimited!("pcat boot: failure");
622                self.stop_pre_boot_pio();
623                self.logger.log_event(PcatEvent::BootFailure)
624            }
625            PcatAddress::REPORT_BOOT_ATTEMPT => {
626                tracelimit::info_ratelimited!("pcat boot: attempt");
627                self.stop_pre_boot_pio();
628                self.logger.log_event(PcatEvent::BootAttempt)
629            }
630            _ => {}
631        }
632        None
633    }
634
635    fn defer_wait(&mut self, duration: Duration) -> DeferredToken {
636        tracing::trace!(?duration, "deferring wait request");
637        self.vmtime_wait
638            .set_timeout(self.vmtime_wait.now().wrapping_add(duration));
639        let (write, token) = defer_write();
640        self.deferred_wait = Some(write);
641        token
642    }
643
644    /// Unmap the pre-boot PIO stubs if they are active.
645    /// This should be called before booting into an OS, since
646    /// the BIOS should no longer try to access these ports.
647    fn stop_pre_boot_pio(&mut self) {
648        if self.pre_boot_pio.is_active() {
649            tracing::info!("disabling pre-boot legacy port-io stubs");
650            self.pre_boot_pio.unmap();
651        }
652    }
653}
654
655open_enum::open_enum! {
656    /// Must match constants in VMCONFIG.EQU
657    enum PcatAddress: u32 {
658        FIRST_MEMORY_BLOCK_SIZE      = 0x00,
659        NUM_LOCK_ENABLED             = 0x01,
660        BIOS_GUID                    = 0x02,
661        BIOS_SYSTEM_SERIAL_NUMBER    = 0x03,
662        BIOS_BASE_SERIAL_NUMBER      = 0x04,
663        BIOS_CHASSIS_SERIAL_NUMBER   = 0x05,
664        BIOS_CHASSIS_ASSET_TAG       = 0x06,
665        BOOT_DEVICE_ORDER            = 0x07,
666        BIOS_PROCESSOR_COUNT         = 0x08,
667        PROCESSOR_LOCAL_APIC_ID      = 0x09,
668        SRAT_SIZE                    = 0x0A,
669        SRAT_OFFSET                  = 0x0B,
670        SRAT_DATA                    = 0x0C,
671        MEMORY_AMOUNT_ABOVE_4GB      = 0x0D,
672        GENERATION_ID_PTR_LOW        = 0x0E,
673        GENERATION_ID_PTR_HIGH       = 0x0F,
674        SLEEP_STATES                 = 0x10,
675
676        PCI_IO_GAP_START             = 0x12,
677
678        PROCESSOR_STA_ENABLE         = 0x16,
679        WAIT_NANO100                 = 0x17,
680        WAIT1_MILLISECOND            = 0x18,
681        WAIT10_MILLISECONDS          = 0x19,
682        BOOT_FINALIZE                = 0x1A,
683        WAIT2_MILLISECOND            = 0x1B,
684        BIOS_LOCK_STRING             = 0x1C,
685        PROCESSOR_DMTF_TABLE         = 0x1D,
686        ENTROPY_TABLE                = 0x1E,
687        MEMORY_ABOVE_HIGH_MMIO       = 0x1F,
688        HIGH_MMIO_GAP_BASE_IN_MB     = 0x20,
689        HIGH_MMIO_GAP_LENGTH_IN_MB   = 0x21,
690        E820_ENTRY                   = 0x22,
691        INITIAL_MEGABYTES_BELOW_GAP  = 0x23,
692
693        REPORT_BOOT_FAILURE          = 0x3A,
694        REPORT_BOOT_ATTEMPT          = 0x3B,
695    }
696}
697
698/// Handler for PCAT BIOS e820 Enlightenment
699///
700/// The following documentation is copied wholesale from the OS repo.
701///
702/// * * *
703///
704/// The guest OS will discover the parts of GPA space that are populated with
705/// usable RAM by using the INT 15 E820 interface. This interface returns one
706/// entry of the table per invocation, with an iterator value passed back and
707/// forth through EBX.
708///
709/// Our virtual AMI BIOS is constructed in a way that's difficult to change
710/// without odd side effects, as many things look at the E820 table entries
711/// internally, and it's not always clear which parts are switched on or off,
712/// making changes hard to validate.
713///
714/// Extending the AMI BIOS to understand an unbounded number of memory blocks,
715/// each with a small gap between them is more difficult than just calling out
716/// to the worker process and handing it here. On the other hand, some
717/// parameters, such as the location of the Extended BIOS Data Area (EBDA) are
718/// really BIOS-internal things and moving them to the worker process would be
719/// fragile. So the algorithm here is that the BIOS responds to queries about
720/// everything involving the first memory block. The BIOS sets itself up within
721/// that. Any subsequent memory block is handled here within the worker process.
722///
723/// From the ACPI spec:
724///
725/// ```text
726/// Input:
727///
728///     Register    |   Parameter   |   Description
729///                 |               |
730///       EAX       | Function Code |   E820
731///                 |               |
732///       EBX       | Continuation  |   Contains the loop counter.
733///                 |               |
734///       ES:DI     | Buffer Ptr    |   Pointer to a buffer with the table entry.
735///                 |               |
736///       ECX       | Buffer Size   |   Size of passed in struct.
737///                 |               |
738///       EDX       | Signature     |   'SMAP'
739///
740/// Output:
741///
742///       EAX       | Signature     |   'SMAP'
743///                 |               |
744///       ES:DI     | Buffer Ptr    |   same as input
745///                 |               |
746///       ECX       | Size          |   20 bytes
747///                 |               |
748///       EBX       | Continuation  |   Value that the caller should use to get
749///                 |               |   the next entry.
750///```
751///
752/// In order to avoid opening an aperture to the guest here, the BIOS takes
753/// register contents modified by this function and unpacks them into the
754/// caller's buffer.
755///
756/// The AMI BIOS will subtract the number of entries that it wants to handle
757/// internally from EBX before writing it to the BIOS port, so that this
758/// function will see indices starting with 0.
759///
760/// So we return to the guest using this port as a FIFO. Each successive read
761/// returns a different part of the data:
762///
763/// ```text
764///       0 (b:0)       | 1 == "entry exists"
765///       0 (b:1)       | 0 == "memory",      1 == "reserved"
766///       0 (b:2)       | 0 == "last entry",    1 == "there's more data"
767///       0 (31:3)      | Length in megabytes low (48:20)
768///       1             | Base Address Low
769///       2             | Base Address High
770/// ```
771fn handle_int15_e820_query(mem_layout: &MemoryLayout, e820_entry: u8, read_count: u32) -> u32 {
772    // The first memory range is the one that the BIOS itself knows about, and
773    // the one for which the BIOS will answer the guest OS's questions. This is
774    // done because the BIOS places various tables (EBDA, ACPI "reclaim", ACPI
775    // NVS, etc.) in this memory block, carving things out of it.
776    //
777    // The BIOS, on the other hand, has no idea, at least in the core BIOS code,
778    // that the other memory blocks exist. This is necessary because there can
779    // be a series of gaps between memory blocks that are hard to accommodate
780    // within the BIOS. For reporting things above the gaps, this function looks
781    // at the upper memory blocks.
782    let index = (e820_entry + 1) as usize;
783
784    // Special case: if there is only a single RAM range, no error should be
785    // logged + zero should be returned, indicating that there are no further
786    // RAM regions.
787    if e820_entry == 0 && mem_layout.ram().len() == 1 {
788        return 0;
789    }
790
791    let Some(ram) = mem_layout.ram().get(index) else {
792        tracelimit::warn_ratelimited!(?e820_entry, "unexpected e820 entry");
793        return 0;
794    };
795
796    match read_count {
797        0 => {
798            let mut data = 1; // entry exists
799            data |= if index + 1 != mem_layout.ram().len() {
800                0b100 // more data
801            } else {
802                0 // last entry
803            };
804            data |= ram.range.len().to_mb() << 3; // clamp reported RAM to the nearest megabyte
805            data
806        }
807        1 => ram.range.start() as u32,
808        2 => (ram.range.start() >> 32) as u32,
809        _ => {
810            tracelimit::warn_ratelimited!(?read_count, "invalid E820 read count");
811            0
812        }
813    }
814}
815
816impl ChangeDeviceState for PcatBiosDevice {
817    fn start(&mut self) {}
818
819    async fn stop(&mut self) {}
820
821    async fn reset(&mut self) {
822        self.generation_id.reset();
823        self.state = PcatBiosState::new();
824    }
825}
826
827impl ChipsetDevice for PcatBiosDevice {
828    fn supports_pio(&mut self) -> Option<&mut dyn PortIoIntercept> {
829        Some(self)
830    }
831
832    fn supports_mmio(&mut self) -> Option<&mut dyn MmioIntercept> {
833        Some(self)
834    }
835
836    fn supports_poll_device(&mut self) -> Option<&mut dyn PollDevice> {
837        Some(self)
838    }
839}
840
841impl PollDevice for PcatBiosDevice {
842    fn poll_device(&mut self, cx: &mut Context<'_>) {
843        self.generation_id.poll(cx);
844        while self.vmtime_wait.poll_timeout(cx).is_ready() {
845            if let Some(deferred) = self.deferred_wait.take() {
846                tracing::trace!("releasing deferred wait");
847                deferred.complete();
848            }
849        }
850    }
851}
852
853impl MmioIntercept for PcatBiosDevice {
854    fn mmio_read(&mut self, _addr: u64, _data: &mut [u8]) -> IoResult {
855        tracelimit::error_ratelimited!("firmware should be mapped, should not be visible as MMIO");
856        IoResult::Ok
857    }
858
859    fn mmio_write(&mut self, addr: u64, _data: &[u8]) -> IoResult {
860        match addr {
861            0xf5bea | 0xf5bfa => {
862                // There is a bug in the firmware's throttle_getchar_FAR
863                // enlightenment: it expects to write to a value in the ROM
864                // segment, but this is not writable after POST. Just ignore
865                // this, it means that getchar is not actually throttled after
866                // POST (e.g. in DOS).
867            }
868            _ => tracelimit::warn_ratelimited!(addr, "unexpected firmware write"),
869        }
870        IoResult::Ok
871    }
872
873    fn get_static_regions(&mut self) -> &[(&str, RangeInclusive<u64>)] {
874        &[
875            ("rom-low", 0xf0000..=0xfffff),
876            ("rom-high", 0xfffc_0000..=0xffff_ffff),
877        ]
878    }
879}
880
881impl PortIoIntercept for PcatBiosDevice {
882    fn io_read(&mut self, io_port: u16, data: &mut [u8]) -> IoResult {
883        if io_port == POST_IO_PORT {
884            data.copy_from_slice(&self.state.port80.to_ne_bytes()[..data.len()]);
885            return IoResult::Ok;
886        }
887
888        if self.pre_boot_pio.contains_port(io_port) {
889            tracing::trace!(?io_port, "stubbed pre-boot pio read");
890            data.fill(!0);
891            return IoResult::Ok;
892        }
893
894        if data.len() != 4 {
895            return IoResult::Err(IoError::InvalidAccessSize);
896        }
897
898        let offset = io_port - IO_PORT_RANGE_BEGIN;
899        let v = match offset {
900            IO_PORT_ADDR_OFFSET => self.state.address,
901            IO_PORT_DATA_OFFSET => self.read_data(self.state.address),
902            _ => return IoResult::Err(IoError::InvalidRegister),
903        };
904        data.copy_from_slice(&v.to_ne_bytes());
905
906        tracing::trace!(
907            offset,
908            address = self.state.address,
909            read_count = self.state.read_count,
910            value = v,
911            "bios read",
912        );
913
914        if offset == IO_PORT_DATA_OFFSET {
915            self.state.read_count += 1;
916        }
917
918        IoResult::Ok
919    }
920
921    fn io_write(&mut self, io_port: u16, data: &[u8]) -> IoResult {
922        if io_port == POST_IO_PORT {
923            let mut v = [0; 4];
924            v[..data.len()].copy_from_slice(data);
925            let data = u32::from_ne_bytes(v);
926
927            tracing::debug!(data, "pcat boot: checkpoint");
928
929            // magic number specific to PCAT BIOS
930            const AT_END_POST_CHECKPOINT: u32 = 0x50ac;
931            if data == AT_END_POST_CHECKPOINT {
932                self.stop_pre_boot_pio();
933            }
934
935            // Store the port 80 data. Consider keeping a ring of
936            // these for inspect in the future.
937            self.state.port80 = data;
938            return IoResult::Ok;
939        }
940
941        if self.pre_boot_pio.contains_port(io_port) {
942            tracing::trace!(?io_port, ?data, "stubbed pre-boot pio write");
943            return IoResult::Ok;
944        }
945
946        if data.len() != 4 {
947            return IoResult::Err(IoError::InvalidAccessSize);
948        }
949
950        let offset = io_port - IO_PORT_RANGE_BEGIN;
951        let v = u32::from_ne_bytes(data.try_into().unwrap());
952        let r = match offset {
953            IO_PORT_ADDR_OFFSET => Ok(self.write_address(v)),
954            IO_PORT_DATA_OFFSET => self.write_data(self.state.address, v),
955            _ => return IoResult::Err(IoError::InvalidRegister),
956        };
957
958        match r {
959            Ok(Some(token)) => return IoResult::Defer(token),
960            Ok(None) => {}
961            Err(err) => {
962                tracelimit::warn_ratelimited!(
963                    error = &err as &dyn std::error::Error,
964                    "bios command error"
965                );
966            }
967        }
968
969        tracing::trace!(
970            offset,
971            address = self.state.address,
972            read_count = self.state.read_count,
973            data = v,
974            "bios write",
975        );
976
977        IoResult::Ok
978    }
979
980    fn get_static_regions(&mut self) -> &[(&str, RangeInclusive<u16>)] {
981        &[
982            ("pcat_bios", IO_PORT_RANGE_BEGIN..=IO_PORT_RANGE_END),
983            // NOTE: POST port 0x80 might overlap with a an ISA DMA page register.
984            ("post", POST_IO_PORT..=POST_IO_PORT),
985        ]
986    }
987}
988
989/// Helper trait to convert bytes to various other units
990trait ConvertBytes {
991    /// Convert from bytes to megabytes
992    fn to_mb(self) -> u32;
993    /// Convert from bytes to kiloytes
994    fn to_kb(self) -> u32;
995}
996
997impl ConvertBytes for u64 {
998    fn to_mb(self) -> u32 {
999        (self >> 20).try_into().unwrap()
1000    }
1001
1002    fn to_kb(self) -> u32 {
1003        (self >> 10).try_into().unwrap()
1004    }
1005}
1006
1007/// Encapsulates ownership over various legacy port io locations that the PCAT
1008/// BIOS attempts to access during init.
1009///
1010/// We don't implement any of the devices backing these ports, so in order to
1011/// cut down on the large amount of "unknown device" logging, we claim these
1012/// ports for the PCAT BIOS helper device during pre-boot, and then release
1013/// ownership post-boot.
1014#[derive(Inspect)]
1015struct PreBootStubbedPio {
1016    #[inspect(iter_by_index)]
1017    ranges: Vec<Box<dyn ControlPortIoIntercept>>,
1018}
1019
1020impl PreBootStubbedPio {
1021    const LEN_PORT: &'static [(u16, u16)] = &[
1022        // ISA PnP
1023        (1, 0x279), // index
1024        (1, 0xa79), // write data port
1025        (1, 0x20b), // initial value for read data port
1026        (1, 0x20f), // ...which PCAT will increment by 4
1027        (1, 0x213),
1028        (1, 0x217),
1029        (1, 0x21b),
1030        (1, 0x21f),
1031        (1, 0x223),
1032        (1, 0x227), // ...until it gives up (after 8x tries)
1033        // something to do with archaic dual VGA init?
1034        (2, 0x102),
1035        (2, 0x46e8),
1036        // something to do with piix4 "routing ports"?
1037        (1, 0xeb),
1038        // (1, 0xed), // gets claimed as part of the 0xED IO port delay device
1039        (1, 0xee),
1040        // no idea ¯\_(ツ)_/¯
1041        (1, 0x6f0),
1042    ];
1043
1044    fn new(register_pio: &mut dyn RegisterPortIoIntercept) -> PreBootStubbedPio {
1045        let mut ranges = Vec::new();
1046        for &(len, port) in Self::LEN_PORT {
1047            let mut control = register_pio.new_io_region("legacy-port-stub", len);
1048            control.map(port);
1049            ranges.push(control)
1050        }
1051        PreBootStubbedPio { ranges }
1052    }
1053
1054    fn is_active(&self) -> bool {
1055        !self.ranges.is_empty()
1056    }
1057
1058    fn unmap(&mut self) {
1059        for mut range in self.ranges.drain(..) {
1060            range.unmap()
1061        }
1062    }
1063
1064    fn contains_port(&self, port: u16) -> bool {
1065        if !self.is_active() {
1066            return false;
1067        }
1068
1069        Self::LEN_PORT
1070            .iter()
1071            .any(|&(len, p)| (p..p + len).contains(&port))
1072    }
1073}
1074
1075mod save_restore {
1076    use super::*;
1077    use vmcore::save_restore::RestoreError;
1078    use vmcore::save_restore::SaveError;
1079    use vmcore::save_restore::SaveRestore;
1080
1081    mod state {
1082        use generation_id::GenerationId;
1083        use mesh::payload::Protobuf;
1084        use vmcore::save_restore::SaveRestore;
1085        use vmcore::save_restore::SavedStateRoot;
1086
1087        #[derive(Protobuf, SavedStateRoot)]
1088        #[mesh(package = "firmware.pcat")]
1089        pub struct SavedState {
1090            #[mesh(1)]
1091            pub address: u32,
1092            #[mesh(2)]
1093            pub read_count: u32,
1094            #[mesh(3)]
1095            pub e820_entry: u8,
1096            #[mesh(4)]
1097            pub srat_offset: u32,
1098            #[mesh(5)]
1099            pub srat_size: u32,
1100            #[mesh(6)]
1101            pub port80: u32,
1102            #[mesh(7)]
1103            pub entropy: [u8; 64],
1104            #[mesh(8)]
1105            pub entropy_placed: bool,
1106
1107            #[mesh(9)]
1108            pub genid: <GenerationId as SaveRestore>::SavedState,
1109        }
1110    }
1111
1112    impl SaveRestore for PcatBiosDevice {
1113        type SavedState = state::SavedState;
1114
1115        fn save(&mut self) -> Result<Self::SavedState, SaveError> {
1116            let PcatBiosState {
1117                address,
1118                read_count,
1119                e820_entry,
1120                srat_offset,
1121                srat_size,
1122                port80,
1123                entropy,
1124                entropy_placed,
1125            } = self.state;
1126
1127            let saved_state = state::SavedState {
1128                address,
1129                read_count,
1130                e820_entry,
1131                srat_offset,
1132                srat_size,
1133                port80,
1134                entropy,
1135                entropy_placed,
1136                genid: self.generation_id.save()?,
1137            };
1138
1139            // sanity check that there aren't any outstanding deferred IOs
1140            assert!(self.deferred_wait.is_none());
1141
1142            Ok(saved_state)
1143        }
1144
1145        fn restore(&mut self, state: Self::SavedState) -> Result<(), RestoreError> {
1146            let state::SavedState {
1147                address,
1148                read_count,
1149                e820_entry,
1150                srat_offset,
1151                srat_size,
1152                port80,
1153                entropy,
1154                entropy_placed,
1155                genid,
1156            } = state;
1157
1158            self.state = PcatBiosState {
1159                address,
1160                read_count,
1161                e820_entry,
1162                srat_offset,
1163                srat_size,
1164                port80,
1165                entropy,
1166                entropy_placed,
1167            };
1168
1169            self.generation_id.restore(genid)?;
1170
1171            Ok(())
1172        }
1173    }
1174}