Skip to content

Commit 7f2b79e

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 b53a7c5 commit 7f2b79e

8 files changed

Lines changed: 134 additions & 61 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: 3 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];
@@ -1505,6 +1504,7 @@ mod tests {
15051504
ro_mem.to_mgr_snapshot_mem().unwrap(),
15061505
scratch_mem,
15071506
NextAction::Initialise(layout.get_guest_code_address() as u64),
1507+
layout.get_guest_code_address() as u64,
15081508
);
15091509

15101510
let (mut hshm, gshm) = mem_mgr.build().unwrap();

src/hyperlight_host/src/mem/layout.rs

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -562,41 +562,78 @@ impl SandboxMemoryLayout {
562562
/// the virtual base equals the physical load address and no conflict
563563
/// is possible by construction.
564564
///
565-
/// For non-PIE binaries, the code appears at the ELF's declared
566-
/// virtual address (`elf_base_va`), which may differ from the physical
567-
/// load address. This method checks that the resulting virtual range
568-
/// `[elf_base_va, elf_base_va + loaded_size)` does not overlap any
569-
/// non-Code region.
565+
/// Determine the virtual base address of the code region.
566+
///
567+
/// For PIE binaries, a random page-aligned address is chosen within
568+
/// 47-bit canonical user space (ASLR). For non-PIE binaries, the
569+
/// code appears at the ELF's declared virtual address (`elf_base_va`).
570+
///
571+
/// In both cases the resulting virtual range is validated against all
572+
/// non-Code memory regions to prevent overlap.
570573
pub(crate) fn code_virt_base(
571574
&self,
572575
is_pie: bool,
573576
elf_base_va: u64,
574577
loaded_size: u64,
575578
) -> Result<u64> {
576-
let load_addr = self.get_guest_code_address() as u64;
577-
let virt_base = if is_pie { load_addr } else { elf_base_va };
578-
579-
if !is_pie {
580-
let code_virt_end = virt_base + loaded_size;
581-
for rgn in self.get_memory_regions_::<GuestMemoryRegion>(())?.iter() {
582-
if rgn.region_type == MemoryRegionType::Code {
583-
continue;
584-
}
585-
let rgn_start = rgn.guest_region.start as u64;
586-
let rgn_end = rgn_start + rgn.guest_region.len() as u64;
587-
if virt_base < rgn_end && rgn_start < code_virt_end {
588-
return Err(new_error!(
589-
"Non-PIE code mapping [{:#x}, {:#x}) conflicts with {:?} region [{:#x}, {:#x})",
590-
virt_base,
591-
code_virt_end,
592-
rgn.region_type,
593-
rgn_start,
594-
rgn_end,
595-
));
596-
}
579+
let code_size_pages = loaded_size.div_ceil(PAGE_SIZE_USIZE as u64);
580+
let virt_base = if !is_pie {
581+
elf_base_va
582+
} else {
583+
// Pick a random page-aligned address within 47-bit canonical user space.
584+
// Lower bound: 0x1000000 (16 MiB, above all identity-mapped layout regions)
585+
// Upper bound: accounts for code region size so it doesn't overflow
586+
use rand::RngExt;
587+
let mut rng = rand::rng();
588+
let min_page = 0x1000_u64; // 0x1000 * PAGE_SIZE = 0x1000000
589+
let max_page = 0x7_FFFF_FFFF_u64
590+
.checked_sub(code_size_pages)
591+
.ok_or_else(|| {
592+
new_error!(
593+
"PIE code region too large ({} pages) for ASLR randomization",
594+
code_size_pages
595+
)
596+
})?;
597+
let page_number = rng.random_range(min_page..max_page);
598+
page_number
599+
.checked_mul(PAGE_SIZE_USIZE as u64)
600+
.ok_or_else(|| new_error!("ASLR page number overflow"))?
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 = virt_base.checked_add(loaded_size).ok_or_else(|| {
606+
new_error!(
607+
"Code mapping overflow: base {:#x} + size {:#x}",
608+
virt_base,
609+
loaded_size
610+
)
611+
})?;
612+
for rgn in self.get_memory_regions_::<GuestMemoryRegion>(())?.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 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+
virt_base,
622+
code_virt_end,
623+
rgn.region_type,
624+
rgn_start,
625+
rgn_end,
626+
));
597627
}
598628
}
599629

630+
tracing::debug!(
631+
code_virt_base = format_args!("{:#x}", virt_base),
632+
elf_base_va = format_args!("{:#x}", elf_base_va),
633+
is_pie,
634+
"code region virtual base address"
635+
);
636+
600637
Ok(virt_base)
601638
}
602639

@@ -723,6 +760,9 @@ impl SandboxMemoryLayout {
723760
}
724761

725762
/// Guest address of the code section in the sandbox.
763+
/// Used by WHP (Windows) and mem_profile feature; not called on
764+
/// minimal Linux feature sets, hence the allow.
765+
#[allow(dead_code)]
726766
pub(crate) fn get_guest_code_address(&self) -> usize {
727767
Self::BASE_ADDRESS + self.guest_code_offset()
728768
}

