Skip to content

Commit dc6b03f

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 1799d75 commit dc6b03f

6 files changed

Lines changed: 107 additions & 67 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 & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -470,41 +470,31 @@ impl OciSnapshotConfig {
470470
}
471471
}
472472

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

497487
// ELF entry point GVA for `AT_ENTRY` in core dumps. 0 means
498-
// unknown. Any other value must point inside the snapshot
499-
// region, like `entrypoint_addr`.
500-
if self.original_entrypoint_addr != 0
501-
&& (self.original_entrypoint_addr < snap_lo || self.original_entrypoint_addr >= snap_hi)
488+
// unknown. Any other value must be within the 47-bit canonical
489+
// user-space range (same as entrypoint_addr), since with ASLR
490+
// or non-PIE the entry point may not be inside the snapshot
491+
// physical region.
492+
if self.original_entrypoint_addr != 0 && self.original_entrypoint_addr > max_gva_entrypoint
502493
{
503494
return Err(crate::new_error!(
504-
"snapshot original entrypoint addr {:#x} is outside the snapshot region [{:#x}, {:#x})",
495+
"snapshot original entrypoint addr {:#x} is outside the valid GVA range (0, {:#x}]",
505496
self.original_entrypoint_addr,
506-
snap_lo,
507-
snap_hi
497+
max_gva_entrypoint
508498
));
509499
}
510500

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)