Skip to content

Commit 8c610e3

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 e630b8d commit 8c610e3

7 files changed

Lines changed: 110 additions & 86 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
@@ -675,10 +675,9 @@ pub(super) mod debug {
675675
.dbg_mem_access_fn
676676
.try_lock()
677677
.map_err(|_| ProcessDebugRequestError::TryLockError(file!(), line!()))?
678-
.layout
679-
.get_guest_code_address();
678+
.code_virt_base;
680679

681-
Ok(DebugResponse::GetCodeSectionOffset(offset as u64))
680+
Ok(DebugResponse::GetCodeSectionOffset(offset))
682681
}
683682
DebugMsg::ReadAddr(addr, len) => {
684683
let mut data = vec![0u8; len];

src/hyperlight_host/src/mem/layout.rs

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

583601
let mut regions = self.get_memory_regions_::<GuestMemoryRegion>(())?;
584602

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

@@ -610,6 +634,13 @@ impl SandboxMemoryLayout {
610634
}
611635
}
612636

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

@@ -736,6 +767,9 @@ impl SandboxMemoryLayout {
736767
}
737768

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

src/hyperlight_host/src/sandbox/initialized_multi_use.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1977,9 +1977,12 @@ mod tests {
19771977
/// `read_guest_memory_by_gva`, then assert both views are identical.
19781978
#[cfg(feature = "trace_guest")]
19791979
fn assert_gva_read_matches(sbox: &mut MultiUseSandbox, gva: u64, len: usize) {
1980-
// Guest reads via its own page tables
1980+
// Guest reads via its own page tables.
1981+
// do_map = false: the code region is already mapped (identity-mapped
1982+
// or ASLR-mapped), so we must not remap it with an identity mapping
1983+
// that would use the GVA as a physical address.
19811984
let expected: Vec<u8> = sbox
1982-
.call("ReadMappedBuffer", (gva, len as u64, true))
1985+
.call("ReadMappedBuffer", (gva, len as u64, false))
19831986
.unwrap();
19841987
assert_eq!(expected.len(), len);
19851988

@@ -2003,7 +2006,7 @@ mod tests {
20032006
#[cfg(feature = "trace_guest")]
20042007
fn read_guest_memory_by_gva_single_page() {
20052008
let mut sbox = sandbox_for_gva_tests();
2006-
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2009+
let code_gva = sbox.mem_mgr.code_virt_base;
20072010
assert_gva_read_matches(&mut sbox, code_gva, 128);
20082011
}
20092012

@@ -2013,7 +2016,7 @@ mod tests {
20132016
#[cfg(feature = "trace_guest")]
20142017
fn read_guest_memory_by_gva_full_page() {
20152018
let mut sbox = sandbox_for_gva_tests();
2016-
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2019+
let code_gva = sbox.mem_mgr.code_virt_base;
20172020
assert_gva_read_matches(&mut sbox, code_gva, 4096);
20182021
}
20192022

@@ -2023,7 +2026,7 @@ mod tests {
20232026
#[cfg(feature = "trace_guest")]
20242027
fn read_guest_memory_by_gva_unaligned_cross_page() {
20252028
let mut sbox = sandbox_for_gva_tests();
2026-
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2029+
let code_gva = sbox.mem_mgr.code_virt_base;
20272030
// Start 1 byte before the second page boundary and read 4097 bytes
20282031
// (spans 2 full page boundaries).
20292032
let start = code_gva + 4096 - 1;
@@ -2039,7 +2042,7 @@ mod tests {
20392042
#[cfg(feature = "trace_guest")]
20402043
fn read_guest_memory_by_gva_two_full_pages() {
20412044
let mut sbox = sandbox_for_gva_tests();
2042-
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2045+
let code_gva = sbox.mem_mgr.code_virt_base;
20432046
assert_gva_read_matches(&mut sbox, code_gva, 4096 * 2);
20442047
}
20452048

@@ -2050,7 +2053,7 @@ mod tests {
20502053
#[cfg(feature = "trace_guest")]
20512054
fn read_guest_memory_by_gva_cross_page_boundary() {
20522055
let mut sbox = sandbox_for_gva_tests();
2053-
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2056+
let code_gva = sbox.mem_mgr.code_virt_base;
20542057
// Start 100 bytes before the first page boundary, read across it.
20552058
let start = code_gva + 4096 - 100;
20562059
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
@@ -476,53 +476,31 @@ impl OciSnapshotConfig {
476476
}
477477
}
478478

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

506493
// ELF entry point GVA for `AT_ENTRY` in core dumps. 0 means
507-
// unknown. Any other value must point inside the snapshot
508-
// region, like `entrypoint_addr`.
509-
let snapshot_hi = code_lo
510-
.checked_add(self.layout.snapshot_size as u64)
511-
.ok_or_else(|| {
512-
crate::new_error!(
513-
"snapshot layout overflow: BASE_ADDRESS + snapshot_size ({}) does not fit in u64",
514-
self.layout.snapshot_size
515-
)
516-
})?;
517-
if self.original_entrypoint_addr != 0
518-
&& (self.original_entrypoint_addr < code_lo
519-
|| self.original_entrypoint_addr >= snapshot_hi)
494+
// unknown. Any other value must be within the 47-bit canonical
495+
// user-space range (same as entrypoint_addr), since with ASLR
496+
// or non-PIE the entry point may not be inside the snapshot
497+
// physical region.
498+
if self.original_entrypoint_addr != 0 && self.original_entrypoint_addr > max_gva_entrypoint
520499
{
521500
return Err(crate::new_error!(
522-
"snapshot original entrypoint addr {:#x} is outside the snapshot region [{:#x}, {:#x})",
501+
"snapshot original entrypoint addr {:#x} is outside the valid GVA range (0, {:#x}]",
523502
self.original_entrypoint_addr,
524-
code_lo,
525-
snapshot_hi
503+
max_gva_entrypoint
526504
));
527505
}
528506

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

@@ -1876,14 +1875,11 @@ fn original_entrypoint_addr_zero_accepted() {
18761875
}
18771876

18781877
#[test]
1879-
fn entrypoint_addr_outside_code_rejected() {
1878+
fn entrypoint_addr_outside_canonical_range_rejected() {
18801879
let (_dir, path) = save_for_mutation();
18811880
rewrite_config(&path, |cfg| {
1882-
let code_size = cfg["layout"]["code_size"].as_u64().unwrap();
1883-
let page_size = hyperlight_common::vmem::PAGE_SIZE as u64;
1884-
let peb_addr =
1885-
SandboxMemoryLayout::BASE_ADDRESS as u64 + code_size.next_multiple_of(page_size);
1886-
cfg["entrypoint_addr"] = Value::from(peb_addr);
1881+
// 0x8000_0000_0000 is just above the 47-bit canonical limit
1882+
cfg["entrypoint_addr"] = Value::from(0x8000_0000_0000u64);
18871883
});
18881884
let err = unwrap_err_snapshot(Snapshot::checked_load(
18891885
&path,

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,6 @@ impl Snapshot {
333333
guest_blob_mem_flags,
334334
)?;
335335

336-
let load_addr = layout.get_guest_code_address() as u64;
337336
let base_va = exe_info.base_va();
338337
let entrypoint_va: u64 = exe_info.entrypoint().into();
339338
let loaded_size = exe_info.loaded_size() as u64;
@@ -348,7 +347,7 @@ impl Snapshot {
348347
let mut memory = vec![0; layout.get_memory_size()?];
349348

350349
let load_info = exe_info.load(
351-
load_addr.try_into()?,
350+
code_virt_base.try_into()?,
352351
&mut memory[layout.guest_code_offset()..],
353352
)?;
354353

@@ -406,7 +405,15 @@ impl Snapshot {
406405
)
407406
})?;
408407

409-
let entrypoint_gva = code_virt_base + entrypoint_offset;
408+
let entrypoint_gva = code_virt_base
409+
.checked_add(entrypoint_offset)
410+
.ok_or_else(|| {
411+
crate::new_error!(
412+
"Entrypoint overflow: code_virt_base {:#x} + offset {:#x}",
413+
code_virt_base,
414+
entrypoint_offset
415+
)
416+
})?;
410417

411418
Ok(Self {
412419
memory: ReadonlySharedMemory::from_bytes(&memory, layout.snapshot_size())?,
@@ -653,6 +660,12 @@ impl Snapshot {
653660
self.original_entrypoint
654661
}
655662

663+
/// Returns the virtual base address of the code region in guest space.
664+
#[allow(dead_code)]
665+
pub(crate) fn code_virt_base(&self) -> u64 {
666+
self.code_virt_base
667+
}
668+
656669
/// Validate that `provided` is a superset of the host functions
657670
/// recorded in this snapshot: every function that was registered
658671
/// at snapshot time must also be present in `provided` with a

0 commit comments

Comments
 (0)