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
5 changes: 5 additions & 0 deletions .github/workflows/dep_build_guests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ jobs:
just build-rust-guests ${{ inputs.config }}
just move-rust-guests ${{ inputs.config }}

- name: Build non-PIE Rust guests
run: |
just build-rust-guests-non-pie ${{ inputs.config }}
just move-rust-guests-non-pie ${{ inputs.config }}

Comment on lines +90 to +94
- name: Build C guests
run: |
just build-c-guests ${{ inputs.config }}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ $RECYCLE.BIN/

# Rust build artifacts
**/**target
**/**target-non-pie
libhyperlight_host.so
libhyperlight_host.d
hyperlight_host.dll
Expand Down
19 changes: 18 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ build target=default-target:
{{ cargo-cmd }} build --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }}

# build testing guest binaries
guests: build-and-move-rust-guests build-and-move-c-guests
guests: build-and-move-rust-guests build-and-move-rust-guests-non-pie build-and-move-c-guests

# Ensure the pinned cargo-hyperlight is installed. We compare the *actual*
# installed binary's reported version instead of relying on `cargo install`
Expand All @@ -75,6 +75,23 @@ build-rust-guests target=default-target features="": (ensure-cargo-hyperlight)
build-and-move-rust-guests: (build-rust-guests "debug") (move-rust-guests "debug") (build-rust-guests "release") (move-rust-guests "release")
build-and-move-c-guests: (build-c-guests "debug") (move-c-guests "debug") (build-c-guests "release") (move-c-guests "release")

# Build non-PIE variants of rust guests for testing ELF VA mapping.
# NOTE: non-PIE guests are x86_64-only; aarch64 is not yet supported.
# Phase 1 builds the sysroot without RUSTFLAGS (avoids RUSTFLAGS leaking
# into the sysroot wrapper build in cargo-hyperlight).
# Phase 2 uses plain cargo with --sysroot and non-PIE link flags.
build-rust-guests-non-pie target=default-target: (ensure-cargo-hyperlight)
cd src/tests/rust_guests/simpleguest && cargo hyperlight build --target-dir ../target-non-pie --profile={{ if target == "debug" { "dev" } else { target } }}
{{ if os() == "windows" { "$env:RUSTC_BOOTSTRAP=1; $env:RUSTFLAGS='--sysroot=' + (Resolve-Path src/tests/rust_guests/target-non-pie/sysroot).Path + ' -C relocation-model=static -C link-args=--no-pie -C link-args=--image-base=0x1000000 --cfg=hyperlight --check-cfg=cfg(hyperlight) -Clink-args=-eentrypoint';" } else { "" } }} cd src/tests/rust_guests/simpleguest && {{ if os() == "windows" { "" } else { "RUSTC_BOOTSTRAP=1 RUSTFLAGS=\"--sysroot=$(cd .. && pwd)/target-non-pie/sysroot -C relocation-model=static -C link-args=--no-pie -C link-args=--image-base=0x1000000 --cfg=hyperlight --check-cfg=cfg(hyperlight) -Clink-args=-eentrypoint\"" } }} cargo build --target x86_64-hyperlight-none --target-dir ../target-non-pie/build --profile={{ if target == "debug" { "dev" } else { target } }}

non_pie_guests_target := "src/tests/rust_guests/target-non-pie/build/x86_64-hyperlight-none"

@move-rust-guests-non-pie target=default-target:
{{ if os() == "windows" { "New-Item -ItemType Directory -Path " + rust_guests_bin_dir + "/" + target + "/non_pie -Force | Out-Null" } else { "mkdir -p " + rust_guests_bin_dir + "/" + target + "/non_pie" } }}
cp {{ non_pie_guests_target }}/{{ target }}/simpleguest {{ rust_guests_bin_dir }}/{{ target }}/non_pie/

build-and-move-rust-guests-non-pie: (build-rust-guests-non-pie "debug") (move-rust-guests-non-pie "debug") (build-rust-guests-non-pie "release") (move-rust-guests-non-pie "release")

clean: clean-rust

