Skip to content

Commit c55b6f5

Browse files
cshungCopilot
andcommitted
feat: enable ASLR for PIE guest binaries
Randomize the virtual base address for PIE guest code regions instead of using identity mapping. This provides address space layout randomization (ASLR) for PIE guests, making the code region virtual address unpredictable across sandbox instantiations. The random base is chosen from a page-aligned range within 47-bit canonical user space [0x1000000, max - code_size). Non-PIE binaries continue to use their declared ELF base VA. Changes: - layout.rs: code_virt_base() now randomizes VA for PIE guests and always validates against memory region conflicts - mgr.rs: thread code_virt_base through SandboxMemoryManager - snapshot/mod.rs: store code_virt_base in Snapshot, use it for relocation processing in exe_info.load() - config.rs: relax entrypoint validation to allow non-identity-mapped virtual addresses (ASLR / non-PIE) - initialized_multi_use.rs: trace_guest tests use code_virt_base instead of assuming GVA == GPA Signed-off-by: cshung <3410332+cshung@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6f20a05d-6bee-4e2e-b320-12f8d9759bbc Signed-off-by: cshung <3410332+cshung@users.noreply.github.com>
1 parent 46c86bc commit c55b6f5

7 files changed

Lines changed: 109 additions & 91 deletions

File tree

Justfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ build-and-move-rust-guests: (build-rust-guests "debug") (move-rust-guests "debug
7676
build-and-move-c-guests: (build-c-guests "debug") (move-c-guests "debug") (build-c-guests "release") (move-c-guests "release")
7777

7878
# Build non-PIE variants of rust guests for testing ELF VA mapping.
79+
# NOTE: non-PIE guests are x86_64-only; aarch64 is not yet supported.
7980
# Phase 1 builds the sysroot without RUSTFLAGS (avoids RUSTFLAGS leaking
8081
# into the sysroot wrapper build in cargo-hyperlight).
8182
# Phase 2 uses plain cargo with --sysroot and non-PIE link flags.

src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -720,10 +720,9 @@ pub(super) mod debug {
720720
.dbg_mem_access_fn
721721
.try_lock()
722722
.map_err(|_| ProcessDebugRequestError::TryLockError(file!(), line!()))?
723-
.layout
724-
.get_guest_code_address();
723+
.code_virt_base;
725724

726-
Ok(DebugResponse::GetCodeSectionOffset(offset as u64))
725+
Ok(DebugResponse::GetCodeSectionOffset(offset))
727726
}
728727
DebugMsg::ReadAddr(addr, len) => {
729728
let mut data = vec![0u8; len];

src/hyperlight_host/src/mem/layout.rs

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -557,15 +557,12 @@ impl SandboxMemoryLayout {
557557
/// guest memory regions with the Code region's `guest_virt_addr`
558558
/// already set to the computed virtual base.
559559
///
560-
/// For PIE binaries (`is_pie == true`), the code is identity-mapped so
561-
/// the virtual base equals the physical load address and no conflict
562-
/// is possible by construction.
560+
/// For PIE binaries, a random page-aligned address is chosen within
561+
/// 47-bit canonical user space (ASLR). For non-PIE binaries, the
562+
/// code appears at the ELF's declared virtual address (`elf_base_va`).
563563
///
564-
/// For non-PIE binaries, the code appears at the ELF's declared
565-
/// virtual address (`elf_base_va`), which may differ from the physical
566-
/// load address. This method checks that the resulting virtual range
567-
/// `[elf_base_va, elf_base_va + loaded_size)` does not overlap any
568-
/// non-Code region.
564+
/// In both cases the resulting virtual range is validated against all
565+
/// non-Code memory regions to prevent overlap.
569566
///
570567
/// Returns `(code_virt_base, regions)`.
571568
pub(crate) fn get_guest_regions_with_code_va(
@@ -574,35 +571,56 @@ impl SandboxMemoryLayout {
574571
elf_base_va: u64,
575572
loaded_size: u64,
576573
) -> Result<(u64, Vec<MemoryRegion_<GuestMemoryRegion>>)> {
577-
let load_addr = self.get_guest_code_address() as u64;
578-
let code_virt_base = if is_pie { load_addr } else { elf_base_va };
574+
let code_size_pages = loaded_size.div_ceil(PAGE_SIZE_USIZE as u64);
575+
let code_virt_base = if !is_pie {
576+
elf_base_va
577+
} else {
578+
// Pick a random page-aligned address within 47-bit canonical user space.
579+
// Lower bound: 0x1000000 (16 MiB, above all identity-mapped layout regions)
580+
// Upper bound: accounts for code region size so it doesn't overflow
581+
use rand::RngExt;
582+
let mut rng = rand::rng();
583+
let min_page = 0x1000_u64; // 0x1000 * PAGE_SIZE = 0x1000000
584+
let max_page = 0x7_FFFF_FFFF_u64
585+
.checked_sub(code_size_pages)
586+
.ok_or_else(|| {
587+
new_error!(
588+
"PIE code region too large ({} pages) for ASLR randomization",
589+
code_size_pages
590+
)
591+
})?;
592+
let page_number = rng.random_range(min_page..max_page);
593+
page_number
594+
.checked_mul(PAGE_SIZE_USIZE as u64)
595+
.ok_or_else(|| new_error!("ASLR page number overflow"))?
596+
};
579597

580598
let mut regions = self.get_memory_regions()?;
581599

582-
if !is_pie {
583-
let code_virt_end = code_virt_base.checked_add(loaded_size).ok_or_else(|| {
584-
new_error!(
585-
"Code mapping overflow: base {:#x} + size {:#x}",
600+
// Verify the code mapping does not conflict with other mappings
601+
// (both non-PIE with declared VA and PIE with randomized ASLR base).
602+
let code_virt_end = code_virt_base.checked_add(loaded_size).ok_or_else(|| {
603+
new_error!(
604+
"Code mapping overflow: base {:#x} + size {:#x}",
605+
code_virt_base,
606+
loaded_size
607+
)
608+
})?;
609+
for rgn in regions.iter() {
610+
if rgn.region_type == MemoryRegionType::Code {
611+
continue;
612+
}
613+
let rgn_start = rgn.guest_region.start as u64;
614+
let rgn_end = rgn_start.saturating_add(rgn.guest_region.len() as u64);
615+
if code_virt_base < rgn_end && rgn_start < code_virt_end {
616+
return Err(new_error!(
617+
"Code mapping [{:#x}, {:#x}) conflicts with {:?} region [{:#x}, {:#x})",
586618
code_virt_base,
587-
loaded_size
588-
)
589-
})?;
590-
for rgn in regions.iter() {
591-
if rgn.region_type == MemoryRegionType::Code {
592-
continue;
593-
}
594-
let rgn_start = rgn.guest_region.start as u64;
595-
let rgn_end = rgn_start + rgn.guest_region.len() as u64;
596-
if code_virt_base < rgn_end && rgn_start < code_virt_end {
597-
return Err(new_error!(
598-
"Non-PIE code mapping [{:#x}, {:#x}) conflicts with {:?} region [{:#x}, {:#x})",
599-
code_virt_base,
600-
code_virt_end,
601-
rgn.region_type,
602-
rgn_start,
603-
rgn_end,
604-
));
605-
}
619+
code_virt_end,
620+
rgn.region_type,
621+
rgn_start,
622+
rgn_end,
623+
));
606624
}
607625
}
608626

@@ -615,6 +633,13 @@ impl SandboxMemoryLayout {
615633
}
616634
}
617635

636+
tracing::debug!(
637+
code_virt_base = format_args!("{:#x}", code_virt_base),
638+
elf_base_va = format_args!("{:#x}", elf_base_va),
639+
is_pie,
640+
"code region virtual base address"
641+
);
642+
618643
Ok((code_virt_base, regions))
619644
}
620645

@@ -741,6 +766,9 @@ impl SandboxMemoryLayout {
741766
}
742767

743768
/// Guest address of the code section in the sandbox.
769+
/// Used by WHP (Windows) and mem_profile feature; not called on
770+
/// minimal Linux feature sets, hence the allow.
771+
#[allow(dead_code)]
744772
pub(crate) fn get_guest_code_address(&self) -> usize {
745773
Self::BASE_ADDRESS + self.guest_code_offset()
746774
}

src/hyperlight_host/src/sandbox/initialized_multi_use.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2010,9 +2010,12 @@ mod tests {
20102010
/// `read_guest_memory_by_gva`, then assert both views are identical.
20112011
#[cfg(feature = "trace_guest")]
20122012
fn assert_gva_read_matches(sbox: &mut MultiUseSandbox, gva: u64, len: usize) {
2013-
// Guest reads via its own page tables
2013+
// Guest reads via its own page tables.
2014+
// do_map = false: the code region is already mapped (identity-mapped
2015+
// or ASLR-mapped), so we must not remap it with an identity mapping
2016+
// that would use the GVA as a physical address.
20142017
let expected: Vec<u8> = sbox
2015-
.call("ReadMappedBuffer", (gva, len as u64, true))
2018+
.call("ReadMappedBuffer", (gva, len as u64, false))
20162019
.unwrap();
20172020
assert_eq!(expected.len(), len);
20182021

@@ -2036,7 +2039,7 @@ mod tests {
20362039
#[cfg(feature = "trace_guest")]
20372040
fn read_guest_memory_by_gva_single_page() {
20382041
let mut sbox = sandbox_for_gva_tests();
2039-
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2042+
let code_gva = sbox.mem_mgr.code_virt_base;
20402043
assert_gva_read_matches(&mut sbox, code_gva, 128);
20412044
}
20422045

@@ -2046,7 +2049,7 @@ mod tests {
20462049
#[cfg(feature = "trace_guest")]
20472050
fn read_guest_memory_by_gva_full_page() {
20482051
let mut sbox = sandbox_for_gva_tests();
2049-
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2052+
let code_gva = sbox.mem_mgr.code_virt_base;
20502053
assert_gva_read_matches(&mut sbox, code_gva, 4096);
20512054
}
20522055

@@ -2056,7 +2059,7 @@ mod tests {
20562059
#[cfg(feature = "trace_guest")]
20572060
fn read_guest_memory_by_gva_unaligned_cross_page() {
20582061
let mut sbox = sandbox_for_gva_tests();
2059-
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2062+
let code_gva = sbox.mem_mgr.code_virt_base;
20602063
// Start 1 byte before the second page boundary and read 4097 bytes
20612064
// (spans 2 full page boundaries).
20622065
let start = code_gva + 4096 - 1;
@@ -2072,7 +2075,7 @@ mod tests {
20722075
#[cfg(feature = "trace_guest")]
20732076
fn read_guest_memory_by_gva_two_full_pages() {
20742077
let mut sbox = sandbox_for_gva_tests();
2075-
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2078+
let code_gva = sbox.mem_mgr.code_virt_base;
20762079
assert_gva_read_matches(&mut sbox, code_gva, 4096 * 2);
20772080
}
20782081

@@ -2083,7 +2086,7 @@ mod tests {
20832086
#[cfg(feature = "trace_guest")]
20842087
fn read_guest_memory_by_gva_cross_page_boundary() {
20852088
let mut sbox = sandbox_for_gva_tests();
2086-
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2089+
let code_gva = sbox.mem_mgr.code_virt_base;
20872090
// Start 100 bytes before the first page boundary, read across it.
20882091
let start = code_gva + 4096 - 100;
20892092
assert_gva_read_matches(&mut sbox, start, 200);

src/hyperlight_host/src/sandbox/snapshot/file/config.rs

Lines changed: 16 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -483,53 +483,31 @@ impl OciSnapshotConfig {
483483
}
484484
}
485485

486-
// The saved dispatch entrypoint must be in the executable code
487-
// region. Code occupies the page-rounded prefix of the snapshot.
488-
let code_lo = SandboxMemoryLayout::BASE_ADDRESS as u64;
489-
let code_hi = code_lo
490-
.checked_add(self.layout.code_size.next_multiple_of(PAGE_SIZE) as u64)
491-
.ok_or_else(|| {
492-
crate::new_error!(
493-
"snapshot layout overflow: BASE_ADDRESS + code_size ({}) does not fit in u64",
494-
self.layout.code_size
495-
)
496-
})?;
497-
if self.entrypoint_addr < code_lo || self.entrypoint_addr >= code_hi {
486+
// Validate the entrypoint GVA.
487+
// `entrypoint_addr` is a GVA loaded into RIP. For ASLR or non-PIE
488+
// guests the code may be mapped at a non-identity VA, so we only
489+
// require the address is non-zero and within the 47-bit canonical
490+
// user-space range.
491+
let max_gva_entrypoint = 0x7FFF_FFFF_FFFFu64; // 47-bit canonical
492+
if self.entrypoint_addr == 0 || self.entrypoint_addr > max_gva_entrypoint {
498493
return Err(crate::new_error!(
499-
"snapshot entrypoint addr {:#x} is outside the code region [{:#x}, {:#x})",
494+
"snapshot entrypoint addr {:#x} is outside the valid GVA range (0, {:#x}]",
500495
self.entrypoint_addr,
501-
code_lo,
502-
code_hi
503-
));
504-
}
505-
#[cfg(target_arch = "aarch64")]
506-
if !self.entrypoint_addr.is_multiple_of(4) {
507-
return Err(crate::new_error!(
508-
"snapshot entrypoint addr {:#x} is not 4-byte aligned",
509-
self.entrypoint_addr
496+
max_gva_entrypoint
510497
));
511498
}
512499

513500
// ELF entry point GVA for `AT_ENTRY` in core dumps. 0 means
514-
// unknown. Any other value must point inside the snapshot
515-
// region, like `entrypoint_addr`.
516-
let snapshot_hi = code_lo
517-
.checked_add(self.layout.snapshot_size as u64)
518-
.ok_or_else(|| {
519-
crate::new_error!(
520-
"snapshot layout overflow: BASE_ADDRESS + snapshot_size ({}) does not fit in u64",
521-
self.layout.snapshot_size
522-
)
523-
})?;
524-
if self.original_entrypoint_addr != 0
525-
&& (self.original_entrypoint_addr < code_lo
526-
|| self.original_entrypoint_addr >= snapshot_hi)
501+
// unknown. Any other value must be within the 47-bit canonical
502+
// user-space range (same as entrypoint_addr), since with ASLR
503+
// or non-PIE the entry point may not be inside the snapshot
504+
// physical region.
505+
if self.original_entrypoint_addr != 0 && self.original_entrypoint_addr > max_gva_entrypoint
527506
{
528507
return Err(crate::new_error!(
529-
"snapshot original entrypoint addr {:#x} is outside the snapshot region [{:#x}, {:#x})",
508+
"snapshot original entrypoint addr {:#x} is outside the valid GVA range (0, {:#x}]",
530509
self.original_entrypoint_addr,
531-
code_lo,
532-
snapshot_hi
510+
max_gva_entrypoint
533511
));
534512
}
535513

src/hyperlight_host/src/sandbox/snapshot/file_tests.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ use serde_json::Value;
2525
use sha2::{Digest as _, Sha256};
2626

2727
use crate::func::Registerable;
28-
use crate::mem::layout::SandboxMemoryLayout;
2928
use crate::sandbox::snapshot::{OciDigest, OciReference, OciTag, Snapshot};
3029
use crate::{GuestBinary, HostFunctions, MultiUseSandbox, UninitializedSandbox};
3130

@@ -2034,14 +2033,11 @@ fn original_entrypoint_addr_zero_accepted() {
20342033
}
20352034

20362035
#[test]
2037-
fn entrypoint_addr_outside_code_rejected() {
2036+
fn entrypoint_addr_outside_canonical_range_rejected() {
20382037
let (_dir, path) = save_for_mutation();
20392038
rewrite_config(&path, |cfg| {
2040-
let code_size = cfg["layout"]["code_size"].as_u64().unwrap();
2041-
let page_size = hyperlight_common::vmem::PAGE_SIZE as u64;
2042-
let peb_addr =
2043-
SandboxMemoryLayout::BASE_ADDRESS as u64 + code_size.next_multiple_of(page_size);
2044-
cfg["entrypoint_addr"] = Value::from(peb_addr);
2039+
// 0x8000_0000_0000 is just above the 47-bit canonical limit
2040+
cfg["entrypoint_addr"] = Value::from(0x8000_0000_0000u64);
20452041
});
20462042
let err = unwrap_err_snapshot(Snapshot::checked_load(
20472043
&path,

src/hyperlight_host/src/sandbox/snapshot/mod.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,6 @@ impl Snapshot {
339339
guest_blob_mem_flags,
340340
)?;
341341

342-
let load_addr = layout.get_guest_code_address() as u64;
343342
let base_va = exe_info.base_va();
344343
let entrypoint_va: u64 = exe_info.entrypoint().into();
345344
let loaded_size = exe_info.loaded_size() as u64;
@@ -354,7 +353,7 @@ impl Snapshot {
354353
let mut memory = vec![0; layout.get_memory_size()?];
355354

356355
let load_info = exe_info.load(
357-
load_addr.try_into()?,
356+
code_virt_base.try_into()?,
358357
&mut memory[layout.guest_code_offset()..],
359358
)?;
360359

@@ -412,7 +411,15 @@ impl Snapshot {
412411
)
413412
})?;
414413

415-
let entrypoint_gva = code_virt_base + entrypoint_offset;
414+
let entrypoint_gva = code_virt_base
415+
.checked_add(entrypoint_offset)
416+
.ok_or_else(|| {
417+
crate::new_error!(
418+
"Entrypoint overflow: code_virt_base {:#x} + offset {:#x}",
419+
code_virt_base,
420+
entrypoint_offset
421+
)
422+
})?;
416423

417424
Ok(Self {
418425
memory: ReadonlySharedMemory::from_bytes(&memory, layout.snapshot_size())?,
@@ -670,6 +677,12 @@ impl Snapshot {
670677
self.original_entrypoint
671678
}
672679

680+
/// Returns the virtual base address of the code region in guest space.
681+
#[allow(dead_code)]
682+
pub(crate) fn code_virt_base(&self) -> u64 {
683+
self.code_virt_base
684+
}
685+
673686
/// Validate that `provided` is a superset of the host functions
674687
/// recorded in this snapshot: every function that was registered
675688
/// at snapshot time must also be present in `provided` with a

0 commit comments

Comments
 (0)