Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions src/hyperlight_common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ fuzzing = ["dep:arbitrary"]
trace_guest = []
mem_profile = []
std = ["thiserror/std", "log/std", "tracing/std"]
init-paging = []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feature is not initializaing paging, but providing data structures for managing virtual memory.
I would make the data structures unconditionally available (i.e., no feature needed), as they have no side-effects (no extra dependencies, no runtime side effects, I wouldn't feature gate it here, but would feature gate it in hl-host / hl-guest)
Alternatively, how about calling it "virtual-memory"?


[lib]
bench = false # see https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options
Expand Down
217 changes: 217 additions & 0 deletions src/hyperlight_common/src/arch/amd64/vm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/*
Copyright 2025 The Hyperlight Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

use crate::vm::{Mapping, MappingKind, TableOps};

#[inline(always)]
/// Utility function to extract an (inclusive on both ends) bit range
/// from a quadword.
fn bits<const HIGH_BIT: u8, const LOW_BIT: u8>(x: u64) -> u64 {
(x & ((1 << (HIGH_BIT + 1)) - 1)) >> LOW_BIT
}

/// A helper structure indicating a mapping operation that needs to be
/// performed
struct MapRequest<T> {
table_base: T,
vmin: VirtAddr,
len: u64,
}

/// A helper structure indicating that a particular PTE needs to be
/// modified
struct MapResponse<T> {
entry_ptr: T,
vmin: VirtAddr,
len: u64,
}

struct ModifyPteIterator<const HIGH_BIT: u8, const LOW_BIT: u8, Op: TableOps> {
request: MapRequest<Op::TableAddr>,
n: u64,
}
impl<const HIGH_BIT: u8, const LOW_BIT: u8, Op: TableOps> Iterator
for ModifyPteIterator<HIGH_BIT, LOW_BIT, Op>
{
type Item = MapResponse<Op::TableAddr>;
fn next(&mut self) -> Option<Self::Item> {
if (self.n << LOW_BIT) >= self.request.len {
return None;
}
// next stage parameters
let mut next_vmin = self.request.vmin + (self.n << LOW_BIT);
let lower_bits_mask = (1 << LOW_BIT) - 1;
if self.n > 0 {
next_vmin &= !lower_bits_mask;
}
let entry_ptr = Op::entry_addr(
self.request.table_base,
bits::<HIGH_BIT, LOW_BIT>(next_vmin) << 3,
);
let len_from_here = self.request.len - (next_vmin - self.request.vmin);
let max_len = (1 << LOW_BIT) - (next_vmin & lower_bits_mask);
let next_len = core::cmp::min(len_from_here, max_len);
Comment on lines +54 to +66
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not too sure what's going on here.
This is somewhat different from the previous implementation, but I'm not too sure what was going on there either, so... 🤷

Maybe some comments would help


// update our state
self.n += 1;

Some(MapResponse {
entry_ptr,
vmin: next_vmin,
len: next_len,
})
}
}
fn modify_ptes<const HIGH_BIT: u8, const LOW_BIT: u8, Op: TableOps>(
r: MapRequest<Op::TableAddr>,
) -> ModifyPteIterator<HIGH_BIT, LOW_BIT, Op> {
ModifyPteIterator { request: r, n: 0 }
}

/// Page-mapping callback to allocate a next-level page table if necessary.
/// # Safety
/// This function modifies page table data structures, and should not be called concurrently
/// with any other operations that modify the page tables.
unsafe fn alloc_pte_if_needed<Op: TableOps>(
op: &Op,
x: MapResponse<Op::TableAddr>,
) -> MapRequest<Op::TableAddr> {
let pte = unsafe { op.read_entry(x.entry_ptr) };
let present = pte & 0x1;
if present != 0 {
Comment on lines +93 to +94
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let present = pte & 0x1;
if present != 0 {
let present = pte & 0x1 != 0;
if present {

or even better

Suggested change
let present = pte & 0x1;
if present != 0 {
let present = bits<0,0>(pte) != 0;
if present {

return MapRequest {
table_base: Op::from_phys(pte & !0xfff),
vmin: x.vmin,
len: x.len,
};
}

let page_addr = unsafe { op.alloc_table() };

#[allow(clippy::identity_op)]
#[allow(clippy::precedence)]
let pte = Op::to_phys(page_addr) |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we do Op::to_phys(page_addr)?
the previous code was page_addr

1 << 5 | // A - we don't track accesses at table level
0 << 4 | // PCD - leave caching enabled
0 << 3 | // PWT - write-back
1 << 2 | // U/S - allow user access to everything (for now)
1 << 1 | // R/W - we don't use block-level permissions
1 << 0; // P - this entry is present
unsafe { op.write_entry(x.entry_ptr, pte) };
MapRequest {
table_base: page_addr,
vmin: x.vmin,
len: x.len,
}
}

/// Map a normal memory page
/// # Safety
/// This function modifies page table data structures, and should not be called concurrently
/// with any other operations that modify the page tables.
#[allow(clippy::identity_op)]
#[allow(clippy::precedence)]
unsafe fn map_page<Op: TableOps>(op: &Op, mapping: &Mapping, r: MapResponse<Op::TableAddr>) {
let pte = match &mapping.kind {
MappingKind::BasicMapping(bm) =>
// TODO: Support not readable
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TODO comment should be more specific about the architectural limitation. On x86-64, pages cannot be made write-only or execute-only without being readable (there's no separate "readable" bit in the page table entry). The comment should clarify whether this limitation is acceptable or if there are plans to work around it.

Suggested change
// TODO: Support not readable
// NOTE: On x86-64, there is no separate "readable" bit in the page table entry.
// This means that pages cannot be made write-only or execute-only without also being readable.
// All pages that are mapped as writable or executable are also implicitly readable.
// If support for "not readable" mappings is required in the future, it would need to be
// implemented using additional mechanisms (e.g., page-fault handling or memory protection keys),
// but for now, this architectural limitation is accepted.

Copilot uses AI. Check for mistakes.
{
(mapping.phys_base + (r.vmin - mapping.virt_base)) |
(!bm.executable as u64) << 63 | // NX - no execute unless allowed
1 << 7 | // 1 - RES1 according to manual
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting bit 7 (PAT bit) to 1 is incorrect. According to the Intel and AMD manuals, bit 7 in a page table entry is the PAT (Page Attribute Table) bit, not a reserved bit that must be 1. Setting this bit unconditionally changes the memory type of all mapped pages to use PAT entry 1 instead of the default entry 0, which could cause unexpected caching behavior.

Unless there's a specific reason to use a non-default PAT entry, this bit should be set to 0 to match the PCD=0, PWT=0 settings (which together select PAT entry 0 for normal write-back cached memory).

Suggested change
1 << 7 | // 1 - RES1 according to manual
0 << 7 | // PAT=0 (default write-back caching)

Copilot uses AI. Check for mistakes.
1 << 6 | // D - we don't presently track dirty state for anything
1 << 5 | // A - we don't presently track access for anything
0 << 4 | // PCD - leave caching enabled
0 << 3 | // PWT - write-back
1 << 2 | // U/S - allow user access to everything (for now)
(bm.writable as u64) << 1 | // R/W - for now make everything r/w
1 << 0 // P - this entry is present
}
};
unsafe {
op.write_entry(r.entry_ptr, pte);
}
}

// There are no notable architecture-specific safety considerations
// here, and the general conditions are documented in the
// architecture-independent re-export in vm.rs
#[allow(clippy::missing_safety_doc)]
pub unsafe fn map<Op: TableOps>(op: &Op, mapping: Mapping) {
modify_ptes::<47, 39, Op>(MapRequest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is code that was just moved around, but it would be nice if there was some comments explaining what's going on here.

table_base: op.root_table(),
vmin: mapping.virt_base,
len: mapping.len,
})
.map(|r| unsafe { alloc_pte_if_needed(op, r) })
.flat_map(modify_ptes::<38, 30, Op>)
.map(|r| unsafe { alloc_pte_if_needed(op, r) })
.flat_map(modify_ptes::<29, 21, Op>)
.map(|r| unsafe { alloc_pte_if_needed(op, r) })
.flat_map(modify_ptes::<20, 12, Op>)
.map(|r| unsafe { map_page(op, &mapping, r) })
.for_each(drop);
}
Comment on lines +153 to +167
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new page table manipulation code in hyperlight_common lacks unit tests. This is critical code that handles memory mapping with complex logic including bit manipulation, iterator state management, and unsafe operations. Given that similar modules in the codebase have comprehensive test coverage, tests should be added to verify:

  • Correct handling of page-aligned and unaligned virtual addresses
  • Proper calculation of entry indices at each page table level
  • Correct PTE flag generation for different mapping types
  • Edge cases like zero-length mappings or boundaries crossing page table entries

This is especially important given the bugs found in the host's TableOps implementation, which would have been caught by tests.

Copilot uses AI. Check for mistakes.

/// # Safety
/// This function traverses page table data structures, and should not
/// be called concurrently with any other operations that modify the
/// page table.
unsafe fn require_pte_exist<Op: TableOps>(
op: &Op,
x: MapResponse<Op::TableAddr>,
) -> Option<MapRequest<Op::TableAddr>> {
let pte = unsafe { op.read_entry(x.entry_ptr) };
let present = pte & 0x1;
if present == 0 {
Comment on lines +178 to +179
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let present = pte & 0x1;
if present == 0 {
let present = bits<0,0>(pte) != 0;
if !present {

return None;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this might have changed behavious, is that intentional?
This used to panic, now it retuns None.

}
Some(MapRequest {
table_base: Op::from_phys(pte & !0xfff),
vmin: x.vmin,
len: x.len,
})
}

// There are no notable architecture-specific safety considerations
// here, and the general conditions are documented in the
// architecture-independent re-export in vm.rs
#[allow(clippy::missing_safety_doc)]
pub unsafe fn vtop<Op: TableOps>(op: &Op, address: u64) -> Option<u64> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I king of liked better the previous name dbg_print_address_pte, what is vtop?

modify_ptes::<47, 39, Op>(MapRequest {
table_base: op.root_table(),
vmin: address,
len: 1,
})
.filter_map(|r| unsafe { require_pte_exist::<Op>(op, r) })
.flat_map(modify_ptes::<38, 30, Op>)
.filter_map(|r| unsafe { require_pte_exist::<Op>(op, r) })
.flat_map(modify_ptes::<29, 21, Op>)
.filter_map(|r| unsafe { require_pte_exist::<Op>(op, r) })
.flat_map(modify_ptes::<20, 12, Op>)
.filter_map(|r| {
let pte = unsafe { op.read_entry(r.entry_ptr) };
let present = pte & 0x1;
if present == 0 { None } else { Some(pte) }
})
.next()
}

pub const PAGE_SIZE: usize = 4096;
pub const PAGE_TABLE_SIZE: usize = 4096;
pub type PageTableEntry = u64;
pub type VirtAddr = u64;
pub type PhysAddr = u64;
3 changes: 3 additions & 0 deletions src/hyperlight_common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ pub mod resource;

/// cbindgen:ignore
pub mod func;
// cbindgen:ignore
#[cfg(feature = "init-paging")]
pub mod vm;
131 changes: 131 additions & 0 deletions src/hyperlight_common/src/vm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
Copyright 2025 The Hyperlight Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

#[cfg_attr(target_arch = "x86_64", path = "arch/amd64/vm.rs")]
mod arch;

pub use arch::{PAGE_SIZE, PAGE_TABLE_SIZE, PageTableEntry, PhysAddr, VirtAddr};
pub const PAGE_TABLE_ENTRIES_PER_TABLE: usize =
PAGE_TABLE_SIZE / core::mem::size_of::<PageTableEntry>();

/// The operations used to actually access the page table structures,
/// used to allow the same code to be used in the host and the guest
/// for page table setup
pub trait TableOps {
/// The type of table addresses
type TableAddr: Copy;

/// Allocate a zeroed table
///
/// # Safety
/// The current implementations of this function are not
/// inherently unsafe, but the guest implementation will likely
/// become so in the future when a real physical page allocator is
/// implemented.
///
/// Currently, callers should take care not to call this on
/// multiple threads at the same time.
///
/// # Panics
/// This function may panic if:
/// - The Layout creation fails
/// - Memory allocation fails
unsafe fn alloc_table(&self) -> Self::TableAddr;

/// Offset the table address by the u64 entry offset
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation for entry_addr is unclear about whether the entry_offset parameter is a byte offset or an entry index. Looking at the implementation in arch/amd64/vm.rs line 62, the offset is passed as a byte offset (multiplied by 8), but the trait documentation doesn't specify this. The documentation should clarify that entry_offset is expected to be a byte offset within the page table, not an entry index.

Suggested change
/// Offset the table address by the u64 entry offset
/// Offset the table address by the given offset in bytes.
///
/// # Parameters
/// - `addr`: The base address of the table.
/// - `entry_offset`: The offset in **bytes** within the page table. This is
/// not an entry index; callers must multiply the entry index by the size
/// of a page table entry (typically 8 bytes) to obtain the correct byte offset.
///
/// # Returns
/// The address of the entry at the given byte offset from the base address.

Copilot uses AI. Check for mistakes.
fn entry_addr(addr: Self::TableAddr, entry_offset: u64) -> Self::TableAddr;

/// Read a u64 from the given address, used to read existing page
/// table entries
///
/// # Safety
/// This reads from the given memory address, and so all the usual
/// Rust things about raw pointers apply. This will also be used
/// to update guest page tables, so especially in the guest, it is
/// important to ensure that the page tables updates do not break
/// invariants. The implementor of the trait should ensure that
/// nothing else will be reading/writing the address at the same
/// time as mapping code using the trait.
unsafe fn read_entry(&self, addr: Self::TableAddr) -> PageTableEntry;

/// Write a u64 to the given address, used to write updated page
/// table entries
///
/// # Safety
/// This writes to the given memory address, and so all the usual
/// Rust things about raw pointers apply. This will also be used
/// to update guest page tables, so especially in the guest, it is
/// important to ensure that the page tables updates do not break
/// invariants. The implementor of the trait should ensure that
/// nothing else will be reading/writing the address at the same
/// time as mapping code using the trait.
unsafe fn write_entry(&self, addr: Self::TableAddr, x: PageTableEntry);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
unsafe fn write_entry(&self, addr: Self::TableAddr, x: PageTableEntry);
unsafe fn write_entry(&self, addr: Self::TableAddr, entry: PageTableEntry);


/// Convert an abstract physical address to a concrete u64 which
/// can be e.g. written into a table
fn to_phys(addr: Self::TableAddr) -> PhysAddr;

/// Convert a concrete u64 which may have been e.g. read from a
/// table back into an abstract physical address
fn from_phys(addr: PhysAddr) -> Self::TableAddr;
Comment on lines +77 to +83
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these comments correct? or are they swapped?


/// Return the address of the root page table
fn root_table(&self) -> Self::TableAddr;
}

#[derive(Debug)]
pub struct BasicMapping {
pub readable: bool,
pub writable: bool,
pub executable: bool,
}

#[derive(Debug)]
pub enum MappingKind {
BasicMapping(BasicMapping),
/* TODO: What useful things other than basic mappings actually
* require touching the tables? */
}

