hyperlight_host/hypervisor/
mod.rs

1/*
2Copyright 2024 The Hyperlight Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17use log::LevelFilter;
18use tracing::{instrument, Span};
19
20use crate::error::HyperlightError::ExecutionCanceledByHost;
21use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags};
22use crate::metrics::METRIC_GUEST_CANCELLATION;
23use crate::{log_then_return, new_error, HyperlightError, Result};
24
25/// Util for handling x87 fpu state
26#[cfg(any(kvm, mshv, target_os = "windows"))]
27pub mod fpu;
28/// Handlers for Hypervisor custom logic
29pub mod handlers;
30/// HyperV-on-linux functionality
31#[cfg(mshv)]
32pub mod hyperv_linux;
33#[cfg(target_os = "windows")]
34/// Hyperv-on-windows functionality
35pub(crate) mod hyperv_windows;
36pub(crate) mod hypervisor_handler;
37
38/// GDB debugging support
39#[cfg(gdb)]
40mod gdb;
41
42#[cfg(kvm)]
43/// Functionality to manipulate KVM-based virtual machines
44pub mod kvm;
45#[cfg(target_os = "windows")]
46/// Hyperlight Surrogate Process
47pub(crate) mod surrogate_process;
48#[cfg(target_os = "windows")]
49/// Hyperlight Surrogate Process
50pub(crate) mod surrogate_process_manager;
51/// WindowsHypervisorPlatform utilities
52#[cfg(target_os = "windows")]
53pub(crate) mod windows_hypervisor_platform;
54/// Safe wrappers around windows types like `PSTR`
55#[cfg(target_os = "windows")]
56pub(crate) mod wrappers;
57
58#[cfg(crashdump)]
59pub(crate) mod crashdump;
60
61use std::fmt::Debug;
62use std::str::FromStr;
63use std::sync::{Arc, Mutex};
64
65#[cfg(gdb)]
66use gdb::VcpuStopReason;
67
68#[cfg(gdb)]
69use self::handlers::{DbgMemAccessHandlerCaller, DbgMemAccessHandlerWrapper};
70use self::handlers::{
71    MemAccessHandlerCaller, MemAccessHandlerWrapper, OutBHandlerCaller, OutBHandlerWrapper,
72};
73use crate::hypervisor::hypervisor_handler::HypervisorHandler;
74use crate::mem::ptr::RawPtr;
75
76pub(crate) const CR4_PAE: u64 = 1 << 5;
77pub(crate) const CR4_OSFXSR: u64 = 1 << 9;
78pub(crate) const CR4_OSXMMEXCPT: u64 = 1 << 10;
79pub(crate) const CR0_PE: u64 = 1;
80pub(crate) const CR0_MP: u64 = 1 << 1;
81pub(crate) const CR0_ET: u64 = 1 << 4;
82pub(crate) const CR0_NE: u64 = 1 << 5;
83pub(crate) const CR0_WP: u64 = 1 << 16;
84pub(crate) const CR0_AM: u64 = 1 << 18;
85pub(crate) const CR0_PG: u64 = 1 << 31;
86pub(crate) const EFER_LME: u64 = 1 << 8;
87pub(crate) const EFER_LMA: u64 = 1 << 10;
88pub(crate) const EFER_SCE: u64 = 1;
89pub(crate) const EFER_NX: u64 = 1 << 11;
90
91/// These are the generic exit reasons that we can handle from a Hypervisor the Hypervisors run method is responsible for mapping from
92/// the hypervisor specific exit reasons to these generic ones
93pub enum HyperlightExit {
94    #[cfg(gdb)]
95    /// The vCPU has exited due to a debug event
96    Debug(VcpuStopReason),
97    /// The vCPU has halted
98    Halt(),
99    /// The vCPU has issued a write to the given port with the given value
100    IoOut(u16, Vec<u8>, u64, u64),
101    /// The vCPU has attempted to read or write from an unmapped address
102    Mmio(u64),
103    /// The vCPU tried to access memory but was missing the required permissions
104    AccessViolation(u64, MemoryRegionFlags, MemoryRegionFlags),
105    /// The vCPU execution has been cancelled
106    Cancelled(),
107    /// The vCPU has exited for a reason that is not handled by Hyperlight
108    Unknown(String),
109    /// The operation should be retried, for example this can happen on Linux where a call to run the CPU can return EAGAIN
110    Retry(),
111}
112
113/// A common set of hypervisor functionality
114///
115/// Note: a lot of these structures take in an `Option<HypervisorHandler>`.
116/// This is because, if we are coming from the C API, we don't have a HypervisorHandler and have
117/// to account for the fact the Hypervisor was set up beforehand.
118pub(crate) trait Hypervisor: Debug + Sync + Send {
119    /// Initialise the internally stored vCPU with the given PEB address and
120    /// random number seed, then run it until a HLT instruction.
121    #[allow(clippy::too_many_arguments)]
122    fn initialise(
123        &mut self,
124        peb_addr: RawPtr,
125        seed: u64,
126        page_size: u32,
127        outb_handle_fn: OutBHandlerWrapper,
128        mem_access_fn: MemAccessHandlerWrapper,
129        hv_handler: Option<HypervisorHandler>,
130        guest_max_log_level: Option<LevelFilter>,
131        #[cfg(gdb)] dbg_mem_access_fn: DbgMemAccessHandlerWrapper,
132    ) -> Result<()>;
133
134    /// Dispatch a call from the host to the guest using the given pointer
135    /// to the dispatch function _in the guest's address space_.
136    ///
137    /// Do this by setting the instruction pointer to `dispatch_func_addr`
138    /// and then running the execution loop until a halt instruction.
139    ///
140    /// Returns `Ok` if the call succeeded, and an `Err` if it failed
141    fn dispatch_call_from_host(
142        &mut self,
143        dispatch_func_addr: RawPtr,
144        outb_handle_fn: OutBHandlerWrapper,
145        mem_access_fn: MemAccessHandlerWrapper,
146        hv_handler: Option<HypervisorHandler>,
147        #[cfg(gdb)] dbg_mem_access_fn: DbgMemAccessHandlerWrapper,
148    ) -> Result<()>;
149
150    /// Handle an IO exit from the internally stored vCPU.
151    fn handle_io(
152        &mut self,
153        port: u16,
154        data: Vec<u8>,
155        rip: u64,
156        instruction_length: u64,
157        outb_handle_fn: OutBHandlerWrapper,
158    ) -> Result<()>;
159
160    /// Run the vCPU
161    fn run(&mut self) -> Result<HyperlightExit>;
162
163    /// Returns a Some(HyperlightExit::AccessViolation(..)) if the given gpa doesn't have
164    /// access its corresponding region. Returns None otherwise, or if the region is not found.
165    fn get_memory_access_violation(
166        &self,
167        gpa: usize,
168        mem_regions: &[MemoryRegion],
169        access_info: MemoryRegionFlags,
170    ) -> Option<HyperlightExit> {
171        // find the region containing the given gpa
172        let region = mem_regions
173            .iter()
174            .find(|region| region.guest_region.contains(&gpa));
175
176        if let Some(region) = region {
177            if !region.flags.contains(access_info)
178                || region.flags.contains(MemoryRegionFlags::STACK_GUARD)
179            {
180                return Some(HyperlightExit::AccessViolation(
181                    gpa as u64,
182                    access_info,
183                    region.flags,
184                ));
185            }
186        }
187        None
188    }
189
190    /// Get the logging level to pass to the guest entrypoint
191    fn get_max_log_level(&self) -> u32 {
192        // Check to see if the RUST_LOG environment variable is set
193        // and if so, parse it to get the log_level for hyperlight_guest
194        // if that is not set get the log level for the hyperlight_host
195
196        // This is done as the guest will produce logs based on the log level returned here
197        // producing those logs is expensive and we don't want to do it if the host is not
198        // going to process them
199
200        let val = std::env::var("RUST_LOG").unwrap_or_default();
201
202        let level = if val.contains("hyperlight_guest") {
203            val.split(',')
204                .find(|s| s.contains("hyperlight_guest"))
205                .unwrap_or("")
206                .split('=')
207                .nth(1)
208                .unwrap_or("")
209        } else if val.contains("hyperlight_host") {
210            val.split(',')
211                .find(|s| s.contains("hyperlight_host"))
212                .unwrap_or("")
213                .split('=')
214                .nth(1)
215                .unwrap_or("")
216        } else {
217            // look for a value string that does not contain "="
218            val.split(',').find(|s| !s.contains("=")).unwrap_or("")
219        };
220
221        log::info!("Determined guest log level: {}", level);
222        // Convert the log level string to a LevelFilter
223        // If no value is found, default to Error
224        LevelFilter::from_str(level).unwrap_or(LevelFilter::Error) as u32
225    }
226
227    /// get a mutable trait object from self
228    fn as_mut_hypervisor(&mut self) -> &mut dyn Hypervisor;
229
230    /// Get the partition handle for WHP
231    #[cfg(target_os = "windows")]
232    fn get_partition_handle(&self) -> windows::Win32::System::Hypervisor::WHV_PARTITION_HANDLE;
233
234    #[cfg(crashdump)]
235    fn get_memory_regions(&self) -> &[MemoryRegion];
236
237    #[cfg(gdb)]
238    /// handles the cases when the vCPU stops due to a Debug event
239    fn handle_debug(
240        &mut self,
241        _dbg_mem_access_fn: Arc<Mutex<dyn DbgMemAccessHandlerCaller>>,
242        _stop_reason: VcpuStopReason,
243    ) -> Result<()> {
244        unimplemented!()
245    }
246}
247
248/// A virtual CPU that can be run until an exit occurs
249pub struct VirtualCPU {}
250
251impl VirtualCPU {
252    /// Run the given hypervisor until a halt instruction is reached
253    #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
254    pub fn run(
255        hv: &mut dyn Hypervisor,
256        hv_handler: Option<HypervisorHandler>,
257        outb_handle_fn: Arc<Mutex<dyn OutBHandlerCaller>>,
258        mem_access_fn: Arc<Mutex<dyn MemAccessHandlerCaller>>,
259        #[cfg(gdb)] dbg_mem_access_fn: Arc<Mutex<dyn DbgMemAccessHandlerCaller>>,
260    ) -> Result<()> {
261        loop {
262            match hv.run() {
263                #[cfg(gdb)]
264                Ok(HyperlightExit::Debug(stop_reason)) => {
265                    if let Err(e) = hv.handle_debug(dbg_mem_access_fn.clone(), stop_reason) {
266                        log_then_return!(e);
267                    }
268                }
269
270                Ok(HyperlightExit::Halt()) => {
271                    break;
272                }
273                Ok(HyperlightExit::IoOut(port, data, rip, instruction_length)) => {
274                    hv.handle_io(port, data, rip, instruction_length, outb_handle_fn.clone())?
275                }
276                Ok(HyperlightExit::Mmio(addr)) => {
277                    #[cfg(crashdump)]
278                    crashdump::crashdump_to_tempfile(hv)?;
279
280                    mem_access_fn
281                        .clone()
282                        .try_lock()
283                        .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))?
284                        .call()?;
285
286                    log_then_return!("MMIO access address {:#x}", addr);
287                }
288                Ok(HyperlightExit::AccessViolation(addr, tried, region_permission)) => {
289                    #[cfg(crashdump)]
290                    crashdump::crashdump_to_tempfile(hv)?;
291
292                    if region_permission.intersects(MemoryRegionFlags::STACK_GUARD) {
293                        return Err(HyperlightError::StackOverflow());
294                    }
295                    log_then_return!(HyperlightError::MemoryAccessViolation(
296                        addr,
297                        tried,
298                        region_permission
299                    ));
300                }
301                Ok(HyperlightExit::Cancelled()) => {
302                    // Shutdown is returned when the host has cancelled execution
303                    // After termination, the main thread will re-initialize the VM
304                    if let Some(hvh) = hv_handler {
305                        // If hvh is None, then we are running from the C API, which doesn't use
306                        // the HypervisorHandler
307                        hvh.set_running(false);
308                        #[cfg(target_os = "linux")]
309                        hvh.set_run_cancelled(true);
310                    }
311                    metrics::counter!(METRIC_GUEST_CANCELLATION).increment(1);
312                    log_then_return!(ExecutionCanceledByHost());
313                }
314                Ok(HyperlightExit::Unknown(reason)) => {
315                    #[cfg(crashdump)]
316                    crashdump::crashdump_to_tempfile(hv)?;
317
318                    log_then_return!("Unexpected VM Exit {:?}", reason);
319                }
320                Ok(HyperlightExit::Retry()) => continue,
321                Err(e) => {
322                    #[cfg(crashdump)]
323                    crashdump::crashdump_to_tempfile(hv)?;
324
325                    return Err(e);
326                }
327            }
328        }
329
330        Ok(())
331    }
332}
333
334#[cfg(all(test, any(target_os = "windows", kvm)))]
335pub(crate) mod tests {
336    use std::path::Path;
337    use std::sync::{Arc, Mutex};
338    use std::time::Duration;
339
340    use hyperlight_testing::dummy_guest_as_string;
341
342    #[cfg(gdb)]
343    use super::handlers::DbgMemAccessHandlerWrapper;
344    use super::handlers::{MemAccessHandlerWrapper, OutBHandlerWrapper};
345    use crate::hypervisor::hypervisor_handler::{
346        HvHandlerConfig, HypervisorHandler, HypervisorHandlerAction,
347    };
348    use crate::mem::ptr::RawPtr;
349    use crate::sandbox::uninitialized::GuestBinary;
350    use crate::sandbox::{SandboxConfiguration, UninitializedSandbox};
351    use crate::{new_error, Result};
352
353    pub(crate) fn test_initialise(
354        outb_hdl: OutBHandlerWrapper,
355        mem_access_hdl: MemAccessHandlerWrapper,
356        #[cfg(gdb)] dbg_mem_access_fn: DbgMemAccessHandlerWrapper,
357    ) -> Result<()> {
358        let filename = dummy_guest_as_string().map_err(|e| new_error!("{}", e))?;
359        if !Path::new(&filename).exists() {
360            return Err(new_error!(
361                "test_initialise: file {} does not exist",
362                filename
363            ));
364        }
365
366        let sandbox = UninitializedSandbox::new(GuestBinary::FilePath(filename.clone()), None)?;
367        let (hshm, gshm) = sandbox.mgr.build();
368        drop(hshm);
369
370        let hv_handler_config = HvHandlerConfig {
371            outb_handler: outb_hdl,
372            mem_access_handler: mem_access_hdl,
373            #[cfg(gdb)]
374            dbg_mem_access_handler: dbg_mem_access_fn,
375            seed: 1234567890,
376            page_size: 4096,
377            peb_addr: RawPtr::from(0x230000),
378            dispatch_function_addr: Arc::new(Mutex::new(None)),
379            max_init_time: Duration::from_millis(
380                SandboxConfiguration::DEFAULT_MAX_INITIALIZATION_TIME as u64,
381            ),
382            max_exec_time: Duration::from_millis(
383                SandboxConfiguration::DEFAULT_MAX_EXECUTION_TIME as u64,
384            ),
385            max_wait_for_cancellation: Duration::from_millis(
386                SandboxConfiguration::DEFAULT_MAX_WAIT_FOR_CANCELLATION as u64,
387            ),
388            max_guest_log_level: None,
389        };
390
391        let mut hv_handler = HypervisorHandler::new(hv_handler_config);
392
393        // call initialise on the hypervisor implementation with specific values
394        // for PEB (process environment block) address, seed and page size.
395        //
396        // these values are not actually used, they're just checked inside
397        // the dummy guest, and if they don't match these values, the dummy
398        // guest issues a write to an invalid memory address, which in turn
399        // fails this test.
400        //
401        // in this test, we're not actually testing whether a guest can issue
402        // memory operations, call functions, etc... - we're just testing
403        // whether we can configure the shared memory region, load a binary
404        // into it, and run the CPU to completion (e.g., a HLT interrupt)
405
406        hv_handler.start_hypervisor_handler(
407            gshm,
408            #[cfg(gdb)]
409            None,
410        )?;
411
412        hv_handler.execute_hypervisor_handler_action(HypervisorHandlerAction::Initialise)
413    }
414}