src/hyperlight_host/src/mem/mgr.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ pub(crate) struct SandboxMemoryManager<S: SharedMemory> {
150150
/// restored snapshot's own generation number so the guest-visible
151151
/// counter tracks which snapshot the sandbox is a clone of.
152152
pub(crate) snapshot_count: u64,
153+
/// Virtual base address of the code region in guest space
154+
pub(crate) code_virt_base: u64,
153155
}
154156

155157
/// Buffer for building guest page tables during snapshot creation.
@@ -276,6 +278,7 @@ where
276278
shared_mem: SnapshotSharedMemory<S>,
277279
scratch_mem: S,
278280
entrypoint: NextAction,
281+
code_virt_base: u64,
279282
) -> Self {
280283
Self {
281284
layout,
@@ -284,6 +287,7 @@ where
284287
entrypoint,
285288
abort_buffer: Vec::new(),
286289
snapshot_count: 0,
290+
code_virt_base,
287291
}
288292
}
289293

@@ -316,6 +320,7 @@ where
316320
entrypoint,
317321
self.snapshot_count,
318322
host_functions,
323+
self.code_virt_base,
319324
)
320325
}
321326
}
@@ -326,7 +331,8 @@ impl SandboxMemoryManager<ExclusiveSharedMemory> {
326331
let shared_mem = s.memory().to_mgr_snapshot_mem()?;
327332
let scratch_mem = ExclusiveSharedMemory::new(s.layout().get_scratch_size())?;
328333
let entrypoint = s.entrypoint();
329-
let mut mgr = Self::new(layout, shared_mem, scratch_mem, entrypoint);
334+
let code_virt_base = s.code_virt_base();
335+
let mut mgr = Self::new(layout, shared_mem, scratch_mem, entrypoint, code_virt_base);
330336
// Inherit the snapshot's generation number for the same
331337
// reason `restore_snapshot` does: the guest-visible counter
332338
// reflects "which snapshot is the sandbox currently a clone
@@ -360,6 +366,7 @@ impl SandboxMemoryManager<ExclusiveSharedMemory> {
360366
entrypoint: self.entrypoint,
361367
abort_buffer: self.abort_buffer,
362368
snapshot_count: self.snapshot_count,
369+
code_virt_base: self.code_virt_base,
363370
};
364371
let guest_mgr = SandboxMemoryManager {
365372
shared_mem: gshm,
@@ -368,6 +375,7 @@ impl SandboxMemoryManager<ExclusiveSharedMemory> {
368375
entrypoint: self.entrypoint,
369376
abort_buffer: Vec::new(), // Guest doesn't need abort buffer
370377
snapshot_count: self.snapshot_count,
378+
code_virt_base: self.code_virt_base,
371379
};
372380
host_mgr.update_scratch_bookkeeping()?;
373381
Ok((host_mgr, guest_mgr))

src/hyperlight_host/src/sandbox/initialized_multi_use.rs

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

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

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

@@ -2017,7 +2020,7 @@ mod tests {
20172020
#[cfg(feature = "trace_guest")]
20182021
fn read_guest_memory_by_gva_unaligned_cross_page() {
20192022
let mut sbox = sandbox_for_gva_tests();
2020-
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2023+
let code_gva = sbox.mem_mgr.code_virt_base;
20212024
// Start 1 byte before the second page boundary and read 4097 bytes
20222025
// (spans 2 full page boundaries).
20232026
let start = code_gva + 4096 - 1;
@@ -2033,7 +2036,7 @@ mod tests {
20332036
#[cfg(feature = "trace_guest")]
20342037
fn read_guest_memory_by_gva_two_full_pages() {
20352038
let mut sbox = sandbox_for_gva_tests();
2036-
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2039+
let code_gva = sbox.mem_mgr.code_virt_base;
20372040
assert_gva_read_matches(&mut sbox, code_gva, 4096 * 2);
20382041
}
20392042

@@ -2044,7 +2047,7 @@ mod tests {
20442047
#[cfg(feature = "trace_guest")]
20452048
fn read_guest_memory_by_gva_cross_page_boundary() {
20462049
let mut sbox = sandbox_for_gva_tests();
2047-
let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64;
2050+
let code_gva = sbox.mem_mgr.code_virt_base;
20482051
// Start 100 bytes before the first page boundary, read across it.
20492052
let start = code_gva + 4096 - 100;
20502053
assert_gva_read_matches(&mut sbox, start, 200);

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

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -456,27 +456,17 @@ impl OciSnapshotConfig {
456456
}
457457
}
458458

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

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,12 @@ impl Snapshot {
871871
load_info: crate::mem::exe::LoadInfo::dummy(),
872872
stack_top_gva: cfg.stack_top_gva,
873873
sregs: Some(cfg.sregs),
874+
// TODO(#1655): File-snapshot format doesn't persist code_virt_base.
875+
// This is acceptable because file snapshots currently pre-date
876+
// ASLR and always use identity-mapped code (VA == GPA). If file
877+
// snapshots ever need to round-trip through an ASLR sandbox, the
878+
// format must be extended to include code_virt_base.
879+
code_virt_base: 0,
874880
entrypoint,
875881
snapshot_generation,
876882
host_functions,

0 commit comments

Comments
 (0)