From 91b4cf88c6363b79fd7597e2f4cd2879ce35a0ad Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:16:08 -0700 Subject: [PATCH 1/4] feat: support non-PIE ELF loading at declared virtual addresses Add support for running non-PIE (ET_EXEC) guest binaries by mapping code at the ELF's declared virtual address rather than assuming identity mapping (physical == virtual). Changes: - Add is_pie() and base_va() methods to ExeInfo/ElfInfo to detect ET_DYN vs ET_EXEC binaries and extract the base virtual address - Add SandboxMemoryLayout::code_virt_base() to compute the correct virtual base for the code region and validate it doesn't conflict with other memory regions - Update snapshot creation to use non-identity virtual mapping for non-PIE code regions - Add non-PIE guest build step to CI (cargo hyperlight with -C relocation-model=static -C link-args=--no-pie) - Add integration test verifying non-PIE guest execution - Add test helper for locating non-PIE guest binaries 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> --- .github/workflows/dep_build_guests.yml | 7 ++ .gitignore | 1 + Justfile | 18 +++++- .../src/hypervisor/crashdump.rs | 1 + src/hyperlight_host/src/hypervisor/gdb/mod.rs | 1 + src/hyperlight_host/src/mem/elf.rs | 9 +++ src/hyperlight_host/src/mem/exe.rs | 6 ++ src/hyperlight_host/src/mem/layout.rs | 64 ++++++++++++++++++- src/hyperlight_host/src/mem/memory_region.rs | 11 +++- src/hyperlight_host/src/mem/mgr.rs | 11 ++++ src/hyperlight_host/src/mem/shared_mem.rs | 1 + .../src/sandbox/file_mapping.rs | 2 + .../src/sandbox/initialized_multi_use.rs | 1 + .../src/sandbox/snapshot/file/config.rs | 40 +++++++----- .../src/sandbox/snapshot/file/mod.rs | 2 + .../src/sandbox/snapshot/file_tests.rs | 25 ++++++-- .../src/sandbox/snapshot/mod.rs | 37 +++++++++-- src/hyperlight_host/tests/integration_test.rs | 19 +++++- src/hyperlight_testing/src/lib.rs | 31 +++++++++ 19 files changed, 254 insertions(+), 33 deletions(-) diff --git a/.github/workflows/dep_build_guests.yml b/.github/workflows/dep_build_guests.yml index 41cb58738..1c110028d 100644 --- a/.github/workflows/dep_build_guests.yml +++ b/.github/workflows/dep_build_guests.yml @@ -62,6 +62,7 @@ jobs: with: path: | src/tests/rust_guests/target/sysroot + src/tests/rust_guests/target-non-pie/sysroot key: sysroot-linux-${{ inputs.arch }}-${{ inputs.config }}-${{ hashFiles('rust-toolchain.toml') }} - name: Rust cache @@ -87,6 +88,12 @@ jobs: just build-rust-guests ${{ inputs.config }} just move-rust-guests ${{ inputs.config }} + - name: Build non-PIE Rust guests + if: inputs.arch == 'X64' + run: | + just build-rust-guests-non-pie ${{ inputs.config }} + just move-rust-guests-non-pie ${{ inputs.config }} + - name: Build C guests run: | just build-c-guests ${{ inputs.config }} diff --git a/.gitignore b/.gitignore index 3aae7b792..27126b8de 100644 --- a/.gitignore +++ b/.gitignore @@ -454,6 +454,7 @@ $RECYCLE.BIN/ # Rust build artifacts **/**target +**/**target-non-pie libhyperlight_host.so libhyperlight_host.d hyperlight_host.dll diff --git a/Justfile b/Justfile index f69d88c41..f2f5cca2d 100644 --- a/Justfile +++ b/Justfile @@ -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` @@ -75,6 +75,22 @@ 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. +# 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: diff --git a/src/hyperlight_host/src/hypervisor/crashdump.rs b/src/hyperlight_host/src/hypervisor/crashdump.rs index 391c3673c..df0de1fc9 100644 --- a/src/hyperlight_host/src/hypervisor/crashdump.rs +++ b/src/hyperlight_host/src/hypervisor/crashdump.rs @@ -474,6 +474,7 @@ mod test { let ptr = dummy_vec.as_ptr() as usize; let regions = vec![CrashDumpRegion { guest_region: 0x1000..0x2000, + guest_virt_addr: 0x1000, host_region: ptr..ptr + dummy_vec.len(), flags: MemoryRegionFlags::READ | MemoryRegionFlags::WRITE, region_type: crate::mem::memory_region::MemoryRegionType::Code, diff --git a/src/hyperlight_host/src/hypervisor/gdb/mod.rs b/src/hyperlight_host/src/hypervisor/gdb/mod.rs index 5f82be0c3..76f80f4fa 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/mod.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/mod.rs @@ -426,6 +426,7 @@ mod tests { guest_mmap_regions: vec![MemoryRegion { host_region: mapped_mem as usize..mapped_mem.wrapping_add(size) as usize, guest_region: BASE_VIRT..BASE_VIRT + size, + guest_virt_addr: BASE_VIRT, flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE, region_type: MemoryRegionType::Heap, }], diff --git a/src/hyperlight_host/src/mem/elf.rs b/src/hyperlight_host/src/mem/elf.rs index 7309412f1..aba4e6cb2 100644 --- a/src/hyperlight_host/src/mem/elf.rs +++ b/src/hyperlight_host/src/mem/elf.rs @@ -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")] @@ -42,6 +43,8 @@ pub(crate) struct ElfInfo { shdrs: Vec, entry: u64, relocs: Vec, + /// 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, @@ -143,6 +146,7 @@ impl ElfInfo { .collect(), entry: elf.entry, relocs, + is_pie: elf.header.e_type == ET_DYN, guest_bin_version, }) } @@ -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> { diff --git a/src/hyperlight_host/src/mem/exe.rs b/src/hyperlight_host/src/mem/exe.rs index 9ee316642..17789ad4e 100644 --- a/src/hyperlight_host/src/mem/exe.rs +++ b/src/hyperlight_host/src/mem/exe.rs @@ -89,6 +89,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 { diff --git a/src/hyperlight_host/src/mem/layout.rs b/src/hyperlight_host/src/mem/layout.rs index 6422b9b11..e802805e2 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -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; @@ -555,6 +555,64 @@ impl SandboxMemoryLayout { Ok(builder.build()) } + /// Compute the virtual base address for the code region, validate + /// that it does not overlap any other memory region, and return the + /// guest memory regions with the Code region's `guest_virt_addr` + /// already set to the computed virtual base. + /// + /// 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. + /// + /// For non-PIE binaries, the code appears at the ELF's declared + /// virtual address (`elf_base_va`), which may differ from the physical + /// load address. This method checks that the resulting virtual range + /// `[elf_base_va, elf_base_va + loaded_size)` does not overlap any + /// non-Code region. + /// + /// Returns `(code_virt_base, regions)`. + pub(crate) fn get_guest_regions_with_code_va( + &self, + is_pie: bool, + elf_base_va: u64, + loaded_size: u64, + ) -> Result<(u64, Vec>)> { + let load_addr = self.get_guest_code_address() as u64; + let code_virt_base = if is_pie { load_addr } else { elf_base_va }; + + let mut regions = self.get_memory_regions_::(())?; + + if !is_pie { + let code_virt_end = code_virt_base + loaded_size; + for rgn in regions.iter() { + if rgn.region_type == MemoryRegionType::Code { + continue; + } + let rgn_start = rgn.guest_region.start as u64; + let rgn_end = rgn_start + rgn.guest_region.len() as u64; + if code_virt_base < rgn_end && rgn_start < code_virt_end { + return Err(new_error!( + "Non-PIE code mapping [{:#x}, {:#x}) conflicts with {:?} region [{:#x}, {:#x})", + code_virt_base, + code_virt_end, + rgn.region_type, + rgn_start, + rgn_end, + )); + } + } + } + + // Set the Code region's guest_virt_addr to code_virt_base. + for rgn in regions.iter_mut() { + if rgn.region_type == MemoryRegionType::Code { + rgn.guest_virt_addr = code_virt_base as usize; + } + } + + Ok((code_virt_base, regions)) + } + #[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] diff --git a/src/hyperlight_host/src/mem/memory_region.rs b/src/hyperlight_host/src/mem/memory_region.rs index 5d839647a..a17dffd27 100644 --- a/src/hyperlight_host/src/mem/memory_region.rs +++ b/src/hyperlight_host/src/mem/memory_region.rs @@ -291,8 +291,12 @@ impl MemoryRegionKind for GuestMemoryRegion { /// the same memory permissions #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct MemoryRegion_ { - /// the range of guest memory addresses + /// the range of guest physical addresses pub guest_region: Range, + /// the guest virtual address at which this region should be mapped. + /// For identity-mapped regions this equals `guest_region.start`. + /// For non-PIE code it is the ELF's declared virtual address. + pub guest_virt_addr: usize, /// the range of host memory addresses /// /// Note that Range<()> = () x () = (). @@ -356,6 +360,7 @@ impl MemoryRegionVecBuilder { let host_end = ::add(self.host_base_virt_addr, size); self.regions.push(MemoryRegion_ { guest_region: self.guest_base_phys_addr..guest_end, + guest_virt_addr: self.guest_base_phys_addr, host_region: self.host_base_virt_addr..host_end, flags, region_type, @@ -367,8 +372,10 @@ impl MemoryRegionVecBuilder { // we know this is safe because we check if the regions are empty above let last_region = self.regions.last().unwrap(); let host_end = ::add(last_region.host_region.end, size); + let guest_start = last_region.guest_region.end; let new_region = MemoryRegion_ { - guest_region: last_region.guest_region.end..last_region.guest_region.end + size, + guest_region: guest_start..guest_start + size, + guest_virt_addr: guest_start, host_region: last_region.host_region.end..host_end, flags, region_type, diff --git a/src/hyperlight_host/src/mem/mgr.rs b/src/hyperlight_host/src/mem/mgr.rs index c93f1cac1..d0d96964b 100644 --- a/src/hyperlight_host/src/mem/mgr.rs +++ b/src/hyperlight_host/src/mem/mgr.rs @@ -147,6 +147,10 @@ pub(crate) struct SandboxMemoryManager { /// preserved across the `Initialise` -> `Call` transition so it /// can fill `AT_ENTRY` in guest core dumps. 0 if unknown. pub(crate) original_entrypoint: u64, + /// Virtual base address of the code region. + /// For PIE binaries this equals the physical load address (identity-mapped). + /// For non-PIE binaries this is the ELF-declared base VA. + pub(crate) code_virt_base: u64, /// Buffer for accumulating guest abort messages pub(crate) abort_buffer: Vec, /// Generation counter: how many snapshots have been taken from @@ -288,6 +292,7 @@ where scratch_mem, next_action, original_entrypoint: 0, + code_virt_base: 0, abort_buffer: Vec::new(), snapshot_count: 0, } @@ -323,6 +328,7 @@ where #[cfg(target_arch = "x86_64")] msrs, next_action, + self.code_virt_base, self.original_entrypoint, self.snapshot_count, host_functions, @@ -338,6 +344,7 @@ impl SandboxMemoryManager { let next_action = s.next_action(); let mut mgr = Self::new(layout, shared_mem, scratch_mem, next_action); mgr.original_entrypoint = s.original_entrypoint(); + mgr.code_virt_base = s.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 @@ -370,6 +377,7 @@ impl SandboxMemoryManager { layout: self.layout, next_action: self.next_action, original_entrypoint: self.original_entrypoint, + code_virt_base: self.code_virt_base, abort_buffer: self.abort_buffer, snapshot_count: self.snapshot_count, }; @@ -379,6 +387,7 @@ impl SandboxMemoryManager { layout: self.layout, next_action: self.next_action, original_entrypoint: self.original_entrypoint, + code_virt_base: self.code_virt_base, abort_buffer: Vec::new(), // Guest doesn't need abort buffer snapshot_count: self.snapshot_count, }; @@ -519,6 +528,7 @@ impl SandboxMemoryManager { // Carry the guest ELF entry point across restore so crashdumps // report the restored image's entry. self.original_entrypoint = snapshot.original_entrypoint(); + self.code_virt_base = snapshot.code_virt_base; self.update_scratch_bookkeeping()?; Ok((gsnapshot, gscratch)) @@ -640,6 +650,7 @@ impl SandboxMemoryManager { regions.push(CrashDumpRegion { guest_region: virt_base..virt_end, + guest_virt_addr: virt_base, host_region: host_base..host_base + host_len, flags, region_type, diff --git a/src/hyperlight_host/src/mem/shared_mem.rs b/src/hyperlight_host/src/mem/shared_mem.rs index 4b706cc41..e47c4380a 100644 --- a/src/hyperlight_host/src/mem/shared_mem.rs +++ b/src/hyperlight_host/src/mem/shared_mem.rs @@ -514,6 +514,7 @@ fn mapping_at( MemoryRegion { guest_region: guest_base..(guest_base + size), + guest_virt_addr: guest_base, host_region: s.host_region_base() ..::add(s.host_region_base(), size), region_type, diff --git a/src/hyperlight_host/src/sandbox/file_mapping.rs b/src/hyperlight_host/src/sandbox/file_mapping.rs index 4f3ed2a4d..d0e367074 100644 --- a/src/hyperlight_host/src/sandbox/file_mapping.rs +++ b/src/hyperlight_host/src/sandbox/file_mapping.rs @@ -164,6 +164,7 @@ impl PreparedFileMapping { Ok(MemoryRegion { host_region: host_base..host_end, guest_region: guest_start..guest_end, + guest_virt_addr: guest_start, flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE, region_type: MemoryRegionType::MappedFile, }) @@ -184,6 +185,7 @@ impl PreparedFileMapping { host_region: *mmap_base as usize ..(*mmap_base as usize).wrapping_add(*mmap_size), guest_region: guest_start..guest_end, + guest_virt_addr: guest_start, flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE, region_type: MemoryRegionType::MappedFile, }) diff --git a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs index 623c24667..be268818e 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -1587,6 +1587,7 @@ mod tests { MemoryRegion { host_region: mem.host_region_base()..mem.host_region_end(), guest_region: guest_base..(guest_base + len), + guest_virt_addr: guest_base, flags, region_type: MemoryRegionType::Heap, } diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs index 01c2f501b..5c2da1bac 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs @@ -187,6 +187,12 @@ pub(super) struct OciSnapshotConfig { /// treats as unknown. #[serde(default)] pub(super) original_entrypoint_addr: u64, + /// Virtual base address of the code region. For PIE guests this equals + /// the physical load address; for non-PIE guests it is the ELF-declared + /// base VA. Optional: older snapshots deserialize to `0`, meaning + /// identity-mapped (VA == GPA). + #[serde(default)] + pub(super) code_virt_base: u64, /// Special registers captured from the paused vCPU, restored /// verbatim when resuming the call. pub(super) sregs: CommonSpecialRegisters, @@ -478,13 +484,19 @@ impl OciSnapshotConfig { } // The saved dispatch entrypoint must be in the executable code - // region. Code occupies the page-rounded prefix of the snapshot. - let code_lo = SandboxMemoryLayout::BASE_ADDRESS as u64; + // region. For non-PIE or ASLR guests the code region's virtual + // base differs from the physical load address. + let code_lo = if self.code_virt_base != 0 { + self.code_virt_base + } else { + SandboxMemoryLayout::BASE_ADDRESS as u64 + }; let code_hi = code_lo .checked_add(self.layout.code_size.next_multiple_of(PAGE_SIZE) as u64) .ok_or_else(|| { crate::new_error!( - "snapshot layout overflow: BASE_ADDRESS + code_size ({}) does not fit in u64", + "snapshot layout overflow: code_virt_base ({:#x}) + code_size ({}) does not fit in u64", + code_lo, self.layout.code_size ) })?; @@ -505,25 +517,16 @@ impl OciSnapshotConfig { } // ELF entry point GVA for `AT_ENTRY` in core dumps. 0 means - // unknown. Any other value must point inside the snapshot - // region, like `entrypoint_addr`. - let snapshot_hi = code_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 - ) - })?; + // unknown. Any other value must point inside the code region, + // like `entrypoint_addr`. if self.original_entrypoint_addr != 0 - && (self.original_entrypoint_addr < code_lo - || self.original_entrypoint_addr >= snapshot_hi) + && (self.original_entrypoint_addr < code_lo || self.original_entrypoint_addr >= code_hi) { return Err(crate::new_error!( - "snapshot original entrypoint addr {:#x} is outside the snapshot region [{:#x}, {:#x})", + "snapshot original entrypoint addr {:#x} is outside the code region [{:#x}, {:#x})", self.original_entrypoint_addr, code_lo, - snapshot_hi + code_hi )); } @@ -763,6 +766,7 @@ mod tests { stack_top_gva: 0x2000, entrypoint_addr: SandboxMemoryLayout::BASE_ADDRESS as u64, original_entrypoint_addr: 0, + code_virt_base: 0, sregs: distinct_sregs(), #[cfg(target_arch = "x86_64")] msrs: Vec::new(), @@ -846,6 +850,7 @@ mod schema_pin { "stack_top_gva": 3735928559, "entrypoint_addr": 8192, "original_entrypoint_addr": 0, + "code_virt_base": 0, "sregs": { "cs": { "base": 1, @@ -1032,6 +1037,7 @@ mod schema_pin { "stack_top_gva": 3735928559, "entrypoint_addr": 8192, "original_entrypoint_addr": 0, + "code_virt_base": 0, "sregs": { "tcr_el1": 1, "mair_el1": 2, diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs index 2a0f00d2f..064d15a80 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs @@ -610,6 +610,7 @@ impl Snapshot { stack_top_gva: self.stack_top_gva, entrypoint_addr, original_entrypoint_addr: self.original_entrypoint, + code_virt_base: self.code_virt_base, sregs: *sregs, #[cfg(target_arch = "x86_64")] msrs: self @@ -907,6 +908,7 @@ impl Snapshot { msrs: Some(cfg.msrs), next_action, original_entrypoint: cfg.original_entrypoint_addr, + code_virt_base: cfg.code_virt_base, snapshot_generation, host_functions, }) diff --git a/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs b/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs index 4e0604c86..23a7c0b62 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs @@ -25,7 +25,6 @@ use serde_json::Value; use sha2::{Digest as _, Sha256}; use crate::func::Registerable; -use crate::mem::layout::SandboxMemoryLayout; use crate::sandbox::snapshot::{OciDigest, OciReference, OciTag, Snapshot}; use crate::{GuestBinary, HostFunctions, MultiUseSandbox, UninitializedSandbox}; @@ -2037,11 +2036,11 @@ fn original_entrypoint_addr_zero_accepted() { fn entrypoint_addr_outside_code_rejected() { let (_dir, path) = save_for_mutation(); rewrite_config(&path, |cfg| { + let code_virt_base = cfg["code_virt_base"].as_u64().unwrap(); let code_size = cfg["layout"]["code_size"].as_u64().unwrap(); let page_size = hyperlight_common::vmem::PAGE_SIZE as u64; - let peb_addr = - SandboxMemoryLayout::BASE_ADDRESS as u64 + code_size.next_multiple_of(page_size); - cfg["entrypoint_addr"] = Value::from(peb_addr); + let beyond_code = code_virt_base + code_size.next_multiple_of(page_size); + cfg["entrypoint_addr"] = Value::from(beyond_code); }); let err = unwrap_err_snapshot(Snapshot::checked_load( &path, @@ -2954,6 +2953,24 @@ fn save_returns_manifest_digest_that_loads() { assert_eq!(loaded.snapshot_generation(), expected_gen); } +/// `code_virt_base` must survive a save/load round-trip so GDB and +/// tracing can resolve symbols for non-PIE (or ASLR) guests after +/// restoring from a file snapshot. +#[test] +fn round_trip_preserves_code_virt_base() { + let snap = create_snapshot(); + // Default PIE guest is identity-mapped, so code_virt_base should + // equal get_guest_code_address (i.e. the GPA of the code region). + let original = snap.code_virt_base; + assert_ne!(original, 0, "fixture must have a non-zero code_virt_base"); + + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("layout"); + snap.save(&path, &OciTag::new("latest").unwrap()).unwrap(); + let loaded = Snapshot::checked_load(&path, OciTag::new("latest").unwrap()).unwrap(); + assert_eq!(loaded.code_virt_base, original); +} + /// The returned digest is the sha256 of the manifest blob, matching the /// digest recorded for that tag's manifest descriptor in `index.json`. #[test] diff --git a/src/hyperlight_host/src/sandbox/snapshot/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/mod.rs index e1578e2e7..8b0018844 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/mod.rs @@ -36,7 +36,7 @@ use crate::hypervisor::regs::CommonSpecialRegisters; use crate::hypervisor::regs::MsrEntry; use crate::mem::exe::{ExeInfo, LoadInfo}; use crate::mem::layout::SandboxMemoryLayout; -use crate::mem::memory_region::{GuestMemoryRegion, MemoryRegion, MemoryRegionFlags}; +use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; use crate::mem::mgr::{GuestPageTableBuffer, SnapshotSharedMemory}; use crate::mem::shared_mem::{ReadonlySharedMemory, SharedMemory}; use crate::sandbox::SandboxConfiguration; @@ -101,8 +101,13 @@ pub struct Snapshot { /// The next action that should be performed on this snapshot next_action: NextAction, + /// Virtual base address of the code region. + /// For PIE binaries this equals the physical load address (identity-mapped). + /// For non-PIE binaries this is the ELF-declared base VA. + pub(crate) code_virt_base: u64, + /// Guest virtual address of the guest binary's ELF entry point - /// (`load_addr + e_entry - base_va`). Unlike `next_action`, which + /// (`code_virt_base + e_entry - base_va`). Unlike `next_action`, which /// transitions to `Call(dispatch_addr)` once the guest has run, /// this preserves the original entry across that transition. Used /// to fill `AT_ENTRY` in guest core dumps so a debugger can @@ -337,6 +342,14 @@ impl Snapshot { let load_addr = layout.get_guest_code_address() as u64; let base_va = exe_info.base_va(); let entrypoint_va: u64 = exe_info.entrypoint().into(); + let loaded_size = exe_info.loaded_size() as u64; + let is_pie = exe_info.is_pie(); + + // Get the memory regions with the Code region's guest_virt_addr + // already set to the correct virtual base (identity-mapped for PIE, + // ELF-declared VA for non-PIE), and validate no overlap conflicts. + let (code_virt_base, regions) = + layout.get_guest_regions_with_code_va(is_pie, base_va, loaded_size)?; let mut memory = vec![0; layout.get_memory_size()?]; @@ -354,7 +367,7 @@ impl Snapshot { let pt_buf = GuestPageTableBuffer::new(layout.get_pt_base_gpa() as usize); // 1. Map the (ideally readonly) pages of snapshot data - for rgn in layout.get_memory_regions_::(())?.iter() { + for rgn in regions.iter() { let readable = rgn.flags.contains(MemoryRegionFlags::READ); let executable = rgn.flags.contains(MemoryRegionFlags::EXECUTE); let writable = rgn.flags.contains(MemoryRegionFlags::WRITE); @@ -370,9 +383,10 @@ impl Snapshot { executable, }) }; + let mapping = Mapping { phys_base: rgn.guest_region.start as u64, - virt_base: rgn.guest_region.start as u64, + virt_base: rgn.guest_virt_addr as u64, len: rgn.guest_region.len() as u64, kind, }; @@ -390,7 +404,15 @@ impl Snapshot { - hyperlight_common::layout::SCRATCH_TOP_EXN_STACK_OFFSET + 1; - let entrypoint_gva = load_addr + entrypoint_va - base_va; + let entrypoint_offset = entrypoint_va.checked_sub(base_va).ok_or_else(|| { + crate::new_error!( + "ELF entrypoint VA ({:#x}) is below base VA ({:#x})", + entrypoint_va, + base_va + ) + })?; + + let entrypoint_gva = code_virt_base + entrypoint_offset; Ok(Self { memory: ReadonlySharedMemory::from_bytes(&memory, layout.snapshot_size())?, @@ -401,6 +423,7 @@ impl Snapshot { #[cfg(target_arch = "x86_64")] msrs: None, next_action: NextAction::Initialise(entrypoint_gva), + code_virt_base, original_entrypoint: entrypoint_gva, snapshot_generation: 0, host_functions: HostFunctionDetails { @@ -429,6 +452,7 @@ impl Snapshot { sregs: CommonSpecialRegisters, #[cfg(target_arch = "x86_64")] msrs: Vec, next_action: NextAction, + code_virt_base: u64, original_entrypoint: u64, snapshot_generation: u64, host_functions: HostFunctionDetails, @@ -585,6 +609,7 @@ impl Snapshot { #[cfg(target_arch = "x86_64")] msrs: Some(msrs), next_action, + code_virt_base, original_entrypoint, snapshot_generation, host_functions, @@ -802,6 +827,7 @@ mod tests { #[cfg(target_arch = "x86_64")] Vec::new(), super::NextAction::None, + 0, // code_virt_base 0, 1, HostFunctionDetails::default(), @@ -822,6 +848,7 @@ mod tests { #[cfg(target_arch = "x86_64")] Vec::new(), super::NextAction::None, + 0, // code_virt_base 0, 2, HostFunctionDetails::default(), diff --git a/src/hyperlight_host/tests/integration_test.rs b/src/hyperlight_host/tests/integration_test.rs index b449ea68d..e6bc8efc7 100644 --- a/src/hyperlight_host/tests/integration_test.rs +++ b/src/hyperlight_host/tests/integration_test.rs @@ -21,7 +21,7 @@ use std::time::Duration; use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode; use hyperlight_common::log_level::GuestLogFilter; use hyperlight_host::sandbox::SandboxConfiguration; -use hyperlight_host::{HyperlightError, MultiUseSandbox}; +use hyperlight_host::{HyperlightError, MultiUseSandbox, UninitializedSandbox}; use hyperlight_testing::simplelogger::{LOGGER, SimpleLogger}; use serial_test::serial; use tracing_core::LevelFilter; @@ -1838,6 +1838,7 @@ fn memory_region_types_are_publicly_accessible() { let base: ::HostBaseType = 0x1000; let _region = MemoryRegion_:: { guest_region: 0x1000..0x2000, + guest_virt_addr: 0x1000, host_region: base..::add(base, 0x1000), flags: MemoryRegionFlags::READ, region_type: MemoryRegionType::Code, @@ -1858,6 +1859,7 @@ fn memory_region_types_are_publicly_accessible() { }; let _region = MemoryRegion_:: { guest_region: 0x1000..0x2000, + guest_virt_addr: 0x1000, host_region: host_base ..::add(host_base, 0x1000), flags: MemoryRegionFlags::READ, @@ -1890,3 +1892,18 @@ fn hw_timer_interrupts() { ); }); } + +#[test] +#[cfg(target_arch = "x86_64")] +fn non_pie_guest_hello_world() { + let path = + hyperlight_testing::simple_guest_non_pie_as_string().expect("non-PIE guest not found"); + let sandbox = + UninitializedSandbox::new(hyperlight_host::GuestBinary::FilePath(path.into()), None) + .unwrap(); + let mut multi_use_sandbox: MultiUseSandbox = sandbox.evolve().unwrap(); + let result: i32 = multi_use_sandbox + .call("PrintOutput", "Hello from non-PIE guest!\n".to_string()) + .unwrap(); + assert_eq!(result, 26); +} diff --git a/src/hyperlight_testing/src/lib.rs b/src/hyperlight_testing/src/lib.rs index 4c7afb971..4e5548769 100644 --- a/src/hyperlight_testing/src/lib.rs +++ b/src/hyperlight_testing/src/lib.rs @@ -103,6 +103,37 @@ pub fn dummy_guest_as_string() -> Result { .ok_or_else(|| anyhow!("couldn't convert dummy guest PathBuf to string")) } +/// Get a fully qualified OS-specific path to the non-PIE simpleguest elf binary +pub fn simple_guest_non_pie_as_string() -> Result { + let buf = rust_guest_non_pie_as_pathbuf("simpleguest"); + buf.to_str() + .map(|s| s.to_string()) + .ok_or_else(|| anyhow!("couldn't convert non-PIE simple guest PathBuf to string")) +} + +/// Get a new `PathBuf` to a specified non-PIE Rust guest +/// $REPO_ROOT/src/tests/rust_guests/bin/${profile}/non_pie/ +fn rust_guest_non_pie_as_pathbuf(guest: &str) -> PathBuf { + let build_dir_selector = if cfg!(debug_assertions) { + "debug" + } else { + "release" + }; + + join_to_path( + MANIFEST_DIR, + vec![ + "..", + "tests", + "rust_guests", + "bin", + build_dir_selector, + "non_pie", + guest, + ], + ) +} + pub fn c_guest_as_pathbuf(guest: &str) -> PathBuf { let build_dir_selector = if cfg!(debug_assertions) { "debug" From 40063e0c6f8c0c5bcc00d1f148bf90f67da7d3c3 Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:22:12 -0700 Subject: [PATCH 2/4] refactor: make GuestMemoryRegion a GPA-to-GVA mapping Change GuestMemoryRegion::HostBaseType from () to usize so that GuestMemoryRegion becomes a proper mapping: host_region carries guest physical addresses (GPA) and guest_region carries guest virtual addresses (GVA). For identity-mapped regions both are the same. For non-PIE code the Code region's guest_region is overridden to the ELF-declared virtual address. Remove the guest_virt_addr field from MemoryRegion_ since its role is now served by the guest_region/host_region split in GuestMemoryRegion. Use checked_add for the code VA overlap check. 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> --- .../src/hypervisor/crashdump.rs | 1 - src/hyperlight_host/src/hypervisor/gdb/mod.rs | 1 - .../src/hypervisor/hyperlight_vm/x86_64.rs | 4 ++-- src/hyperlight_host/src/mem/layout.rs | 16 +++++++++---- src/hyperlight_host/src/mem/memory_region.rs | 24 +++++++++---------- src/hyperlight_host/src/mem/mgr.rs | 1 - src/hyperlight_host/src/mem/shared_mem.rs | 1 - .../src/sandbox/file_mapping.rs | 2 -- .../src/sandbox/initialized_multi_use.rs | 1 - .../src/sandbox/snapshot/mod.rs | 4 ++-- src/hyperlight_host/tests/integration_test.rs | 2 -- 11 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/hyperlight_host/src/hypervisor/crashdump.rs b/src/hyperlight_host/src/hypervisor/crashdump.rs index df0de1fc9..391c3673c 100644 --- a/src/hyperlight_host/src/hypervisor/crashdump.rs +++ b/src/hyperlight_host/src/hypervisor/crashdump.rs @@ -474,7 +474,6 @@ mod test { let ptr = dummy_vec.as_ptr() as usize; let regions = vec![CrashDumpRegion { guest_region: 0x1000..0x2000, - guest_virt_addr: 0x1000, host_region: ptr..ptr + dummy_vec.len(), flags: MemoryRegionFlags::READ | MemoryRegionFlags::WRITE, region_type: crate::mem::memory_region::MemoryRegionType::Code, diff --git a/src/hyperlight_host/src/hypervisor/gdb/mod.rs b/src/hyperlight_host/src/hypervisor/gdb/mod.rs index 76f80f4fa..5f82be0c3 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/mod.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/mod.rs @@ -426,7 +426,6 @@ mod tests { guest_mmap_regions: vec![MemoryRegion { host_region: mapped_mem as usize..mapped_mem.wrapping_add(size) as usize, guest_region: BASE_VIRT..BASE_VIRT + size, - guest_virt_addr: BASE_VIRT, flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE, region_type: MemoryRegionType::Heap, }], diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs index 372483e3c..cedac76ec 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs @@ -1495,7 +1495,7 @@ mod tests { let pt_buf = GuestPageTableBuffer::new(pt_base_gpa as usize); for rgn in layout - .get_memory_regions_::(()) + .get_memory_regions_::(SandboxMemoryLayout::BASE_ADDRESS) .unwrap() .iter() { @@ -1503,7 +1503,7 @@ mod tests { let writable = rgn.flags.contains(MemoryRegionFlags::WRITE); let executable = rgn.flags.contains(MemoryRegionFlags::EXECUTE); let mapping = Mapping { - phys_base: rgn.guest_region.start as u64, + phys_base: rgn.host_region.start as u64, virt_base: rgn.guest_region.start as u64, len: rgn.guest_region.len() as u64, kind: MappingKind::Basic(BasicMapping { diff --git a/src/hyperlight_host/src/mem/layout.rs b/src/hyperlight_host/src/mem/layout.rs index e802805e2..c7c1612db 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -580,10 +580,16 @@ impl SandboxMemoryLayout { let load_addr = self.get_guest_code_address() as u64; let code_virt_base = if is_pie { load_addr } else { elf_base_va }; - let mut regions = self.get_memory_regions_::(())?; + let mut regions = self.get_memory_regions_::(Self::BASE_ADDRESS)?; if !is_pie { - let code_virt_end = code_virt_base + loaded_size; + let code_virt_end = code_virt_base.checked_add(loaded_size).ok_or_else(|| { + new_error!( + "Code mapping overflow: base {:#x} + size {:#x}", + code_virt_base, + loaded_size + ) + })?; for rgn in regions.iter() { if rgn.region_type == MemoryRegionType::Code { continue; @@ -603,10 +609,12 @@ impl SandboxMemoryLayout { } } - // Set the Code region's guest_virt_addr to code_virt_base. + // Override the Code region's GVA (guest_region) to code_virt_base. + // host_region retains the GPA from the builder. for rgn in regions.iter_mut() { if rgn.region_type == MemoryRegionType::Code { - rgn.guest_virt_addr = code_virt_base as usize; + let len = rgn.guest_region.len(); + rgn.guest_region = code_virt_base as usize..(code_virt_base as usize + len); } } diff --git a/src/hyperlight_host/src/mem/memory_region.rs b/src/hyperlight_host/src/mem/memory_region.rs index a17dffd27..227de835f 100644 --- a/src/hyperlight_host/src/mem/memory_region.rs +++ b/src/hyperlight_host/src/mem/memory_region.rs @@ -282,24 +282,26 @@ impl MemoryRegionKind for HostGuestMemoryRegion { pub(crate) struct GuestMemoryRegion {} impl MemoryRegionKind for GuestMemoryRegion { - type HostBaseType = (); + type HostBaseType = usize; - fn add(_base: Self::HostBaseType, _size: usize) -> Self::HostBaseType {} + fn add(base: Self::HostBaseType, size: usize) -> Self::HostBaseType { + base + size + } } /// represents a single memory region inside the guest. All memory within a region has /// the same memory permissions #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct MemoryRegion_ { - /// the range of guest physical addresses + /// The range of guest addresses. For `GuestMemoryRegion` this is + /// the guest virtual address range (GVA). For `HostGuestMemoryRegion` + /// and `CrashDumpMemoryRegion` this is the guest physical address + /// range (GPA) or GVA depending on the variant. pub guest_region: Range, - /// the guest virtual address at which this region should be mapped. - /// For identity-mapped regions this equals `guest_region.start`. - /// For non-PIE code it is the ELF's declared virtual address. - pub guest_virt_addr: usize, - /// the range of host memory addresses - /// - /// Note that Range<()> = () x () = (). + /// The range of host-side addresses. For `HostGuestMemoryRegion` this + /// is the host virtual address range (HVA). For `GuestMemoryRegion` + /// this is the guest physical address range (GPA). For + /// `CrashDumpMemoryRegion` this is the HVA. pub host_region: Range, /// memory access flags for the given region pub flags: MemoryRegionFlags, @@ -360,7 +362,6 @@ impl MemoryRegionVecBuilder { let host_end = ::add(self.host_base_virt_addr, size); self.regions.push(MemoryRegion_ { guest_region: self.guest_base_phys_addr..guest_end, - guest_virt_addr: self.guest_base_phys_addr, host_region: self.host_base_virt_addr..host_end, flags, region_type, @@ -375,7 +376,6 @@ impl MemoryRegionVecBuilder { let guest_start = last_region.guest_region.end; let new_region = MemoryRegion_ { guest_region: guest_start..guest_start + size, - guest_virt_addr: guest_start, host_region: last_region.host_region.end..host_end, flags, region_type, diff --git a/src/hyperlight_host/src/mem/mgr.rs b/src/hyperlight_host/src/mem/mgr.rs index d0d96964b..90395015d 100644 --- a/src/hyperlight_host/src/mem/mgr.rs +++ b/src/hyperlight_host/src/mem/mgr.rs @@ -650,7 +650,6 @@ impl SandboxMemoryManager { regions.push(CrashDumpRegion { guest_region: virt_base..virt_end, - guest_virt_addr: virt_base, host_region: host_base..host_base + host_len, flags, region_type, diff --git a/src/hyperlight_host/src/mem/shared_mem.rs b/src/hyperlight_host/src/mem/shared_mem.rs index e47c4380a..4b706cc41 100644 --- a/src/hyperlight_host/src/mem/shared_mem.rs +++ b/src/hyperlight_host/src/mem/shared_mem.rs @@ -514,7 +514,6 @@ fn mapping_at( MemoryRegion { guest_region: guest_base..(guest_base + size), - guest_virt_addr: guest_base, host_region: s.host_region_base() ..::add(s.host_region_base(), size), region_type, diff --git a/src/hyperlight_host/src/sandbox/file_mapping.rs b/src/hyperlight_host/src/sandbox/file_mapping.rs index d0e367074..4f3ed2a4d 100644 --- a/src/hyperlight_host/src/sandbox/file_mapping.rs +++ b/src/hyperlight_host/src/sandbox/file_mapping.rs @@ -164,7 +164,6 @@ impl PreparedFileMapping { Ok(MemoryRegion { host_region: host_base..host_end, guest_region: guest_start..guest_end, - guest_virt_addr: guest_start, flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE, region_type: MemoryRegionType::MappedFile, }) @@ -185,7 +184,6 @@ impl PreparedFileMapping { host_region: *mmap_base as usize ..(*mmap_base as usize).wrapping_add(*mmap_size), guest_region: guest_start..guest_end, - guest_virt_addr: guest_start, flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE, region_type: MemoryRegionType::MappedFile, }) diff --git a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs index be268818e..623c24667 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -1587,7 +1587,6 @@ mod tests { MemoryRegion { host_region: mem.host_region_base()..mem.host_region_end(), guest_region: guest_base..(guest_base + len), - guest_virt_addr: guest_base, flags, region_type: MemoryRegionType::Heap, } diff --git a/src/hyperlight_host/src/sandbox/snapshot/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/mod.rs index 8b0018844..67dd9a771 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/mod.rs @@ -385,8 +385,8 @@ impl Snapshot { }; let mapping = Mapping { - phys_base: rgn.guest_region.start as u64, - virt_base: rgn.guest_virt_addr as u64, + phys_base: rgn.host_region.start as u64, + virt_base: rgn.guest_region.start as u64, len: rgn.guest_region.len() as u64, kind, }; diff --git a/src/hyperlight_host/tests/integration_test.rs b/src/hyperlight_host/tests/integration_test.rs index e6bc8efc7..24e63dce2 100644 --- a/src/hyperlight_host/tests/integration_test.rs +++ b/src/hyperlight_host/tests/integration_test.rs @@ -1838,7 +1838,6 @@ fn memory_region_types_are_publicly_accessible() { let base: ::HostBaseType = 0x1000; let _region = MemoryRegion_:: { guest_region: 0x1000..0x2000, - guest_virt_addr: 0x1000, host_region: base..::add(base, 0x1000), flags: MemoryRegionFlags::READ, region_type: MemoryRegionType::Code, @@ -1859,7 +1858,6 @@ fn memory_region_types_are_publicly_accessible() { }; let _region = MemoryRegion_:: { guest_region: 0x1000..0x2000, - guest_virt_addr: 0x1000, host_region: host_base ..::add(host_base, 0x1000), flags: MemoryRegionFlags::READ, From 93e5b1d4234d39ecd2838ef33cf907cc9cc56a54 Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:25:17 -0700 Subject: [PATCH 3/4] refactor: concretise get_memory_regions to GuestMemoryRegion Rename get_memory_regions_ to get_memory_regions and remove the generic type parameter. All callers use GuestMemoryRegion, so the generic is unnecessary. The host_base argument is now always BASE_ADDRESS, supplied internally. 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> --- .github/workflows/dep_build_guests.yml | 4 +--- Justfile | 4 ++-- .../src/hypervisor/hyperlight_vm/x86_64.rs | 8 ++------ src/hyperlight_host/src/mem/layout.rs | 11 ++++------- src/hyperlight_host/tests/integration_test.rs | 1 - 5 files changed, 9 insertions(+), 19 deletions(-) diff --git a/.github/workflows/dep_build_guests.yml b/.github/workflows/dep_build_guests.yml index 1c110028d..3e2fafff1 100644 --- a/.github/workflows/dep_build_guests.yml +++ b/.github/workflows/dep_build_guests.yml @@ -54,7 +54,7 @@ jobs: run: | sudo chown -R $(id -u):$(id -g) /opt/cargo || true - # cargo-hyperlight builds a custom sysroot for x86_64-hyperlight-none target. + # cargo-hyperlight builds a custom sysroot for the Hyperlight guest target. # rust-cache cleans "anything not a dependency" from target dirs, removing the sysroot. # We cache sysroot separately to avoid rebuilding it (~10s) on every run. - name: Sysroot cache @@ -89,7 +89,6 @@ jobs: just move-rust-guests ${{ inputs.config }} - name: Build non-PIE Rust guests - if: inputs.arch == 'X64' run: | just build-rust-guests-non-pie ${{ inputs.config }} just move-rust-guests-non-pie ${{ inputs.config }} @@ -115,4 +114,3 @@ jobs: path: src/tests/c_guests/bin/${{ inputs.config }}/ retention-days: 1 if-no-files-found: error - diff --git a/Justfile b/Justfile index f2f5cca2d..7beb31626 100644 --- a/Justfile +++ b/Justfile @@ -81,9 +81,9 @@ build-and-move-c-guests: (build-c-guests "debug") (move-c-guests "debug") (build # 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 } }} + {{ 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 {{ hyperlight-target }} --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" +non_pie_guests_target := "src/tests/rust_guests/target-non-pie/build/" + hyperlight-target @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" } }} diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs index cedac76ec..86a576c18 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs @@ -930,7 +930,7 @@ mod tests { use crate::hypervisor::regs::{CommonSegmentRegister, CommonTableRegister, MXCSR_DEFAULT}; use crate::hypervisor::virtual_machine::VirtualMachine; use crate::mem::layout::SandboxMemoryLayout; - use crate::mem::memory_region::{GuestMemoryRegion, MemoryRegionFlags}; + use crate::mem::memory_region::MemoryRegionFlags; use crate::mem::mgr::{GuestPageTableBuffer, SandboxMemoryManager}; use crate::mem::ptr::RawPtr; use crate::mem::shared_mem::{ExclusiveSharedMemory, ReadonlySharedMemory}; @@ -1494,11 +1494,7 @@ mod tests { let pt_base_gpa = layout.get_pt_base_gpa(); let pt_buf = GuestPageTableBuffer::new(pt_base_gpa as usize); - for rgn in layout - .get_memory_regions_::(SandboxMemoryLayout::BASE_ADDRESS) - .unwrap() - .iter() - { + for rgn in layout.get_memory_regions().unwrap().iter() { let readable = rgn.flags.contains(MemoryRegionFlags::READ); let writable = rgn.flags.contains(MemoryRegionFlags::WRITE); let executable = rgn.flags.contains(MemoryRegionFlags::EXECUTE); diff --git a/src/hyperlight_host/src/mem/layout.rs b/src/hyperlight_host/src/mem/layout.rs index c7c1612db..ea8b120fd 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -69,7 +69,7 @@ use tracing::{Span, instrument}; use super::memory_region::MemoryRegionType::{self, Code, Heap, InitData, Peb}; use super::memory_region::{ DEFAULT_GUEST_BLOB_MEM_FLAGS, GuestMemoryRegion, MemoryRegion, MemoryRegion_, - MemoryRegionFlags, MemoryRegionKind, MemoryRegionVecBuilder, + MemoryRegionFlags, MemoryRegionVecBuilder, }; #[cfg(readable_shared_mem)] use super::shared_mem::HostSharedMemory; @@ -469,11 +469,8 @@ impl SandboxMemoryLayout { /// Returns the memory regions associated with this memory layout, /// suitable for passing to a hypervisor for mapping into memory - pub(crate) fn get_memory_regions_( - &self, - host_base: K::HostBaseType, - ) -> Result>> { - let mut builder = MemoryRegionVecBuilder::new(Self::BASE_ADDRESS, host_base); + pub(crate) fn get_memory_regions(&self) -> Result>> { + let mut builder = MemoryRegionVecBuilder::new(Self::BASE_ADDRESS, Self::BASE_ADDRESS); // code let peb_offset = builder.push_page_aligned( @@ -580,7 +577,7 @@ impl SandboxMemoryLayout { let load_addr = self.get_guest_code_address() as u64; let code_virt_base = if is_pie { load_addr } else { elf_base_va }; - let mut regions = self.get_memory_regions_::(Self::BASE_ADDRESS)?; + let mut regions = self.get_memory_regions()?; if !is_pie { let code_virt_end = code_virt_base.checked_add(loaded_size).ok_or_else(|| { diff --git a/src/hyperlight_host/tests/integration_test.rs b/src/hyperlight_host/tests/integration_test.rs index 24e63dce2..f89268813 100644 --- a/src/hyperlight_host/tests/integration_test.rs +++ b/src/hyperlight_host/tests/integration_test.rs @@ -1892,7 +1892,6 @@ fn hw_timer_interrupts() { } #[test] -#[cfg(target_arch = "x86_64")] fn non_pie_guest_hello_world() { let path = hyperlight_testing::simple_guest_non_pie_as_string().expect("non-PIE guest not found"); From d73e7e51954ef49dc72834319125b21ec12732e4 Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:15:42 -0700 Subject: [PATCH 4/4] 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> --- Justfile | 1 + .../src/hypervisor/hyperlight_vm/x86_64.rs | 5 +- src/hyperlight_host/src/mem/layout.rs | 94 ++++++++++++------- .../src/sandbox/initialized_multi_use.rs | 17 ++-- .../src/sandbox/snapshot/file/config.rs | 7 -- .../src/sandbox/snapshot/mod.rs | 19 +++- 6 files changed, 90 insertions(+), 53 deletions(-) diff --git a/Justfile b/Justfile index 7beb31626..0ca1b3449 100644 --- a/Justfile +++ b/Justfile @@ -76,6 +76,7 @@ build-and-move-rust-guests: (build-rust-guests "debug") (move-rust-guests "debug 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. diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs index 86a576c18..9b25bb95f 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs @@ -720,10 +720,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]; diff --git a/src/hyperlight_host/src/mem/layout.rs b/src/hyperlight_host/src/mem/layout.rs index ea8b120fd..5c52373d3 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -557,15 +557,12 @@ impl SandboxMemoryLayout { /// guest memory regions with the Code region's `guest_virt_addr` /// already set to the computed virtual base. /// - /// 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. + /// 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`). /// - /// For non-PIE binaries, the code appears at the ELF's declared - /// virtual address (`elf_base_va`), which may differ from the physical - /// load address. This method checks that the resulting virtual range - /// `[elf_base_va, elf_base_va + loaded_size)` does not overlap any - /// non-Code region. + /// In both cases the resulting virtual range is validated against all + /// non-Code memory regions to prevent overlap. /// /// Returns `(code_virt_base, regions)`. pub(crate) fn get_guest_regions_with_code_va( @@ -574,35 +571,56 @@ impl SandboxMemoryLayout { elf_base_va: u64, loaded_size: u64, ) -> Result<(u64, Vec>)> { - let load_addr = self.get_guest_code_address() as u64; - let code_virt_base = if is_pie { load_addr } else { elf_base_va }; + let code_size_pages = loaded_size.div_ceil(PAGE_SIZE_USIZE as u64); + let code_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"))? + }; let mut regions = self.get_memory_regions()?; - if !is_pie { - let code_virt_end = code_virt_base.checked_add(loaded_size).ok_or_else(|| { - new_error!( - "Code mapping overflow: base {:#x} + size {:#x}", + // 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 = code_virt_base.checked_add(loaded_size).ok_or_else(|| { + new_error!( + "Code mapping overflow: base {:#x} + size {:#x}", + code_virt_base, + loaded_size + ) + })?; + for rgn in regions.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 code_virt_base < rgn_end && rgn_start < code_virt_end { + return Err(new_error!( + "Code mapping [{:#x}, {:#x}) conflicts with {:?} region [{:#x}, {:#x})", code_virt_base, - loaded_size - ) - })?; - for rgn in regions.iter() { - if rgn.region_type == MemoryRegionType::Code { - continue; - } - let rgn_start = rgn.guest_region.start as u64; - let rgn_end = rgn_start + rgn.guest_region.len() as u64; - if code_virt_base < rgn_end && rgn_start < code_virt_end { - return Err(new_error!( - "Non-PIE code mapping [{:#x}, {:#x}) conflicts with {:?} region [{:#x}, {:#x})", - code_virt_base, - code_virt_end, - rgn.region_type, - rgn_start, - rgn_end, - )); - } + code_virt_end, + rgn.region_type, + rgn_start, + rgn_end, + )); } } @@ -615,6 +633,13 @@ impl SandboxMemoryLayout { } } + tracing::debug!( + code_virt_base = format_args!("{:#x}", code_virt_base), + elf_base_va = format_args!("{:#x}", elf_base_va), + is_pie, + "code region virtual base address" + ); + Ok((code_virt_base, regions)) } @@ -741,6 +766,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() } diff --git a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs index 623c24667..75d054023 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -2010,9 +2010,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 = sbox - .call("ReadMappedBuffer", (gva, len as u64, true)) + .call("ReadMappedBuffer", (gva, len as u64, false)) .unwrap(); assert_eq!(expected.len(), len); @@ -2036,7 +2039,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); } @@ -2046,7 +2049,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); } @@ -2056,7 +2059,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; @@ -2072,7 +2075,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); } @@ -2083,7 +2086,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); diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs index 5c2da1bac..a9f926e85 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs @@ -508,13 +508,6 @@ impl OciSnapshotConfig { code_hi )); } - #[cfg(target_arch = "aarch64")] - if !self.entrypoint_addr.is_multiple_of(4) { - return Err(crate::new_error!( - "snapshot entrypoint addr {:#x} is not 4-byte aligned", - self.entrypoint_addr - )); - } // ELF entry point GVA for `AT_ENTRY` in core dumps. 0 means // unknown. Any other value must point inside the code region, diff --git a/src/hyperlight_host/src/sandbox/snapshot/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/mod.rs index 67dd9a771..4aa0780ee 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/mod.rs @@ -339,7 +339,6 @@ impl Snapshot { guest_blob_mem_flags, )?; - let load_addr = layout.get_guest_code_address() as u64; let base_va = exe_info.base_va(); let entrypoint_va: u64 = exe_info.entrypoint().into(); let loaded_size = exe_info.loaded_size() as u64; @@ -354,7 +353,7 @@ impl Snapshot { let mut memory = vec![0; layout.get_memory_size()?]; let load_info = exe_info.load( - load_addr.try_into()?, + code_virt_base.try_into()?, &mut memory[layout.guest_code_offset()..], )?; @@ -412,7 +411,15 @@ impl Snapshot { ) })?; - let entrypoint_gva = code_virt_base + entrypoint_offset; + let entrypoint_gva = code_virt_base + .checked_add(entrypoint_offset) + .ok_or_else(|| { + crate::new_error!( + "Entrypoint overflow: code_virt_base {:#x} + offset {:#x}", + code_virt_base, + entrypoint_offset + ) + })?; Ok(Self { memory: ReadonlySharedMemory::from_bytes(&memory, layout.snapshot_size())?, @@ -670,6 +677,12 @@ impl Snapshot { self.original_entrypoint } + /// Returns the virtual base address of the code region in guest space. + #[allow(dead_code)] + pub(crate) fn code_virt_base(&self) -> u64 { + self.code_virt_base + } + /// Validate that `provided` is a superset of the host functions /// recorded in this snapshot: every function that was registered /// at snapshot time must also be present in `provided` with a