diff --git a/.gitignore b/.gitignore index 841616ae4..dec76e560 100644 --- a/.gitignore +++ b/.gitignore @@ -468,3 +468,6 @@ hyperlight_guest.h .mono !.gitkeep + +# gdb +.gdbinit diff --git a/Justfile b/Justfile index 6d28c861e..f9548b564 100644 --- a/Justfile +++ b/Justfile @@ -62,6 +62,22 @@ clean-rust: # Note: most testing recipes take an optional "features" comma separated list argument. If provided, these will be passed to cargo as **THE ONLY FEATURES**, i.e. default features will be disabled. +# convenience recipe to run all tests with the given target and features (similar to CI) +test-like-ci config=default-target hypervisor="kvm": + @# with default features + just test {{config}} {{ if hypervisor == "mshv3" {"mshv3"} else {""} }} + + @# with only one driver enabled + seccomp + inprocess + just test {{config}} inprocess,seccomp,{{ if hypervisor == "mshv" {"mshv2"} else if hypervisor == "mshv3" {"mshv3"} else {"kvm"} }} + + @# make sure certain cargo features compile + cargo check -p hyperlight-host --features crashdump + cargo check -p hyperlight-host --features print_debug + cargo check -p hyperlight-host --features gdb + + @# without any driver (should fail to compile) + just test-compilation-fail {{config}} + # runs all tests test target=default-target features="": (test-unit target features) (test-isolated target features) (test-integration "rust" target features) (test-integration "c" target features) (test-seccomp target features) diff --git a/docs/paging-development-notes.md b/docs/paging-development-notes.md index c025dd2ee..ae63f1a18 100644 --- a/docs/paging-development-notes.md +++ b/docs/paging-development-notes.md @@ -9,7 +9,7 @@ Hyperlight uses paging, which means the all addresses inside a Hyperlight VM are ## Host-to-Guest memory mapping -Into each Hyperlight VM, memory from the host is mapped into the VM as physical memory. The physical memory inside the VM starts at address `0x200_000` and extends linearly to however much memory was mapped into the VM (depends on various parameters). +Into each Hyperlight VM, memory from the host is mapped into the VM as physical memory. The physical memory inside the VM starts at address `0x0` and extends linearly to however much memory was mapped into the VM (depends on various parameters). ## Page table setup @@ -17,27 +17,27 @@ The following page table structs are set up in memory before running a Hyperligh ### PML4 (Page Map Level 4) Table -The PML4 table is located at physical address specified in CR3. In Hyperlight we set `CR3=0x200_000`, which means the PML4 table is located at physical address `0x200_000`. The PML4 table comprises 512 64-bit entries. +The PML4 table is located at physical address specified in CR3. In Hyperlight we set `CR3=0x0`, which means the PML4 table is located at physical address `0x0`. The PML4 table comprises 512 64-bit entries. -In Hyperlight, we only initialize the first entry (at address `0x200_000`), with value `0x201_000`, implying that we only have a single PDPT. +In Hyperlight, we only initialize the first entry (at address `0x0`), with value `0x1_000`, implying that we only have a single PDPT. ### PDPT (Page-directory-pointer Table) -The first and only PDPT is located at physical address `0x201_000`. The PDPT comprises 512 64-bit entries. In Hyperlight, we only initialize the first entry of the PDPT (at address `0x201_000`), with the value `0x202_000`, implying that we only have a single PD. +The first and only PDPT is located at physical address `0x1_000`. The PDPT comprises 512 64-bit entries. In Hyperlight, we only initialize the first entry of the PDPT (at address `0x1_000`), with the value `0x2_000`, implying that we only have a single PD. ### PD (Page Directory) -The first and only PD is located at physical address `0x202_000`. The PD comprises 512 64-bit entries, each entry `i` is set to the value `(i * 0x1000) + 0x203_000`. Thus, the first entry is `0x203_000`, the second entry is `0x204_000` and so on. +The first and only PD is located at physical address `0x2_000`. The PD comprises 512 64-bit entries, each entry `i` is set to the value `(i * 0x1000) + 0x3_000`. Thus, the first entry is `0x3_000`, the second entry is `0x4_000` and so on. ### PT (Page Table) -The page tables start at physical address `0x203_000`. Each page table has 512 64-bit entries. Each entry is set to the value `p << 21|i << 12` where `p` is the page table number and `i` is the index of the entry in the page table. Thus, the first entry of the first page table is `0x000_000`, the second entry is `0x000_000 + 0x1000`, and so on. The first entry of the second page table is `0x200_000 + 0x1000`, the second entry is `0x200_000 + 0x2000`, and so on. Enough page tables are created to cover the size of memory mapped into the VM. +The page tables start at physical address `0x3_000`. Each page table has 512 64-bit entries. Each entry is set to the value `p << 21|i << 12` where `p` is the page table number and `i` is the index of the entry in the page table. Thus, the first entry of the first page table is `0x000_000`, the second entry is `0x000_000 + 0x1000`, and so on. The first entry of the second page table is `0x200_000 + 0x1000`, the second entry is `0x200_000 + 0x2000`, and so on. Enough page tables are created to cover the size of memory mapped into the VM. ## Address Translation Given a 64-bit virtual address X, the corresponding physical address is obtained as follows: -1. PML4 table's physical address is located using CR3 (CR3 is `0x200_000`). +1. PML4 table's physical address is located using CR3 (CR3 is `0x0`). 2. Bits 47:39 of X are used to index into PML4, giving us the address of the PDPT. 3. Bits 38:30 of X are used to index into PDPT, giving us the address of the PD. 4. Bits 29:21 of X are used to index into PD, giving us the address of the PT. @@ -63,7 +63,7 @@ In addition to providing addresses, page table entries also contain access flags PML4E, PDPTE, and PD Entries have the present flag set to 1, and the rest of the flags are not set. -PTE Entries all have the present flag set to 1, apart from those for the address range `0x000_000` to `0x1FF_000` which have the present flag set to 0 as we do not map memory below physical address `0x200_000`. +PTE Entries all have the present flag set to 1. In addition, the following flags are set according to the type of memory being mapped: diff --git a/src/hyperlight_host/src/lib.rs b/src/hyperlight_host/src/lib.rs index 4716eb00c..63d9b1c06 100644 --- a/src/hyperlight_host/src/lib.rs +++ b/src/hyperlight_host/src/lib.rs @@ -57,13 +57,13 @@ pub mod hypervisor; /// /// Virtual Address /// -/// 0x200000 PML4 -/// 0x201000 PDPT -/// 0x202000 PD -/// 0x203000 The guest PE code (When the code has been loaded using LoadLibrary to debug the guest this will not be +/// 0x0000 PML4 +/// 0x1000 PDPT +/// 0x2000 PD +/// 0x3000 The guest PE code (When the code has been loaded using LoadLibrary to debug the guest this will not be /// present and code length will be zero; /// -/// The pointer passed to the Entrypoint in the Guest application is the 0x200000 + size of page table + size of code, +/// The pointer passed to the Entrypoint in the Guest application is the ize of page table + size of code, /// at this address structs below are laid out in this order pub mod mem; /// Metric definitions and helpers diff --git a/src/hyperlight_host/src/mem/layout.rs b/src/hyperlight_host/src/mem/layout.rs index 3a13879c4..8a37257b2 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -64,13 +64,13 @@ use crate::{log_then_return, new_error, Result}; // | Guest Code | // +-------------------------------------------+ // | PT | -// +-------------------------------------------+ 0x203_000 +// +-------------------------------------------+ 0x3_000 // | PD | -// +-------------------------------------------+ 0x202_000 +// +-------------------------------------------+ 0x2_000 // | PDPT | -// +-------------------------------------------+ 0x201_000 +// +-------------------------------------------+ 0x1_000 // | PML4 | -// +-------------------------------------------+ 0x200_000 +// +-------------------------------------------+ 0x0_000 // | ⋮ | // | Unmapped | // | ⋮ | @@ -308,7 +308,7 @@ impl SandboxMemoryLayout { const MAX_MEMORY_SIZE: usize = 0x40000000 - Self::BASE_ADDRESS; /// The base address of the sandbox's memory. - pub(crate) const BASE_ADDRESS: usize = 0x0200000; + pub(crate) const BASE_ADDRESS: usize = 0x0; // the offset into a sandbox's input/output buffer where the stack starts const STACK_POINTER_SIZE_BYTES: u64 = 8; @@ -699,12 +699,10 @@ impl SandboxMemoryLayout { // The size of a single table is 4K, we can map up to 1GB total memory which requires 1 PML4, 1 PDPT, 1 PD and 512 PTs // but we only need enough PTs to map the memory we are using. (In other words we only need 512 PTs to map the memory if the memory size is 1GB) // - // Because we always start the physical address space at 0x200_000 - // we can calculate the amount of memory needed for the PTs by calculating how much memory is needed for the sandbox configuration in total, - // then add 0x200_000 to that (as we start at 0x200_000), + // We can calculate the amount of memory needed for the PTs by calculating how much memory is needed for the sandbox configuration in total, // and then add 3 * 4K (for the PML4, PDPT and PD) to that, // then add 2MB to that (the maximum size of memory required for the PTs themselves is 2MB when we map 1GB of memory in 4K pages), - // then divide that by 0x200_000 (as we can map 2MB in each PT) and then round the result up by 1 . + // then divide that by 0x200_000 (as we can map 2MB in each PT). // This will give us the total size of the PTs required for the sandbox to which we can add the size of the PML4, PDPT and PD. #[instrument(skip_all, parent = Span::current(), level= "Trace")] fn get_total_page_table_size( @@ -741,7 +739,6 @@ impl SandboxMemoryLayout { let num_pages: usize = ((total_mapped_memory_size + AMOUNT_OF_MEMORY_PER_PT - 1) / AMOUNT_OF_MEMORY_PER_PT) - + 1 // Round up + 3; // PML4, PDPT, PD num_pages * PAGE_SIZE_USIZE @@ -1077,7 +1074,7 @@ impl SandboxMemoryLayout { macro_rules! get_address { ($something:ident) => { paste! { - if guest_offset == 0 { + if run_inprocess { let offset = self.[<$something _offset>]; let calculated_addr = shared_mem.calculate_address(offset)?; u64::try_from(calculated_addr)? diff --git a/src/hyperlight_host/src/mem/mgr.rs b/src/hyperlight_host/src/mem/mgr.rs index 2c4f38e83..934af557c 100644 --- a/src/hyperlight_host/src/mem/mgr.rs +++ b/src/hyperlight_host/src/mem/mgr.rs @@ -134,7 +134,6 @@ where mem_size: u64, regions: &mut [MemoryRegion], ) -> Result { - // Add 0x200000 because that's the start of mapped memory // For MSVC, move rsp down by 0x28. This gives the called 'main' // function the appearance that rsp was 16 byte aligned before // the 'call' that calls main (note we don't really have a return value @@ -177,60 +176,50 @@ where // We only need to create enough PTEs to map the amount of memory we have // We need one PT for every 2MB of memory that is mapped // We can use the memory size to calculate the number of PTs we need - // We round up mem_size/2MB and then we need to add 1 as we start our memory mapping at 0x200000 + // We round up mem_size/2MB let mem_size = usize::try_from(mem_size)?; let num_pages: usize = - ((mem_size + AMOUNT_OF_MEMORY_PER_PT - 1) / AMOUNT_OF_MEMORY_PER_PT) + 1; + (mem_size + AMOUNT_OF_MEMORY_PER_PT - 1) / AMOUNT_OF_MEMORY_PER_PT; // Create num_pages PT with 512 PTEs for p in 0..num_pages { for i in 0..512 { let offset = SandboxMemoryLayout::PT_OFFSET + (p * 4096) + (i * 8); // Each PTE maps a 4KB page - let val_to_write = if p == 0 { - (p << 21) as u64 | (i << 12) as u64 - } else { - let flags = match Self::get_page_flags(p, i, regions) { - Ok(region_type) => match region_type { - // TODO: We parse and load the exe according to its sections and then - // have the correct flags set rather than just marking the entire binary as executable - MemoryRegionType::Code => PAGE_PRESENT | PAGE_RW | PAGE_USER, - MemoryRegionType::Stack => { - PAGE_PRESENT | PAGE_RW | PAGE_USER | PAGE_NX - } - #[cfg(feature = "executable_heap")] - MemoryRegionType::Heap => PAGE_PRESENT | PAGE_RW | PAGE_USER, - #[cfg(not(feature = "executable_heap"))] - MemoryRegionType::Heap => { - PAGE_PRESENT | PAGE_RW | PAGE_USER | PAGE_NX - } - // The guard page is marked RW and User so that if it gets written to we can detect it in the host - // If/When we implement an interrupt handler for page faults in the guest then we can remove this access and handle things properly there - MemoryRegionType::GuardPage => { - PAGE_PRESENT | PAGE_RW | PAGE_USER | PAGE_NX - } - MemoryRegionType::InputData => PAGE_PRESENT | PAGE_RW | PAGE_NX, - MemoryRegionType::OutputData => PAGE_PRESENT | PAGE_RW | PAGE_NX, - MemoryRegionType::Peb => PAGE_PRESENT | PAGE_RW | PAGE_NX, - // Host Function Definitions are readonly in the guest - MemoryRegionType::HostFunctionDefinitions => PAGE_PRESENT | PAGE_NX, - MemoryRegionType::PanicContext => PAGE_PRESENT | PAGE_RW | PAGE_NX, - MemoryRegionType::GuestErrorData => { - PAGE_PRESENT | PAGE_RW | PAGE_NX - } - // Host Exception Data are readonly in the guest - MemoryRegionType::HostExceptionData => PAGE_PRESENT | PAGE_NX, - MemoryRegionType::PageTables => PAGE_PRESENT | PAGE_RW | PAGE_NX, - MemoryRegionType::KernelStack => PAGE_PRESENT | PAGE_RW | PAGE_NX, - MemoryRegionType::BootStack => PAGE_PRESENT | PAGE_RW | PAGE_NX, - }, - // If there is an error then the address isn't mapped so mark it as not present - Err(_) => 0, - }; - ((p << 21) as u64 | (i << 12) as u64) | flags + let flags = match Self::get_page_flags(p, i, regions) { + Ok(region_type) => match region_type { + // TODO: We parse and load the exe according to its sections and then + // have the correct flags set rather than just marking the entire binary as executable + MemoryRegionType::Code => PAGE_PRESENT | PAGE_RW | PAGE_USER, + MemoryRegionType::Stack => PAGE_PRESENT | PAGE_RW | PAGE_USER | PAGE_NX, + #[cfg(feature = "executable_heap")] + MemoryRegionType::Heap => PAGE_PRESENT | PAGE_RW | PAGE_USER, + #[cfg(not(feature = "executable_heap"))] + MemoryRegionType::Heap => PAGE_PRESENT | PAGE_RW | PAGE_USER | PAGE_NX, + // The guard page is marked RW and User so that if it gets written to we can detect it in the host + // If/When we implement an interrupt handler for page faults in the guest then we can remove this access and handle things properly there + MemoryRegionType::GuardPage => { + PAGE_PRESENT | PAGE_RW | PAGE_USER | PAGE_NX + } + MemoryRegionType::InputData => PAGE_PRESENT | PAGE_RW | PAGE_NX, + MemoryRegionType::OutputData => PAGE_PRESENT | PAGE_RW | PAGE_NX, + MemoryRegionType::Peb => PAGE_PRESENT | PAGE_RW | PAGE_NX, + // Host Function Definitions are readonly in the guest + MemoryRegionType::HostFunctionDefinitions => PAGE_PRESENT | PAGE_NX, + MemoryRegionType::PanicContext => PAGE_PRESENT | PAGE_RW | PAGE_NX, + MemoryRegionType::GuestErrorData => PAGE_PRESENT | PAGE_RW | PAGE_NX, + // Host Exception Data are readonly in the guest + MemoryRegionType::HostExceptionData => PAGE_PRESENT | PAGE_NX, + MemoryRegionType::PageTables => PAGE_PRESENT | PAGE_RW | PAGE_NX, + MemoryRegionType::KernelStack => PAGE_PRESENT | PAGE_RW | PAGE_NX, + MemoryRegionType::BootStack => PAGE_PRESENT | PAGE_RW | PAGE_NX, + }, + // If there is an error then the address isn't mapped so mark it as not present + Err(_) => 0, }; + let val_to_write = ((p << 21) as u64 | (i << 12) as u64) | flags; shared_mem.write_u64(offset, val_to_write)?; } } diff --git a/src/hyperlight_host/src/mem/ptr.rs b/src/hyperlight_host/src/mem/ptr.rs index bacab051c..eef1dea39 100644 --- a/src/hyperlight_host/src/mem/ptr.rs +++ b/src/hyperlight_host/src/mem/ptr.rs @@ -241,13 +241,4 @@ mod tests { ); } } - - #[test] - fn ptr_fail() { - { - let raw_guest_ptr = RawPtr(SandboxMemoryLayout::BASE_ADDRESS as u64 - 1); - let guest_ptr = GuestPtr::try_from(raw_guest_ptr); - assert!(guest_ptr.is_err()); - } - } }