clean-rust:
Expand Down
6 changes: 3 additions & 3 deletions src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs
Original file line number Diff line number Diff line change
Expand Up @@ -675,10 +675,9 @@ pub(super) mod debug {
.dbg_mem_access_fn
.try_lock()
.map_err(|_| ProcessDebugRequestError::TryLockError(file!(), line!()))?
.layout
.get_guest_code_address();
.code_virt_base;

Ok(DebugResponse::GetCodeSectionOffset(offset as u64))
Ok(DebugResponse::GetCodeSectionOffset(offset))
}
DebugMsg::ReadAddr(addr, len) => {
let mut data = vec![0u8; len];
Expand Down Expand Up @@ -1505,6 +1504,7 @@ mod tests {
ro_mem.to_mgr_snapshot_mem().unwrap(),
scratch_mem,
NextAction::Initialise(layout.get_guest_code_address() as u64),
layout.get_guest_code_address() as u64,
);

let (mut hshm, gshm) = mem_mgr.build().unwrap();
Expand Down
9 changes: 9 additions & 0 deletions src/hyperlight_host/src/mem/elf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
#[cfg(feature = "mem_profile")]
use std::sync::Arc;

use goblin::elf::header::ET_DYN;
#[cfg(target_arch = "aarch64")]
use goblin::elf::reloc::{R_AARCH64_NONE, R_AARCH64_RELATIVE};
#[cfg(target_arch = "x86_64")]
Expand All @@ -42,6 +43,8 @@ pub(crate) struct ElfInfo {
shdrs: Vec<ResolvedSectionHeader>,
entry: u64,
relocs: Vec<Reloc>,
/// Whether this is a position-independent executable (ET_DYN).
is_pie: bool,
/// The hyperlight version string embedded by `hyperlight-guest-bin`, if
/// present. Used to detect version/ABI mismatches between guest and host.
guest_bin_version: Option<String>,
Expand Down Expand Up @@ -143,6 +146,7 @@ impl ElfInfo {
.collect(),
entry: elf.entry,
relocs,
is_pie: elf.header.e_type == ET_DYN,
guest_bin_version,
})
}
Expand All @@ -168,6 +172,11 @@ impl ElfInfo {
self.entry
}

/// Returns whether this is a position-independent executable (ET_DYN).
pub(crate) fn is_pie(&self) -> bool {
self.is_pie
}

