Skip to main content

openhcl_boot/
hypercall.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Hypercall infrastructure.
5
6#[cfg(target_arch = "x86_64")]
7use crate::arch::TdxHypercallPage;
8#[cfg(target_arch = "x86_64")]
9use crate::arch::tdx::invoke_tdcall_hypercall;
10use crate::single_threaded::SingleThreaded;
11use arrayvec::ArrayVec;
12use core::cell::RefCell;
13use core::cell::UnsafeCell;
14use core::mem::size_of;
15use hvdef::HV_PAGE_SIZE;
16use hvdef::Vtl;
17use hvdef::hypercall::HvInputVtl;
18#[cfg(target_arch = "x86_64")]
19use hvdef::hypercall::StartVirtualProcessorX64;
20use memory_range::MemoryRange;
21use minimal_rt::arch::hypercall::invoke_hypercall;
22use zerocopy::FromBytes;
23use zerocopy::IntoBytes;
24
25/// Page-aligned, page-sized buffer for use with hypercalls
26#[repr(C, align(4096))]
27struct HvcallPage {
28    buffer: [u8; HV_PAGE_SIZE as usize],
29}
30
31impl HvcallPage {
32    pub const fn new() -> Self {
33        HvcallPage {
34            buffer: [0; HV_PAGE_SIZE as usize],
35        }
36    }
37
38    /// Address of the hypercall page.
39    fn address(&self) -> u64 {
40        let addr = self.buffer.as_ptr() as u64;
41
42        // These should be page-aligned
43        assert!(addr.is_multiple_of(HV_PAGE_SIZE));
44
45        addr
46    }
47}
48
49/// Static, reusable page for hypercall input
50static HVCALL_INPUT: SingleThreaded<UnsafeCell<HvcallPage>> =
51    SingleThreaded(UnsafeCell::new(HvcallPage::new()));
52
53/// Static, reusable page for hypercall output
54static HVCALL_OUTPUT: SingleThreaded<UnsafeCell<HvcallPage>> =
55    SingleThreaded(UnsafeCell::new(HvcallPage::new()));
56
57static HVCALL: SingleThreaded<RefCell<HvCall>> = SingleThreaded(RefCell::new(HvCall {
58    initialized: false,
59    vtl: Vtl::Vtl0,
60    // The hypercall page is 4KB in the standard setting, but we allocate a large page for
61    // TDX compatibility. This is because the underlying static page is mapped in the
62    // shim's virtual memory hieararchy as a large page, making 2-MB the minimum shareable
63    // memory size between the TDX-enabled shim and hypervisor
64    #[cfg(target_arch = "x86_64")]
65    tdx_io_page: None,
66}));
67
68/// Provides mechanisms to invoke hypercalls within the boot shim.
69/// Internally uses static buffers for the hypercall page, the input
70/// page, and the output page, so this should not be used in any
71/// multi-threaded capacity (which the boot shim currently is not).
72pub struct HvCall {
73    initialized: bool,
74    vtl: Vtl,
75    #[cfg(target_arch = "x86_64")]
76    tdx_io_page: Option<TdxHypercallPage>,
77}
78
79/// Returns an [`HvCall`] instance.
80///
81/// Panics if another instance is already in use.
82#[track_caller]
83pub fn hvcall() -> core::cell::RefMut<'static, HvCall> {
84    HVCALL.borrow_mut()
85}
86
87impl HvCall {
88    fn input_page() -> &'static mut HvcallPage {
89        // SAFETY: `HVCALL` owns the input page.
90        unsafe { &mut *HVCALL_INPUT.get() }
91    }
92
93    fn output_page() -> &'static mut HvcallPage {
94        // SAFETY: `HVCALL` owns the output page.
95        unsafe { &mut *HVCALL_OUTPUT.get() }
96    }
97
98    /// Returns the address of the hypercall page, mapping it first if
99    /// necessary.
100    #[cfg(target_arch = "x86_64")]
101    pub fn hypercall_page(&mut self) -> u64 {
102        self.init_if_needed();
103        core::ptr::addr_of!(minimal_rt::arch::hypercall::HYPERCALL_PAGE) as u64
104    }
105
106    #[cfg(target_arch = "x86_64")]
107    fn init_if_needed(&mut self) {
108        if !self.initialized {
109            self.initialize();
110        }
111    }
112
113    /// Hypercall initialization.
114    pub fn initialize(&mut self) {
115        assert!(!self.initialized);
116        // TODO: revisit os id value. For now, use 1 (which is what UEFI does)
117        let guest_os_id = hvdef::hypercall::HvGuestOsMicrosoft::new().with_os_id(1);
118
119        crate::arch::hypercall::initialize(guest_os_id);
120        self.initialized = true;
121
122        self.vtl = self
123            .get_register(hvdef::HvAllArchRegisterName::VsmVpStatus.into())
124            .map_or(Vtl::Vtl0, |status| {
125                hvdef::HvRegisterVsmVpStatus::from(status.as_u64())
126                    .active_vtl()
127                    .try_into()
128                    .unwrap()
129            });
130    }
131
132    /// Call before jumping to kernel.
133    pub fn uninitialize(&mut self) {
134        if self.initialized {
135            self.initialized = false;
136
137            // TODO: this unintuitive flow is a consequence of isolation-specific code
138            // being conditionally compiled for each architecture
139            //
140            // This should be revisited as more isolation-specific flows are added to
141            // the shim
142            cfg_select! {
143                target_arch = "x86_64" => {
144                    if self.tdx_io_page.is_some() {
145                        self.uninitialize_tdx()
146                    } else {
147                        crate::arch::hypercall::uninitialize();
148                    }
149                }
150                _ => {
151                    crate::arch::hypercall::uninitialize();
152                }
153            }
154        }
155    }
156
157    /// Returns the environment's VTL.
158    pub fn vtl(&mut self) -> Vtl {
159        assert!(self.initialized);
160        self.vtl
161    }
162
163    /// Makes a hypercall.
164    /// rep_count is Some for rep hypercalls
165    fn dispatch_hvcall(
166        &mut self,
167        code: hvdef::HypercallCode,
168        rep_count: Option<usize>,
169    ) -> hvdef::hypercall::HypercallOutput {
170        assert!(self.initialized);
171
172        let control = hvdef::hypercall::Control::new()
173            .with_code(code.0)
174            .with_rep_count(rep_count.unwrap_or_default());
175
176        #[cfg(target_arch = "x86_64")]
177        if let Some(tdx_io_page) = &self.tdx_io_page {
178            return invoke_tdcall_hypercall(control, tdx_io_page);
179        }
180        // SAFETY: Invoking hypercall per TLFS spec
181        unsafe {
182            invoke_hypercall(
183                control,
184                Self::input_page().address(),
185                Self::output_page().address(),
186            )
187        }
188    }
189
190    /// Hypercall for setting a register to a value.
191    pub fn set_register(
192        &mut self,
193        name: hvdef::HvRegisterName,
194        value: hvdef::HvRegisterValue,
195    ) -> Result<(), hvdef::HvError> {
196        const HEADER_SIZE: usize = size_of::<hvdef::hypercall::GetSetVpRegisters>();
197
198        let header = hvdef::hypercall::GetSetVpRegisters {
199            partition_id: hvdef::HV_PARTITION_ID_SELF,
200            vp_index: hvdef::HV_VP_INDEX_SELF,
201            target_vtl: HvInputVtl::CURRENT_VTL,
202            rsvd: [0; 3],
203        };
204
205        // PANIC: Infallable, since the hypercall header is less than the size of a page
206        header
207            .write_to_prefix(Self::input_page().buffer.as_mut_slice())
208            .unwrap();
209
210        let reg = hvdef::hypercall::HvRegisterAssoc {
211            name,
212            pad: Default::default(),
213            value,
214        };
215
216        // PANIC: Infallable, since the hypercall parameter (plus size of header above) is less than the size of a page
217        reg.write_to_prefix(&mut Self::input_page().buffer[HEADER_SIZE..])
218            .unwrap();
219
220        let output = self.dispatch_hvcall(hvdef::HypercallCode::HvCallSetVpRegisters, Some(1));
221
222        output.result()
223    }
224
225    /// Hypercall for setting a register to a value.
226    pub fn get_register(
227        &mut self,
228        name: hvdef::HvRegisterName,
229    ) -> Result<hvdef::HvRegisterValue, hvdef::HvError> {
230        const HEADER_SIZE: usize = size_of::<hvdef::hypercall::GetSetVpRegisters>();
231
232        let header = hvdef::hypercall::GetSetVpRegisters {
233            partition_id: hvdef::HV_PARTITION_ID_SELF,
234            vp_index: hvdef::HV_VP_INDEX_SELF,
235            target_vtl: HvInputVtl::CURRENT_VTL,
236            rsvd: [0; 3],
237        };
238
239        // PANIC: Infallable, since the hypercall header is less than the size of a page
240        header
241            .write_to_prefix(Self::input_page().buffer.as_mut_slice())
242            .unwrap();
243        // PANIC: Infallable, since the hypercall parameter (plus size of header above) is less than the size of a page
244        name.write_to_prefix(&mut Self::input_page().buffer[HEADER_SIZE..])
245            .unwrap();
246
247        let output = self.dispatch_hvcall(hvdef::HypercallCode::HvCallGetVpRegisters, Some(1));
248        output.result()?;
249        let value = hvdef::HvRegisterValue::read_from_prefix(&Self::output_page().buffer)
250            .unwrap()
251            .0; // TODO: zerocopy: use-rest-of-range (https://github.com/microsoft/openvmm/issues/759)
252        Ok(value)
253    }
254
255    /// Hypercall to apply vtl protections to the pages from address start to end
256    #[cfg_attr(target_arch = "aarch64", expect(dead_code))]
257    pub fn apply_vtl2_protections(&mut self, range: MemoryRange) -> Result<(), hvdef::HvError> {
258        const HEADER_SIZE: usize = size_of::<hvdef::hypercall::ModifyVtlProtectionMask>();
259        const MAX_INPUT_ELEMENTS: usize = (HV_PAGE_SIZE as usize - HEADER_SIZE) / size_of::<u64>();
260
261        let header = hvdef::hypercall::ModifyVtlProtectionMask {
262            partition_id: hvdef::HV_PARTITION_ID_SELF,
263            map_flags: hvdef::HV_MAP_GPA_PERMISSIONS_NONE,
264            target_vtl: HvInputVtl::CURRENT_VTL,
265            reserved: [0; 3],
266        };
267
268        let mut current_page = range.start_4k_gpn();
269        while current_page < range.end_4k_gpn() {
270            let remaining_pages = range.end_4k_gpn() - current_page;
271            let count = remaining_pages.min(MAX_INPUT_ELEMENTS as u64) as usize;
272
273            // PANIC: Infallable, since the hypercall header is less than the size of a page
274            header
275                .write_to_prefix(Self::input_page().buffer.as_mut_slice())
276                .unwrap();
277
278            let mut input_offset = HEADER_SIZE;
279            for i in 0..count {
280                let page_num = current_page + i as u64;
281                // PANIC: Infallable, since the hypercall parameter (plus size of header above) is less than the size of a page
282                page_num
283                    .write_to_prefix(&mut Self::input_page().buffer[input_offset..])
284                    .unwrap();
285                input_offset += size_of::<u64>();
286            }
287
288            let output = self.dispatch_hvcall(
289                hvdef::HypercallCode::HvCallModifyVtlProtectionMask,
290                Some(count),
291            );
292
293            output.result()?;
294
295            current_page += count as u64;
296        }
297
298        Ok(())
299    }
300
301    /// Hypercall to enable VP VTL
302    #[cfg(target_arch = "aarch64")]
303    pub fn enable_vp_vtl(&mut self, vp_index: u32) -> Result<(), hvdef::HvError> {
304        let header = hvdef::hypercall::EnableVpVtlArm64 {
305            partition_id: hvdef::HV_PARTITION_ID_SELF,
306            vp_index,
307            // The VTL value here is just a u8 and not the otherwise usual
308            // HvInputVtl value.
309            target_vtl: Vtl::Vtl2.into(),
310            reserved: [0; 3],
311            vp_vtl_context: zerocopy::FromZeros::new_zeroed(),
312        };
313
314        // PANIC: Infallable, since the hypercall header is less than the size of a page
315        header
316            .write_to_prefix(Self::input_page().buffer.as_mut_slice())
317            .unwrap();
318
319        let output = self.dispatch_hvcall(hvdef::HypercallCode::HvCallEnableVpVtl, None);
320        match output.result() {
321            Ok(()) | Err(hvdef::HvError::VtlAlreadyEnabled) => Ok(()),
322            err => err,
323        }
324    }
325
326    /// Hypercall to accept vtl2 pages from address start to end with VTL 2
327    /// protections and no host visibility
328    #[cfg_attr(target_arch = "aarch64", expect(dead_code))]
329    pub fn accept_vtl2_pages(
330        &mut self,
331        range: MemoryRange,
332        memory_type: hvdef::hypercall::AcceptMemoryType,
333    ) -> Result<(), hvdef::HvError> {
334        const HEADER_SIZE: usize = size_of::<hvdef::hypercall::AcceptGpaPages>();
335        const MAX_INPUT_ELEMENTS: usize = (HV_PAGE_SIZE as usize - HEADER_SIZE) / size_of::<u64>();
336
337        let mut current_page = range.start_4k_gpn();
338        while current_page < range.end_4k_gpn() {
339            let header = hvdef::hypercall::AcceptGpaPages {
340                partition_id: hvdef::HV_PARTITION_ID_SELF,
341                page_attributes: hvdef::hypercall::AcceptPagesAttributes::new()
342                    .with_memory_type(memory_type.0)
343                    .with_host_visibility(hvdef::hypercall::HostVisibilityType::PRIVATE) // no host visibility
344                    .with_vtl_set(1 << 2), // applies vtl permissions for vtl 2
345                vtl_permission_set: hvdef::hypercall::VtlPermissionSet {
346                    vtl_permission_from_1: [0; hvdef::hypercall::HV_VTL_PERMISSION_SET_SIZE],
347                },
348                gpa_page_base: current_page,
349            };
350
351            let remaining_pages = range.end_4k_gpn() - current_page;
352            let count = remaining_pages.min(MAX_INPUT_ELEMENTS as u64) as usize;
353
354            // PANIC: Infallable, since the hypercall header is less than the size of a page
355            header
356                .write_to_prefix(Self::input_page().buffer.as_mut_slice())
357                .unwrap();
358
359            let output =
360                self.dispatch_hvcall(hvdef::HypercallCode::HvCallAcceptGpaPages, Some(count));
361
362            output.result()?;
363
364            current_page += count as u64;
365        }
366
367        Ok(())
368    }
369
370    /// Get the corresponding VP indices from a list of VP hardware IDs (APIC
371    /// IDs on x64, MPIDR on ARM64).
372    ///
373    /// This always queries VTL0, since the hardware IDs are the same across the
374    /// VTLs in practice, and the hypercall only succeeds for VTL2 once VTL2 has
375    /// been enabled (which it might not be at this point).
376    pub fn get_vp_index_from_hw_id<const N: usize>(
377        &mut self,
378        hw_ids: &[HwId],
379        output: &mut ArrayVec<u32, N>,
380    ) -> Result<(), hvdef::HvError> {
381        let header = hvdef::hypercall::GetVpIndexFromApicId {
382            partition_id: hvdef::HV_PARTITION_ID_SELF,
383            target_vtl: 0,
384            reserved: [0; 7],
385        };
386
387        // Split the call up to avoid exceeding the hypercall input/output size limits.
388        const HEADER_SIZE: usize = size_of::<hvdef::hypercall::GetVpIndexFromApicId>();
389        const MAX_PER_CALL: usize = (HV_PAGE_SIZE as usize - HEADER_SIZE) / size_of::<HwId>();
390
391        for hw_ids in hw_ids.chunks(MAX_PER_CALL) {
392            // PANIC: Infallable, since the hypercall header is less than the size of a page
393            header
394                .write_to_prefix(Self::input_page().buffer.as_mut_slice())
395                .unwrap();
396            // PANIC: Infallable, since the hypercall parameters are chunked to be less
397            // than the remaining size (after the header) of the input page.
398            hw_ids
399                .write_to_prefix(&mut Self::input_page().buffer[HEADER_SIZE..])
400                .unwrap();
401
402            // SAFETY: The input header and rep slice are the correct types for this hypercall.
403            //         The hypercall output is validated right after the hypercall is issued.
404            let r = self.dispatch_hvcall(
405                hvdef::HypercallCode::HvCallGetVpIndexFromApicId,
406                Some(hw_ids.len()),
407            );
408
409            let n = r.elements_processed();
410            output.extend(
411                <[u32]>::ref_from_bytes(&Self::output_page().buffer[..n * 4])
412                    .unwrap()
413                    .iter()
414                    .copied(),
415            );
416            r.result()?;
417            assert_eq!(n, hw_ids.len());
418        }
419
420        Ok(())
421    }
422}
423
424// TDX specific hypercall methods
425#[cfg(target_arch = "x86_64")]
426impl HvCall {
427    /// Initialize HvCall for use in a TDX CVM
428    pub fn initialize_tdx(&mut self, tdx_io_page: TdxHypercallPage) {
429        assert!(!self.initialized);
430        self.initialized = true;
431        self.vtl = Vtl::Vtl2;
432        self.tdx_io_page = Some(tdx_io_page);
433
434        // TODO: revisit os id value. For now, use 1 (which is what UEFI does)
435        let guest_os_id = hvdef::hypercall::HvGuestOsMicrosoft::new().with_os_id(1);
436
437        crate::arch::tdx::initialize_hypercalls(
438            guest_os_id.into(),
439            self.tdx_io_page.as_ref().unwrap(),
440        );
441    }
442
443    /// Uninitialize HvCall for TDX, unshare IO pages with the hypervisor
444    pub fn uninitialize_tdx(&mut self) {
445        crate::arch::tdx::uninitialize_hypercalls(
446            self.tdx_io_page
447                .take()
448                .expect("an initialized instance of HvCall on TDX must have an io page"),
449        );
450    }
451
452    /// Hypercall to enable VTL2 on a TDX VP
453    pub fn tdx_enable_vp_vtl2(&mut self, vp_index: u32) -> Result<(), hvdef::HvError> {
454        let header = hvdef::hypercall::EnableVpVtlX64 {
455            partition_id: hvdef::HV_PARTITION_ID_SELF,
456            vp_index,
457            // The VTL value here is just a u8 and not the otherwise usual
458            // HvInputVtl value.
459            target_vtl: Vtl::Vtl2.into(),
460            reserved: [0; 3],
461            // Context must be zeroed for the VP VTL startup hypercall on TDX
462            vp_vtl_context: zerocopy::FromZeros::new_zeroed(),
463        };
464        assert!(self.initialized);
465
466        let input_page_addr = self
467            .tdx_io_page
468            .as_ref()
469            .expect("an initialized instance of HvCall on TDX must have an io page")
470            .input();
471        // SAFETY: the input page passed into the initialization of HvCall is a
472        // free buffer that is not concurrently accessed because openhcl_boot
473        // is single threaded
474        let input_page = unsafe { &mut *(input_page_addr as *mut [u8; 4096]) };
475        header
476            .write_to_prefix(input_page)
477            .expect("unable to write to hypercall page");
478
479        let output = self.dispatch_hvcall(hvdef::HypercallCode::HvCallEnableVpVtl, None);
480        match output.result() {
481            Ok(()) | Err(hvdef::HvError::VtlAlreadyEnabled) => Ok(()),
482            err => err,
483        }
484    }
485
486    /// Hypercall to start VP on TDX
487    pub fn tdx_start_vp(&mut self, vp_index: u32) -> Result<(), hvdef::HvError> {
488        let header = StartVirtualProcessorX64 {
489            partition_id: hvdef::HV_PARTITION_ID_SELF,
490            vp_index,
491            target_vtl: Vtl::Vtl2.into(),
492            rsvd0: 0,
493            rsvd1: 0,
494            // Context must be zeroed for the AP startup hypercall on TDX
495            vp_context: zerocopy::FromZeros::new_zeroed(),
496        };
497        assert!(self.initialized);
498
499        let input_page_addr = self
500            .tdx_io_page
501            .as_ref()
502            .expect("an initialized instance of HvCall on TDX must have an io page")
503            .input();
504        // SAFETY: the input page passed into the initialization of HvCall is a
505        // free buffer that is not concurrently accessed
506        let input_page = unsafe { &mut *(input_page_addr as *mut [u8; 4096]) };
507        header
508            .write_to_prefix(input_page)
509            .expect("unable to write to hypercall page");
510
511        let output = self.dispatch_hvcall(hvdef::HypercallCode::HvCallStartVirtualProcessor, None);
512        match output.result() {
513            Ok(()) | Err(hvdef::HvError::VtlAlreadyEnabled) => Ok(()),
514            err => err,
515        }
516    }
517}
518
519/// The "hardware ID" used for [`HvCall::get_vp_index_from_hw_id`]. This is the
520/// APIC ID on x64.
521#[cfg(target_arch = "x86_64")]
522pub type HwId = u32;
523
524/// The "hardware ID" used for [`HvCall::get_vp_index_from_hw_id`]. This is the
525/// MPIDR on ARM64.
526#[cfg(target_arch = "aarch64")]
527pub type HwId = u64;