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