#[derive(Debug)]
pub struct Mapping {
pub phys_base: u64,
pub virt_base: u64,
pub len: u64,
pub kind: MappingKind,
}

/// Assumption: all are page-aligned
///
/// # Safety
/// This function modifies pages backing a virtual memory range which
/// is inherently unsafe w.r.t. the Rust memory model.
///
/// When using this function, please note:
/// - No locking is performed before touching page table data structures,
/// as such do not use concurrently with any other page table operations
/// - TLB invalidation is not performed, if previously-mapped ranges
/// are being remapped, TLB invalidation may need to be performed
/// afterwards.
pub use arch::map;
/// This function is not presently used for anything, but is useful
/// for debugging
///
/// # Safety
/// This function traverses page table data structures, and should not
/// be called concurrently with any other operations that modify the
/// page table.
pub use arch::vtop;
2 changes: 1 addition & 1 deletion src/hyperlight_guest_bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ macros = ["dep:hyperlight-guest-macro", "dep:linkme"]

[dependencies]
hyperlight-guest = { workspace = true, default-features = false }
hyperlight-common = { workspace = true, default-features = false }
hyperlight-common = { workspace = true, default-features = false, features = [ "init-paging" ] }
hyperlight-guest-tracing = { workspace = true, default-features = false }
hyperlight-guest-macro = { workspace = true, default-features = false, optional = true }
buddy_system_allocator = "0.11.0"
Expand Down
Loading
Loading