Skip to content

Change host's memory setup to be minimally configured #297

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 24 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
09bb04e
[.gitignore] added .gdbinit to .gitignore
danbugs Mar 17, 2025
261a797
[docs] updates paging docs
danbugs Mar 19, 2025
5d75c61
[host/{mem/layout,sandbox/mem_mgr}] removes statically defined mem la…
danbugs Mar 19, 2025
e7183ce
[common,guest] added peb and input/output API to common library
danbugs Mar 19, 2025
c1c5781
[host/{mem/region,sandbox{config,builder,uninit}] introduces SandboxB…
danbugs Mar 19, 2025
45f2628
[guest,tests/rust_guests] change guest library to conform to new APIs…
danbugs Mar 19, 2025
9b80eaf
[host/mem/mgr] changes SandboxMemoryManager API to interact w/ CGM
danbugs Mar 19, 2025
39b4ddf
[host/*] change API usage (MemoryRegions, MemMgrWrapper, etc.) across…
danbugs Mar 19, 2025
d90ea82
[host/hypervisor/[*]] update drivers to use CGM
danbugs Mar 19, 2025
5ece18a
[*] clippy fix and fmt
danbugs Apr 11, 2025
4977791
[common,guest] moved outb functionality to hyperlight_common and adde…
danbugs Apr 14, 2025
cba4609
[common/input_output] making input_output stacks portable
danbugs Apr 14, 2025
0bd58d9
[guest,host,common] removed uneeded entrypoint arg
danbugs Apr 15, 2025
849ff55
[guest,host,common] brought back in-process driver
danbugs Apr 16, 2025
b310ebb
[host/hypervisor/hyperv_linux] brought back mshv driver
danbugs Apr 17, 2025
ea4a71a
[host/hypervisor/hyperv_windows] brought back whp driver
danbugs Apr 17, 2025
922b630
[host/{hypervisor/{*drivers,crashdump},sandbox/builder}] re-added cra…
danbugs Apr 17, 2025
58dd56f
[common,guest,host] modified HyperlightPEB API + added rsp mod in hosts
danbugs Apr 17, 2025
38452e0
[common,guest,host] refactored guest and host error data regions
danbugs Apr 19, 2025
6415498
[host,simpleguest] removed remaining references to HostPrint
danbugs Apr 19, 2025
832240d
[host/sandbox/{mem_access,uninit_evolve}] brought back mem_access_han…
danbugs Apr 20, 2025
642fcdc
[host/mem] removed tests in mem/mgr
danbugs Apr 21, 2025
04e8e96
[guest,host/leaked_outb] Fixed inprocess execution for Windows
danbugs Apr 21, 2025
815cb5b
tmp
danbugs Apr 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -468,3 +468,6 @@ hyperlight_guest.h
.mono

!.gitkeep

# gdb
.gdbinit
16 changes: 16 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
@@ -60,6 +60,22 @@ clean-rust:
### TESTING ####
################

# run full CI test matrix
test-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}}

# 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.

# runs all tests
38 changes: 20 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -48,8 +48,8 @@ fn main() -> hyperlight_host::Result<()> {
)?;

// Register a host function
fn sleep_5_secs() -> hyperlight_host::Result<()> {
thread::sleep(std::time::Duration::from_secs(5));
fn host_add() -> hyperlight_host::Result<()> {

Ok(())
}

@@ -92,41 +92,43 @@ use hyperlight_common::flatbuffer_wrappers::function_types::{
ParameterType, ParameterValue, ReturnType,
};
use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode;
use hyperlight_common::flatbuffer_wrappers::util::get_flatbuffer_result_from_int;

use hyperlight_guest::error::{HyperlightGuestError, Result};
use hyperlight_guest::guest_function_definition::GuestFunctionDefinition;
use hyperlight_guest::guest_function_register::register_function;
use hyperlight_guest::host_function_call::{
call_host_function, get_host_value_return_as_int,
};
use hyperlight_common::host_calling::{call_host_function, get_host_return_value};

fn print_output(function_call: &FunctionCall) -> Result<Vec<u8>> {
if let ParameterValue::String(message) = function_call.parameters.clone().unwrap()[0].clone() {
fn add(function_call: &FunctionCall) -> Result<Vec<u8>> {
if let (ParameterValue::Int(a), ParameterValue::Int(b)) = (
function_call.parameters.clone().unwrap()[0].clone(),
function_call.parameters.clone().unwrap()[1].clone(),
) {
call_host_function(
"HostPrint",
Some(Vec::from(&[ParameterValue::String(message.to_string())])),
"HostAdd",
Some(Vec::from(&[ParameterValue::Int(a), ParameterValue::Int(b)])),
ReturnType::Int,
)?;
let result = get_host_value_return_as_int()?;
Ok(get_flatbuffer_result_from_int(result))

let res = get_host_return_value::<i32>()?;

Ok(get_flatbuffer_result(res))
} else {
Err(HyperlightGuestError::new(
ErrorCode::GuestFunctionParameterTypeMismatch,
"Invalid parameters passed to simple_print_output".to_string(),
"Invalid parameters passed to add".to_string(),
))
}
}

#[no_mangle]
pub extern "C" fn hyperlight_main() {
let print_output_def = GuestFunctionDefinition::new(
"PrintOutput".to_string(),
Vec::from(&[ParameterType::String]),
let add_def = GuestFunctionDefinition::new(
"Add".to_string(),
Vec::from(&[ParameterType::Int, ParameterType::Int]),
ReturnType::Int,
print_output as i64,
add as usize,
);
register_function(print_output_def);
register_function(add_def);
}

#[no_mangle]
87 changes: 55 additions & 32 deletions docs/paging-development-notes.md
Original file line number Diff line number Diff line change
@@ -1,80 +1,103 @@
# Paging in Hyperlight

Hyperlight uses paging, which means the all addresses inside a Hyperlight VM are treated as virtual addresses by the processor. Specifically, Hyperlight uses (ordinary) 4-level paging. 4-level paging is used because we set the following control registers on logical cores inside a VM: `CR0.PG = 1, CR4.PAE = 1, IA32_EFER.LME = 1, and CR4.LA57 = 0`. A Hyperlight VM is limited to 1GB of addressable memory, see below for more details. These control register settings have the following effects:
Hyperlight uses paging, which means the all addresses inside a Hyperlight VM are
treated as virtual addresses by the processor. Specifically, Hyperlight uses
(ordinary) 4-level paging. 4-level paging is used because we set the following
control registers on logical cores inside a VM: `CR0.PG = 1, CR4.PAE = 1, IA32_EFER,
LME = 1, and CR4.LA57 = 0`. A Hyperlight VM is limited to 1GB of addressable memory,
see below for more details. These control register settings have the following
effects:

- `CR0.PG = 1`: Enables paging
- `CR4.PAE = 1`: Enables Physical Address Extension (PAE) mode (this is required for 4-level paging)
- `CR4.PAE = 1`: Enables Physical Address Extension (PAE) mode (this is required for
4-level paging)
- `IA32_EFER.LME = 1`: Enables Long Mode (64-bit mode)
- `CR4.LA57 = 0`: Makes sure 5-level paging is disabled

## 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

The following page table structs are set up in memory before running a Hyperlight VM (See [Access Flags](#access-flags) for details on access flags that are also set on each entry)
The following page table structs are set up in memory before running a Hyperlight VM
(See [Access Flags](#access-flags) for details on access flags that are also set on each entry)

### 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=pml4_address`. 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, 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:
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.
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.
5. Bits 20:12 of X are used to index into PT, giving us a base address of a 4K page.
6. Bits 11:0 of X are treated as an offset.
7. The final physical address is the base address + the offset.

However, because we have only one PDPT4E and only one PDPT4E, bits 47:30 must always be zero. Each PDE points to a PT, and because each PTE with index `p,i` (where p is the page table number of i is the entry within that page) has value `p << 21|i << 12`, the base address received in step 5 above is always just bits 29:12 of X itself. **As bits 11:0 are an offset this means that translating a virtual address to a physical address is essentially a NO-OP**.
However, because we have only one PDPT4E and only one PDPT4E, bits 47:30 must always
be zero. Each PDE points to a PT, and because each PTE with index `p,i` (where p i
the page table number of i is the entry within that page) has value `p << 21|i <<
12`, the base address received in step 5 above is always just bits 29:12 of X
itself. **As bits 11:0 are an offset this means that translating a virtual address
to a physical address is essentially a NO-OP**.

A diagram to describe how a linear (virtual) address is translated to physical address inside a Hyperlight VM:
A diagram to describe how a linear (virtual) address is translated to physical
address inside a Hyperlight VM:

![A diagram to describe how a linear (virtual) address is translated to physical](assets/linear-address-translation.png)

Diagram is taken from "The Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A: System Programming Guide"
Diagram is taken from "The Intel® 64 and IA-32 Architectures Software Developer’s
Manual, Volume 3A: System Programming Guide"

### Limitations

Since we only have 1 PML4E and only 1 PDPTE, bits 47:30 of a linear address must be zero. Thus, we have only 30 bits (bit 29:0) to work with, giving us access to (1 << 30) bytes of memory (1GB).
Since we only have 1 PML4E and only 1 PDPTE, bits 47:30 of a linear address must be
zero. Thus, we have only 30 bits (bit 29:0) to work with, giving us access to (1 <<
30) bytes of memory (1GB).

## Access Flags

In addition to providing addresses, page table entries also contain access flags that describe how memory can be accessed, and whether it is present or not. The following access flags are set on each entry:
In addition to providing addresses, page table entries also contain access flags
that describe how memory can be accessed, and whether it is present or not. The
following access flags are set on each entry:

PML4E, PDPTE, and PD Entries have the present flag set to 1, and the rest of the flags are not set.
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`.

In addition, the following flags are set according to the type of memory being mapped:

For `Host Function Definitions` and `Host Exception Data` the NX flag is set to 1 meaning that the memory is not executable in the guest and is not accessible to guest code (ring 3) and is also read only even in ring 0.

For `Input/Output Data`, `Page Table Data`, `PEB`, `PanicContext` and `GuestErrorData` the NX flag is set to 1 meaning that the memory is not executable in the guest and the RW flag is set to 1 meaning that the memory is read/write in ring 0, this means that this data is not accessible to guest code unless accessed via the Hyperlight Guest API (which will be in ring 0).

For `Code` the NX flag is not set meaning that the memory is executable in the guest and the RW flag is set to 1 meaning the data is read/write, as the user/supervisor flag is set then the memory is also read/write accessible to user code. (The code section contains both code and data, so it is marked as read/write. In a future update we will parse the layout of the code and set the access flags accordingly).

For `Stack` the NX flag is set to 1 meaning that the memory is not executable in the guest, the RW flag is set to 1 meaning the data is read/write, as the user/supervisor flag is set then the memory is also read/write accessible to user code.

For `Heap` the RW flag is set to 1 meaning the data is read/write, as the user/supervisor flag is set then the memory is also read/write accessible to user code. The NX flag is not set if the feature `executable_heap` is enabled, otherwise the NX flag is set to 1 meaning that the memory is not executable in the guest. The `executable_heap` feature is disabled by default. It is required to allow data in the heap to be executable to when guests dynamically load or generate code, e.g. `hyperlight-wasm` supports loading of AOT compiled WebAssembly modules, these are loaded dynamically by the Wasm runtime and end up in the heap, therefore for this scenario the `executable_heap` feature must be enabled. In a future update we will implement a mechanism to allow the guest to request memory to be executable at runtime via the Hyperlight Guest API.

For `Guard Pages` the NX flag is set to 1 meaning that the memory is not executable in the guest. The RW flag is set to 1 meaning the data is read/write, as the user/supervisor flag is set then the memory is also read/write accessible to user code. **Note that neither of these flags should really be set as the purpose of the guard pages is to cause a fault if accessed, however, as we deal with this fault in the host not in the guest we need to make the memory accessible to the guest, in a future update we will implement exception and interrupt handling in the guest and then change these flags.**
PTE Entries all have the present flag set to 1.
72 changes: 72 additions & 0 deletions src/hyperlight_common/src/host_calling.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use alloc::string::ToString;
use alloc::vec::Vec;
use core::ffi::c_char;

use anyhow::Result;

use crate::flatbuffer_wrappers::function_call::{FunctionCall, FunctionCallType};
use crate::flatbuffer_wrappers::function_types::{ParameterValue, ReturnType, ReturnValue};
use crate::input_output::{InputDataSection, OutputDataSection};
use crate::outb::{outb, OutBAction};
use crate::PEB;

/// Get a return value from a host function call.
/// This usually requires a host function to be called first using `call_host_function`.
pub fn get_host_return_value<T: TryFrom<ReturnValue>>() -> Result<T> {
let input_data_section: InputDataSection =
unsafe { (*PEB).clone() }.get_input_data_region().into();
let return_value = input_data_section
.try_pop_shared_input_data_into::<ReturnValue>()
.expect("Unable to deserialize a return value from host");

T::try_from(return_value).map_err(|_| {
anyhow::anyhow!(
"Host return value was not a {} as expected",
core::any::type_name::<T>()
)
})
}

/// Calls a host function.
// TODO: Make this generic, return a Result<T, ErrorCode> this should allow callers to call this function and get the result type they expect
// without having to do the conversion themselves
pub fn call_host_function(
function_name: &str,
parameters: Option<Vec<ParameterValue>>,
return_type: ReturnType,
) -> Result<()> {
let host_function_call = FunctionCall::new(
function_name.to_string(),
parameters,
FunctionCallType::Host,
return_type,
);

let host_function_call_buffer: Vec<u8> = host_function_call
.try_into()
.expect("Unable to serialize host function call");

let output_data_section: OutputDataSection =
unsafe { (*PEB).clone() }.get_output_data_region().into();
output_data_section.push_shared_output_data(host_function_call_buffer)?;

outb(OutBAction::CallFunction as u16, 0);

Ok(())
}

/// Uses `hloutb` to issue multiple `DebugPrint` `OutBAction`s to print a message.
pub fn print(message: &str) {
for byte in message.bytes() {
outb(OutBAction::DebugPrint as u16, byte);
}
}

/// Exposes a C API to allow the guest to print a string, byte by byte
///
/// # Safety
/// This function is not thread safe and assumes `outb` is safe to call directly.
#[no_mangle]
pub unsafe extern "C" fn _putchar(c: c_char) {
outb(OutBAction::DebugPrint as u16, c as u8);
}
Loading