/// Returns the hyperlight version string embedded in the guest binary, if
/// present. Used to detect version/ABI mismatches between guest and host.
pub(crate) fn guest_bin_version(&self) -> Option<&str> {
Expand Down
6 changes: 6 additions & 0 deletions src/hyperlight_host/src/mem/exe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ impl ExeInfo {
ExeInfo::Elf(elf) => Offset::from(elf.entrypoint_va()),
}
}
/// Returns whether this is a position-independent executable (ET_DYN).
pub fn is_pie(&self) -> bool {
match self {
ExeInfo::Elf(elf) => elf.is_pie(),
}
}
/// Returns the base virtual address of the loaded binary (lowest PT_LOAD p_vaddr).
pub fn base_va(&self) -> u64 {
match self {
Expand Down
91 changes: 88 additions & 3 deletions src/hyperlight_host/src/mem/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ use std::mem::size_of;
use hyperlight_common::mem::{HyperlightPEB, PAGE_SIZE_USIZE};
use tracing::{Span, instrument};

use super::memory_region::MemoryRegionType::{Code, Heap, InitData, Peb};
use super::memory_region::MemoryRegionType::{self, Code, Heap, InitData, Peb};
use super::memory_region::{
DEFAULT_GUEST_BLOB_MEM_FLAGS, MemoryRegion, MemoryRegion_, MemoryRegionFlags, MemoryRegionKind,
MemoryRegionVecBuilder,
DEFAULT_GUEST_BLOB_MEM_FLAGS, GuestMemoryRegion, MemoryRegion, MemoryRegion_,
MemoryRegionFlags, MemoryRegionKind, MemoryRegionVecBuilder,
};
#[cfg(readable_shared_mem)]
use super::shared_mem::HostSharedMemory;
Expand Down Expand Up @@ -555,6 +555,88 @@ impl SandboxMemoryLayout {
Ok(builder.build())
}

/// Compute the virtual base address for the code region and validate
/// that it does not overlap any other memory region.
///
/// For PIE binaries (`is_pie == true`), the code is identity-mapped so
/// the virtual base equals the physical load address and no conflict
/// is possible by construction.
///
/// Determine the virtual base address of the code region.
///
/// For PIE binaries, a random page-aligned address is chosen within
/// 47-bit canonical user space (ASLR). For non-PIE binaries, the
/// code appears at the ELF's declared virtual address (`elf_base_va`).
///
/// In both cases the resulting virtual range is validated against all
/// non-Code memory regions to prevent overlap.
pub(crate) fn code_virt_base(
&self,
is_pie: bool,
elf_base_va: u64,
loaded_size: u64,
) -> Result<u64> {
let code_size_pages = loaded_size.div_ceil(PAGE_SIZE_USIZE as u64);
let virt_base = if !is_pie {
elf_base_va
} else {
// Pick a random page-aligned address within 47-bit canonical user space.
// Lower bound: 0x1000000 (16 MiB, above all identity-mapped layout regions)
// Upper bound: accounts for code region size so it doesn't overflow
use rand::RngExt;
let mut rng = rand::rng();
let min_page = 0x1000_u64; // 0x1000 * PAGE_SIZE = 0x1000000
let max_page = 0x7_FFFF_FFFF_u64
.checked_sub(code_size_pages)
.ok_or_else(|| {
new_error!(
"PIE code region too large ({} pages) for ASLR randomization",
code_size_pages
)
})?;
let page_number = rng.random_range(min_page..max_page);
page_number
.checked_mul(PAGE_SIZE_USIZE as u64)
.ok_or_else(|| new_error!("ASLR page number overflow"))?
};

// Verify the code mapping does not conflict with other mappings
// (both non-PIE with declared VA and PIE with randomized ASLR base).
let code_virt_end = virt_base.checked_add(loaded_size).ok_or_else(|| {
new_error!(
"Code mapping overflow: base {:#x} + size {:#x}",
virt_base,
loaded_size
)
})?;
for rgn in self.get_memory_regions_::<GuestMemoryRegion>(())?.iter() {
if rgn.region_type == MemoryRegionType::Code {
continue;
}
let rgn_start = rgn.guest_region.start as u64;
let rgn_end = rgn_start.saturating_add(rgn.guest_region.len() as u64);
if virt_base < rgn_end && rgn_start < code_virt_end {
return Err(new_error!(
"Code mapping [{:#x}, {:#x}) conflicts with {:?} region [{:#x}, {:#x})",
virt_base,
code_virt_end,
rgn.region_type,
rgn_start,
rgn_end,
));
}
}

tracing::debug!(
code_virt_base = format_args!("{:#x}", virt_base),
elf_base_va = format_args!("{:#x}", elf_base_va),
is_pie,
"code region virtual base address"
);

Ok(virt_base)
}

#[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
pub(crate) fn write_init_data(&self, out: &mut [u8], bytes: &[u8]) -> Result<()> {
out[self.init_data_offset()..self.init_data_offset() + self.init_data_size]
Expand Down Expand Up @@ -678,6 +760,9 @@ impl SandboxMemoryLayout {
}

/// Guest address of the code section in the sandbox.
/// Used by WHP (Windows) and mem_profile feature; not called on
/// minimal Linux feature sets, hence the allow.
#[allow(dead_code)]
pub(crate) fn get_guest_code_address(&self) -> usize {
Self::BASE_ADDRESS + self.guest_code_offset()
}
Expand Down
10 changes: 9 additions & 1 deletion src/hyperlight_host/src/mem/mgr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ pub(crate) struct SandboxMemoryManager<S: SharedMemory> {
/// restored snapshot's own generation number so the guest-visible
/// counter tracks which snapshot the sandbox is a clone of.
pub(crate) snapshot_count: u64,
/// Virtual base address of the code region in guest space
pub(crate) code_virt_base: u64,
}

/// Buffer for building guest page tables during snapshot creation.
Expand Down Expand Up @@ -276,6 +278,7 @@ where
shared_mem: SnapshotSharedMemory<S>,
scratch_mem: S,
entrypoint: NextAction,
code_virt_base: u64,
) -> Self {
Self {
layout,
Expand All @@ -284,6 +287,7 @@ where
entrypoint,
abort_buffer: Vec::new(),
snapshot_count: 0,
code_virt_base,
}
}

Expand Down Expand Up @@ -316,6 +320,7 @@ where
entrypoint,
self.snapshot_count,
host_functions,
self.code_virt_base,
)
}
}
Expand All @@ -326,7 +331,8 @@ impl SandboxMemoryManager<ExclusiveSharedMemory> {
let shared_mem = s.memory().to_mgr_snapshot_mem()?;
let scratch_mem = ExclusiveSharedMemory::new(s.layout().get_scratch_size())?;
let entrypoint = s.entrypoint();
let mut mgr = Self::new(layout, shared_mem, scratch_mem, entrypoint);
let code_virt_base = s.code_virt_base();
let mut mgr = Self::new(layout, shared_mem, scratch_mem, entrypoint, code_virt_base);
// Inherit the snapshot's generation number for the same
// reason `restore_snapshot` does: the guest-visible counter
// reflects "which snapshot is the sandbox currently a clone
Expand Down Expand Up @@ -360,6 +366,7 @@ impl SandboxMemoryManager<ExclusiveSharedMemory> {
entrypoint: self.entrypoint,
abort_buffer: self.abort_buffer,
snapshot_count: self.snapshot_count,
code_virt_base: self.code_virt_base,
};
let guest_mgr = SandboxMemoryManager {
shared_mem: gshm,
Expand All @@ -368,6 +375,7 @@ impl SandboxMemoryManager<ExclusiveSharedMemory> {
entrypoint: self.entrypoint,
abort_buffer: Vec::new(), // Guest doesn't need abort buffer
snapshot_count: self.snapshot_count,
code_virt_base: self.code_virt_base,
};
host_mgr.update_scratch_bookkeeping()?;
Ok((host_mgr, guest_mgr))
Expand Down
17 changes: 10 additions & 7 deletions src/hyperlight_host/src/sandbox/initialized_multi_use.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1971,9 +1971,12 @@ mod tests {
/// `read_guest_memory_by_gva`, then assert both views are identical.
#[cfg(feature = "trace_guest")]
fn assert_gva_read_matches(sbox: &mut MultiUseSandbox, gva: u64, len: usize) {
// Guest reads via its own page tables
// Guest reads via its own page tables.
// do_map = false: the code region is already mapped (identity-mapped
// or ASLR-mapped), so we must not remap it with an identity mapping
// that would use the GVA as a physical address.
let expected: Vec<u8> = sbox
.call("ReadMappedBuffer", (gva, len as u64, true))
.call("ReadMappedBuffer", (gva, len as u64, false))
.unwrap();
assert_eq!(expected.len(), len);

Expand All @@ -1997,7 +2000,7 @@ mod tests {
#[cfg(feature = "trace_guest")]
fn read_guest_memory_by_gva_single_page() {
let mut sbox = sandbox_for_gva_tests();
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
let code_gva = sbox.mem_mgr.code_virt_base;
assert_gva_read_matches(&mut sbox, code_gva, 128);
}

Expand All @@ -2007,7 +2010,7 @@ mod tests {
#[cfg(feature = "trace_guest")]
fn read_guest_memory_by_gva_full_page() {
let mut sbox = sandbox_for_gva_tests();
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
let code_gva = sbox.mem_mgr.code_virt_base;
assert_gva_read_matches(&mut sbox, code_gva, 4096);
}

Expand All @@ -2017,7 +2020,7 @@ mod tests {
#[cfg(feature = "trace_guest")]
fn read_guest_memory_by_gva_unaligned_cross_page() {
let mut sbox = sandbox_for_gva_tests();
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
let code_gva = sbox.mem_mgr.code_virt_base;
// Start 1 byte before the second page boundary and read 4097 bytes
// (spans 2 full page boundaries).
let start = code_gva + 4096 - 1;
Expand All @@ -2033,7 +2036,7 @@ mod tests {
#[cfg(feature = "trace_guest")]
fn read_guest_memory_by_gva_two_full_pages() {
let mut sbox = sandbox_for_gva_tests();
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
let code_gva = sbox.mem_mgr.code_virt_base;
assert_gva_read_matches(&mut sbox, code_gva, 4096 * 2);
}

Expand All @@ -2044,7 +2047,7 @@ mod tests {
#[cfg(feature = "trace_guest")]
fn read_guest_memory_by_gva_cross_page_boundary() {
let mut sbox = sandbox_for_gva_tests();
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
let code_gva = sbox.mem_mgr.code_virt_base;
// Start 100 bytes before the first page boundary, read across it.
let start = code_gva + 4096 - 100;
assert_gva_read_matches(&mut sbox, start, 200);
Expand Down
28 changes: 9 additions & 19 deletions src/hyperlight_host/src/sandbox/snapshot/file/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -456,27 +456,17 @@ impl OciSnapshotConfig {
}
}

// Entrypoint address must point inside the guest snapshot
// region `[BASE_ADDRESS, BASE_ADDRESS + snapshot_size)`. The
// address is a GVA, bounded by the same range because guests
// identity-map the snapshot region at low VAs. A guest
// dispatching from a non-identity-mapped VA must relax this
// check.
let snap_lo = SandboxMemoryLayout::BASE_ADDRESS as u64;
let snap_hi = snap_lo
.checked_add(self.layout.snapshot_size as u64)
.ok_or_else(|| {
crate::new_error!(
"snapshot layout overflow: BASE_ADDRESS + snapshot_size ({}) does not fit in u64",
self.layout.snapshot_size
)
})?;
if self.entrypoint_addr < snap_lo || self.entrypoint_addr >= snap_hi {
// Validate the entrypoint GVA.
// `entrypoint_addr` is a GVA loaded into RIP. For ASLR or non-PIE
// guests the code may be mapped at a non-identity VA, so we only
// require the address is non-zero and within the 47-bit canonical
// user-space range.
let max_gva_entrypoint = 0x7FFF_FFFF_FFFFu64; // 47-bit canonical
if self.entrypoint_addr == 0 || self.entrypoint_addr > max_gva_entrypoint {
return Err(crate::new_error!(
"snapshot entrypoint addr {:#x} is outside the snapshot region [{:#x}, {:#x})",
"snapshot entrypoint addr {:#x} is outside the valid GVA range (0, {:#x}]",
self.entrypoint_addr,
snap_lo,
snap_hi
max_gva_entrypoint
));
}

Expand Down
Loading
Loading