From dfa401d731783d4875cf2988eecb78804d548a8a 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/7] 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/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 | 28 +++++--- .../src/sandbox/snapshot/file/mod.rs | 2 + .../src/sandbox/snapshot/file_tests.rs | 26 ++++++-- .../src/sandbox/snapshot/mod.rs | 37 +++++++++-- src/hyperlight_host/tests/integration_test.rs | 20 +++++- src/hyperlight_testing/src/lib.rs | 31 +++++++++ 18 files changed, 251 insertions(+), 25 deletions(-) diff --git a/.github/workflows/dep_build_guests.yml b/.github/workflows/dep_build_guests.yml index 808ba179ae..59c2406458 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 3aae7b792e..27126b8ded 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 1583900f04..47b57e10ca 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 6c9aec132d..6dcb711414 100644 --- a/src/hyperlight_host/src/hypervisor/crashdump.rs +++ b/src/hyperlight_host/src/hypervisor/crashdump.rs @@ -461,6 +461,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/mem/elf.rs b/src/hyperlight_host/src/mem/elf.rs index f6cde29068..d18b847bfd 100644 --- a/src/hyperlight_host/src/mem/elf.rs +++ b/src/hyperlight_host/src/mem/elf.rs @@ -4,6 +4,7 @@ #[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")] @@ -29,6 +30,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, @@ -130,6 +133,7 @@ impl ElfInfo { .collect(), entry: elf.entry, relocs, + is_pie: elf.header.e_type == ET_DYN, guest_bin_version, }) } @@ -155,6 +159,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 7bf3a446d4..3e8ec5173b 100644 --- a/src/hyperlight_host/src/mem/exe.rs +++ b/src/hyperlight_host/src/mem/exe.rs @@ -76,6 +76,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 9e7e10b242..948e6815aa 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -54,10 +54,10 @@ use hyperlight_common::mem::HyperlightPEB; use hyperlight_common::vmem::PAGE_SIZE; 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; @@ -554,6 +554,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 26a5331437..f62109113b 100644 --- a/src/hyperlight_host/src/mem/memory_region.rs +++ b/src/hyperlight_host/src/mem/memory_region.rs @@ -276,8 +276,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 () = (). @@ -341,6 +345,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, @@ -352,8 +357,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 e2c4da395b..80004d5e4c 100644 --- a/src/hyperlight_host/src/mem/mgr.rs +++ b/src/hyperlight_host/src/mem/mgr.rs @@ -133,6 +133,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 @@ -274,6 +278,7 @@ where scratch_mem, next_action, original_entrypoint: 0, + code_virt_base: 0, abort_buffer: Vec::new(), snapshot_count: 0, } @@ -309,6 +314,7 @@ where #[cfg(target_arch = "x86_64")] msrs, next_action, + self.code_virt_base, self.original_entrypoint, self.snapshot_count, host_functions, @@ -324,6 +330,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 @@ -356,6 +363,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, }; @@ -365,6 +373,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, }; @@ -513,6 +522,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)) @@ -646,6 +656,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 dcb03fc083..f2f134087e 100644 --- a/src/hyperlight_host/src/mem/shared_mem.rs +++ b/src/hyperlight_host/src/mem/shared_mem.rs @@ -628,6 +628,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 94a70d9992..de4881268c 100644 --- a/src/hyperlight_host/src/sandbox/file_mapping.rs +++ b/src/hyperlight_host/src/sandbox/file_mapping.rs @@ -151,6 +151,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, }) @@ -171,6 +172,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 8613546dc1..82f46ad4e5 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -1586,6 +1586,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 e9b49ef823..38322ef452 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs @@ -180,6 +180,12 @@ pub(super) struct OciSnapshotConfig { /// Initialise->Call transition. Fills `AT_ENTRY` in core dumps so /// gdb resolves PIE symbols. 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, @@ -477,13 +483,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 ) })?; @@ -515,10 +527,10 @@ impl OciSnapshotConfig { })?; if self.original_entrypoint_addr < code_lo || self.original_entrypoint_addr >= snapshot_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 +775,7 @@ mod tests { stack_top_gva: 0x2000, entrypoint_addr: SandboxMemoryLayout::BASE_ADDRESS as u64, original_entrypoint_addr: SandboxMemoryLayout::BASE_ADDRESS as u64, + code_virt_base: 0, sregs: distinct_sregs(), #[cfg(target_arch = "x86_64")] msrs: Vec::new(), @@ -846,9 +859,7 @@ mod schema_pin { "stack_top_gva": 3735928559, "entrypoint_addr": 8192, "original_entrypoint_addr": 4096, - "sregs": { - "cs": { - "base": 1, + "code_virt_base": 0, "limit": 2, "selector": 3, "type_": 4, @@ -1032,6 +1043,7 @@ mod schema_pin { "stack_top_gva": 3735928559, "entrypoint_addr": 8192, "original_entrypoint_addr": 4096, + "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 678d1d1626..d2a7a86f2f 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs @@ -594,6 +594,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 @@ -891,6 +892,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 7e200a50e4..98b2d827f8 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs @@ -12,8 +12,6 @@ use serde_json::Value; use sha2::{Digest as _, Sha256}; use crate::func::Registerable; -use crate::mem::layout::SandboxMemoryLayout; -use crate::mem::shared_mem::SharedMemory as _; use crate::sandbox::snapshot::{OciDigest, OciReference, OciTag, Snapshot}; use crate::{GuestBinary, HostFunctions, MultiUseSandbox, SandboxBuilder}; @@ -2047,11 +2045,11 @@ fn original_entrypoint_addr_zero_rejected() { 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, @@ -2959,6 +2957,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 a4def5b7af..5423ba3668 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/mod.rs @@ -23,7 +23,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; @@ -88,8 +88,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 @@ -324,6 +329,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()?]; @@ -341,7 +354,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); @@ -357,9 +370,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, }; @@ -377,7 +391,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())?, @@ -388,6 +410,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 { @@ -416,6 +439,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, @@ -577,6 +601,7 @@ impl Snapshot { #[cfg(target_arch = "x86_64")] msrs: Some(msrs), next_action, + code_virt_base, original_entrypoint, snapshot_generation, host_functions, @@ -796,6 +821,7 @@ mod tests { #[cfg(target_arch = "x86_64")] Vec::new(), super::NextAction::None, + 0, // code_virt_base 0, 1, HostFunctionDetails::default(), @@ -816,6 +842,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 165d753fd4..8fdadc2f57 100644 --- a/src/hyperlight_host/tests/integration_test.rs +++ b/src/hyperlight_host/tests/integration_test.rs @@ -7,7 +7,8 @@ use std::time::Duration; use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode; use hyperlight_common::log_level::GuestLogFilter; -use hyperlight_host::{HyperlightError, MultiUseSandbox, SandboxBuilder}; +use hyperlight_host::sandbox::SandboxConfiguration; +use hyperlight_host::{HyperlightError, MultiUseSandbox, SandboxBuilder, UninitializedSandbox}; use hyperlight_testing::simplelogger::{LOGGER, SimpleLogger}; use serial_test::serial; use tracing_core::LevelFilter; @@ -1798,6 +1799,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, @@ -1818,6 +1820,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, @@ -1850,3 +1853,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 46b0b8c48c..a9a058bf90 100644 --- a/src/hyperlight_testing/src/lib.rs +++ b/src/hyperlight_testing/src/lib.rs @@ -90,6 +90,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 93b845b547988b7a56f89f8606d61c6387808ebf 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/7] 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/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 -- 10 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/hyperlight_host/src/hypervisor/crashdump.rs b/src/hyperlight_host/src/hypervisor/crashdump.rs index 6dcb711414..6c9aec132d 100644 --- a/src/hyperlight_host/src/hypervisor/crashdump.rs +++ b/src/hyperlight_host/src/hypervisor/crashdump.rs @@ -461,7 +461,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/hyperlight_vm/x86_64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs index 6fabdbfc57..47b640ae1c 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs @@ -1426,7 +1426,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() { @@ -1434,7 +1434,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 948e6815aa..6a177b9b07 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -579,10 +579,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; @@ -602,10 +608,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 f62109113b..bb726f7d21 100644 --- a/src/hyperlight_host/src/mem/memory_region.rs +++ b/src/hyperlight_host/src/mem/memory_region.rs @@ -267,24 +267,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, @@ -345,7 +347,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, @@ -360,7 +361,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 80004d5e4c..182dae970f 100644 --- a/src/hyperlight_host/src/mem/mgr.rs +++ b/src/hyperlight_host/src/mem/mgr.rs @@ -656,7 +656,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 f2f134087e..dcb03fc083 100644 --- a/src/hyperlight_host/src/mem/shared_mem.rs +++ b/src/hyperlight_host/src/mem/shared_mem.rs @@ -628,7 +628,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 de4881268c..94a70d9992 100644 --- a/src/hyperlight_host/src/sandbox/file_mapping.rs +++ b/src/hyperlight_host/src/sandbox/file_mapping.rs @@ -151,7 +151,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, }) @@ -172,7 +171,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 82f46ad4e5..8613546dc1 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -1586,7 +1586,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 5423ba3668..fe523c1a5a 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/mod.rs @@ -372,8 +372,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 8fdadc2f57..37adff2534 100644 --- a/src/hyperlight_host/tests/integration_test.rs +++ b/src/hyperlight_host/tests/integration_test.rs @@ -1799,7 +1799,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, @@ -1820,7 +1819,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 9f00375101caa1f4c933df10a016a9e304bdc5b2 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/7] 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/sandbox/snapshot/file_tests.rs | 1 + src/hyperlight_host/tests/integration_test.rs | 1 - 6 files changed, 10 insertions(+), 19 deletions(-) diff --git a/.github/workflows/dep_build_guests.yml b/.github/workflows/dep_build_guests.yml index 59c2406458..c1409ffc25 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 47b57e10ca..c426140fe5 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 47b640ae1c..bbd9641251 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs @@ -863,7 +863,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}; @@ -1425,11 +1425,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 6a177b9b07..38c7e4f44c 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -57,7 +57,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; @@ -457,11 +457,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( @@ -579,7 +576,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/src/sandbox/snapshot/file_tests.rs b/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs index 98b2d827f8..359b31a4ce 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs @@ -12,6 +12,7 @@ use serde_json::Value; use sha2::{Digest as _, Sha256}; use crate::func::Registerable; +use crate::mem::shared_mem::SharedMemory; use crate::sandbox::snapshot::{OciDigest, OciReference, OciTag, Snapshot}; use crate::{GuestBinary, HostFunctions, MultiUseSandbox, SandboxBuilder}; diff --git a/src/hyperlight_host/tests/integration_test.rs b/src/hyperlight_host/tests/integration_test.rs index 37adff2534..ca4174fadc 100644 --- a/src/hyperlight_host/tests/integration_test.rs +++ b/src/hyperlight_host/tests/integration_test.rs @@ -1853,7 +1853,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 2f55a9ae7c03e8255757005ac1e47cb71560b6de Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Wed, 19 Aug 2026 21:49:48 +0000 Subject: [PATCH 4/7] Centralize guest code addresses in memory layout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: cshung <3410332+cshung@users.noreply.github.com> --- src/hyperlight_host/src/hypervisor/gdb/mod.rs | 2 +- .../src/hypervisor/hyperlight_vm/x86_64.rs | 4 +- src/hyperlight_host/src/mem/layout.rs | 146 ++++++++++-------- src/hyperlight_host/src/mem/memory_region.rs | 22 +-- src/hyperlight_host/src/mem/mgr.rs | 11 -- .../src/sandbox/initialized_multi_use.rs | 10 +- .../src/sandbox/snapshot/file/mod.rs | 9 +- .../src/sandbox/snapshot/file_tests.rs | 12 +- .../src/sandbox/snapshot/mod.rs | 25 +-- .../src/sandbox/trace/mem_profile.rs | 2 +- 10 files changed, 122 insertions(+), 121 deletions(-) diff --git a/src/hyperlight_host/src/hypervisor/gdb/mod.rs b/src/hyperlight_host/src/hypervisor/gdb/mod.rs index 65b770e33d..79ca6e083a 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/mod.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/mod.rs @@ -94,7 +94,7 @@ impl<'a> DebugMemoryView<'a> { } pub(crate) fn code_section_offset(&self) -> u64 { - self.mem_mgr.layout.get_guest_code_address() as u64 + self.mem_mgr.layout.get_guest_code_gva() as u64 } /// Reads memory from the guest's address space with a maximum length of a PAGE_SIZE 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 bbd9641251..7845b1ece6 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs @@ -1476,7 +1476,7 @@ mod tests { layout, ro_mem.to_mgr_snapshot_mem().unwrap(), scratch_mem, - NextAction::Initialise(layout.get_guest_code_address() as u64), + NextAction::Initialise(layout.get_guest_code_gva() as u64), ); let (mut hshm, gshm) = mem_mgr.build().unwrap(); @@ -2181,7 +2181,7 @@ mod tests { a.fxsave(ptr(rax)).unwrap(); // Return dispatch ptr - a.mov(rax, layout.get_guest_code_address() as u64).unwrap(); + a.mov(rax, layout.get_guest_code_gva() as u64).unwrap(); a.hlt().unwrap(); diff --git a/src/hyperlight_host/src/mem/layout.rs b/src/hyperlight_host/src/mem/layout.rs index 38c7e4f44c..00d697b838 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -54,7 +54,7 @@ use hyperlight_common::mem::HyperlightPEB; use hyperlight_common::vmem::PAGE_SIZE; use tracing::{Span, instrument}; -use super::memory_region::MemoryRegionType::{self, Code, Heap, InitData, Peb}; +use super::memory_region::MemoryRegionType::{Code, Heap, InitData, Peb}; use super::memory_region::{ DEFAULT_GUEST_BLOB_MEM_FLAGS, GuestMemoryRegion, MemoryRegion, MemoryRegion_, MemoryRegionFlags, MemoryRegionVecBuilder, @@ -244,6 +244,8 @@ pub(crate) struct SandboxMemoryLayout { heap_size: usize, /// The size of the guest code section. code_size: usize, + /// Guest virtual address of the code section. + code_gva: usize, /// The size of the init data section (guest blob). init_data_size: usize, /// Permission flags for the init data region. @@ -270,6 +272,7 @@ impl Debug for SandboxMemoryLayout { &format_args!("{:#x}", self.get_memory_size().unwrap_or(0)), ) .field("Code Size", &format_args!("{:#x}", self.code_size)) + .field("Code GVA", &format_args!("{:#x}", self.code_gva)) .field("Heap Size", &format_args!("{:#x}", self.heap_size)) .field( "Init Data Size", @@ -324,6 +327,7 @@ impl SandboxMemoryLayout { output_data_size, heap_size, code_size, + code_gva, init_data_size, init_data_permissions, scratch_size, @@ -334,6 +338,7 @@ impl SandboxMemoryLayout { && *output_data_size == other.output_data_size && *heap_size == other.heap_size && *code_size == other.code_size + && *code_gva == other.code_gva && *init_data_size == other.init_data_size && *init_data_permissions == other.init_data_permissions && *scratch_size == other.scratch_size @@ -379,6 +384,7 @@ impl SandboxMemoryLayout { output_data_size, heap_size, code_size, + code_gva: Self::BASE_ADDRESS, init_data_size, init_data_permissions, pt_size: None, @@ -548,73 +554,54 @@ impl SandboxMemoryLayout { )); } - Ok(builder.build()) + let mut regions = builder.build(); + for region in &mut regions { + if region.region_type == Code { + let end = self + .code_gva + .checked_add(region.guest_region.len()) + .ok_or_else(|| { + new_error!( + "code mapping overflow: base {:#x} + size {:#x}", + self.code_gva, + region.guest_region.len() + ) + })?; + region.guest_region = self.code_gva..end; + } + } + Ok(regions) } - /// 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.checked_add(loaded_size).ok_or_else(|| { + /// Set the code GVA after checking that it does not overlap another region. + pub(crate) fn set_code_gva(&mut self, code_gva: u64) -> Result<()> { + let code_gva = usize::try_from(code_gva)?; + let code_end = code_gva + .checked_add(self.code_size.next_multiple_of(PAGE_SIZE)) + .ok_or_else(|| { new_error!( - "Code mapping overflow: base {:#x} + size {:#x}", - code_virt_base, - loaded_size + "code mapping overflow: base {:#x} + size {:#x}", + code_gva, + self.code_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, - )); - } + for region in self.get_memory_regions()? { + if region.region_type == Code { + continue; } - } - - // 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 { - let len = rgn.guest_region.len(); - rgn.guest_region = code_virt_base as usize..(code_virt_base as usize + len); + if code_gva < region.guest_region.end && region.guest_region.start < code_end { + return Err(new_error!( + "code mapping [{:#x}, {:#x}) conflicts with {:?} region [{:#x}, {:#x})", + code_gva, + code_end, + region.region_type, + region.guest_region.start, + region.guest_region.end, + )); } } - - Ok((code_virt_base, regions)) + self.code_gva = code_gva; + Ok(()) } #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] @@ -739,11 +726,16 @@ impl SandboxMemoryLayout { 0 } - /// Guest address of the code section in the sandbox. - pub(crate) fn get_guest_code_address(&self) -> usize { + /// Guest physical address of the code section. + pub(crate) fn get_guest_code_gpa(&self) -> usize { Self::BASE_ADDRESS + self.guest_code_offset() } + /// Guest virtual address of the code section. + pub(crate) fn get_guest_code_gva(&self) -> usize { + self.code_gva + } + /// Guest virtual address of the start of output data. pub(crate) fn get_output_data_buffer_gva(&self) -> u64 { hyperlight_common::layout::scratch_base_gva(self.scratch_size) + self.input_data_size as u64 @@ -837,6 +829,35 @@ mod tests { ); } + #[test] + fn code_gva_updates_code_region() { + let mut layout = + SandboxMemoryLayout::new(SandboxConfiguration::default(), PAGE_SIZE, 0, None).unwrap(); + let code_gva = 0x100_0000; + layout.set_code_gva(code_gva).unwrap(); + + assert_eq!( + layout.get_guest_code_gpa(), + SandboxMemoryLayout::BASE_ADDRESS + ); + assert_eq!(layout.get_guest_code_gva(), code_gva as usize); + let code = layout + .get_memory_regions() + .unwrap() + .into_iter() + .find(|region| region.region_type == Code) + .unwrap(); + assert_eq!(code.host_region.start, SandboxMemoryLayout::BASE_ADDRESS); + assert_eq!(code.guest_region.start, code_gva as usize); + } + + #[test] + fn code_gva_rejects_overlap() { + let mut layout = + SandboxMemoryLayout::new(SandboxConfiguration::default(), PAGE_SIZE, 0, None).unwrap(); + assert!(layout.set_code_gva(layout.peb_address() as u64).is_err()); + } + #[test] fn test_max_memory_sandbox() { let mut cfg = SandboxConfiguration::default(); @@ -881,6 +902,7 @@ mod tests { |l| l.output_data_size += PAGE_SIZE, |l| l.heap_size += PAGE_SIZE, |l| l.code_size += PAGE_SIZE, + |l| l.code_gva += PAGE_SIZE, |l| l.init_data_size += PAGE_SIZE, |l| l.scratch_size += PAGE_SIZE, |l| { diff --git a/src/hyperlight_host/src/mem/memory_region.rs b/src/hyperlight_host/src/mem/memory_region.rs index bb726f7d21..8f6e978c56 100644 --- a/src/hyperlight_host/src/mem/memory_region.rs +++ b/src/hyperlight_host/src/mem/memory_region.rs @@ -261,8 +261,9 @@ impl MemoryRegionKind for HostGuestMemoryRegion { } } -/// Type for memory regions that only track guest addresses. +/// Marker for GPA-to-GVA mappings used to construct guest page tables. /// +/// `host_region` contains GPAs and `guest_region` contains GVAs. #[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] pub(crate) struct GuestMemoryRegion {} @@ -278,15 +279,15 @@ impl MemoryRegionKind for GuestMemoryRegion { /// the same memory permissions #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct MemoryRegion_ { - /// 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. + /// The destination range of the mapping. + /// + /// This is a GVA for `GuestMemoryRegion`, a GPA for + /// `HostGuestMemoryRegion`, and a GVA for `CrashDumpMemoryRegion`. pub guest_region: Range, - /// 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. + /// The source range of the mapping. + /// + /// This is a GPA for `GuestMemoryRegion` and an HVA for + /// `HostGuestMemoryRegion` and `CrashDumpMemoryRegion`. pub host_region: Range, /// memory access flags for the given region pub flags: MemoryRegionFlags, @@ -358,9 +359,8 @@ 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: guest_start..guest_start + size, + guest_region: last_region.guest_region.end..last_region.guest_region.end + size, 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 182dae970f..4d56c92033 100644 --- a/src/hyperlight_host/src/mem/mgr.rs +++ b/src/hyperlight_host/src/mem/mgr.rs @@ -133,10 +133,6 @@ 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 @@ -278,7 +274,6 @@ where scratch_mem, next_action, original_entrypoint: 0, - code_virt_base: 0, abort_buffer: Vec::new(), snapshot_count: 0, } @@ -314,7 +309,6 @@ where #[cfg(target_arch = "x86_64")] msrs, next_action, - self.code_virt_base, self.original_entrypoint, self.snapshot_count, host_functions, @@ -330,7 +324,6 @@ 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 @@ -363,7 +356,6 @@ 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, }; @@ -373,7 +365,6 @@ 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, }; @@ -522,8 +513,6 @@ 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)) } diff --git a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs index 8613546dc1..dc55986d0e 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -2324,7 +2324,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.layout.get_guest_code_gva() as u64; assert_gva_read_matches(&mut sbox, code_gva, 128); } @@ -2334,7 +2334,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.layout.get_guest_code_gva() as u64; assert_gva_read_matches(&mut sbox, code_gva, 4096); } @@ -2344,7 +2344,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.layout.get_guest_code_gva() as u64; // Start 1 byte before the second page boundary and read 4097 bytes // (spans 2 full page boundaries). let start = code_gva + 4096 - 1; @@ -2360,7 +2360,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.layout.get_guest_code_gva() as u64; assert_gva_read_matches(&mut sbox, code_gva, 4096 * 2); } @@ -2371,7 +2371,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.layout.get_guest_code_gva() as u64; // 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/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs index d2a7a86f2f..c3eac0a063 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs @@ -594,7 +594,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, + code_virt_base: self.layout.get_guest_code_gva() as u64, sregs: *sregs, #[cfg(target_arch = "x86_64")] msrs: self @@ -816,6 +816,12 @@ impl Snapshot { cfg.layout.init_data_size, init_data_perms, )?; + let code_gva = if cfg.code_virt_base == 0 { + layout.get_guest_code_gpa() as u64 + } else { + cfg.code_virt_base + }; + layout.set_code_gva(code_gva)?; // `snapshot_size` and `pt_size` are independent fields. if let Some(pt) = cfg.layout.pt_size { layout.set_pt_size(pt)?; @@ -892,7 +898,6 @@ 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 359b31a4ce..08893dfec1 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs @@ -2958,22 +2958,20 @@ 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 +/// The code GVA 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() { +fn round_trip_preserves_code_gva() { 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 original = snap.layout().get_guest_code_gva(); + assert_ne!(original, 0, "fixture must have a non-zero code GVA"); 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); + assert_eq!(loaded.layout().get_guest_code_gva(), original); } /// The returned digest is the sha256 of the manifest blob, matching the diff --git a/src/hyperlight_host/src/sandbox/snapshot/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/mod.rs index fe523c1a5a..a23fcde1a1 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/mod.rs @@ -88,13 +88,8 @@ 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 - /// (`code_virt_base + e_entry - base_va`). Unlike `next_action`, which + /// (`code GVA + 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 @@ -326,17 +321,14 @@ impl Snapshot { guest_blob_mem_flags, )?; - let load_addr = layout.get_guest_code_address() as u64; + let load_addr = layout.get_guest_code_gpa() 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 code_gva = if is_pie { load_addr } else { base_va }; + layout.set_code_gva(code_gva)?; + let regions = layout.get_memory_regions()?; let mut memory = vec![0; layout.get_memory_size()?]; @@ -399,7 +391,7 @@ impl Snapshot { ) })?; - let entrypoint_gva = code_virt_base + entrypoint_offset; + let entrypoint_gva = layout.get_guest_code_gva() as u64 + entrypoint_offset; Ok(Self { memory: ReadonlySharedMemory::from_bytes(&memory, layout.snapshot_size())?, @@ -410,7 +402,6 @@ 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 { @@ -439,7 +430,6 @@ 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, @@ -601,7 +591,6 @@ impl Snapshot { #[cfg(target_arch = "x86_64")] msrs: Some(msrs), next_action, - code_virt_base, original_entrypoint, snapshot_generation, host_functions, @@ -821,7 +810,6 @@ mod tests { #[cfg(target_arch = "x86_64")] Vec::new(), super::NextAction::None, - 0, // code_virt_base 0, 1, HostFunctionDetails::default(), @@ -842,7 +830,6 @@ 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/src/sandbox/trace/mem_profile.rs b/src/hyperlight_host/src/sandbox/trace/mem_profile.rs index f89b83bc43..f4c92a6def 100644 --- a/src/hyperlight_host/src/sandbox/trace/mem_profile.rs +++ b/src/hyperlight_host/src/sandbox/trace/mem_profile.rs @@ -105,7 +105,7 @@ impl MemTraceInfo { &mut *cache, &mut read_stack, ); - iter.map(|f| Ok(f.address() - mem_mgr.layout.get_guest_code_address() as u64)) + iter.map(|f| Ok(f.address() - mem_mgr.layout.get_guest_code_gva() as u64)) .collect() .map_err(|e| new_error!("couldn't unwind: {}", e)) } From 7160bdee02a23964c56357b512b472afcea559e8 Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:45:07 +0000 Subject: [PATCH 5/7] Store code GVA with snapshot memory layout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: cshung <3410332+cshung@users.noreply.github.com> --- .../src/sandbox/snapshot/file/config.rs | 20 +++++++++++-------- .../src/sandbox/snapshot/file/mod.rs | 6 +++--- .../src/sandbox/snapshot/file_tests.rs | 2 +- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs index 38322ef452..9cef68445d 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs @@ -180,12 +180,6 @@ pub(super) struct OciSnapshotConfig { /// Initialise->Call transition. Fills `AT_ENTRY` in core dumps so /// gdb resolves PIE symbols. 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, @@ -216,6 +210,10 @@ pub(super) struct MemoryLayout { pub(super) output_data_size: usize, pub(super) heap_size: usize, pub(super) code_size: usize, + /// Virtual base address of the code region. A value of zero means the + /// code region is identity mapped. + #[serde(default)] + pub(super) code_virt_base: u64, pub(super) init_data_size: usize, /// Memory region flag bits. `None` means default permissions. pub(super) init_data_permissions: Option, @@ -485,8 +483,8 @@ impl OciSnapshotConfig { // The saved dispatch entrypoint must be in the executable code // 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 + let code_lo = if self.layout.code_virt_base != 0 { + self.layout.code_virt_base } else { SandboxMemoryLayout::BASE_ADDRESS as u64 }; @@ -784,6 +782,7 @@ mod tests { output_data_size: 0, heap_size: 0, code_size: 0, + code_virt_base: 0, init_data_size: 0, init_data_permissions: None, scratch_size: 0, @@ -860,6 +859,9 @@ mod schema_pin { "entrypoint_addr": 8192, "original_entrypoint_addr": 4096, "code_virt_base": 0, + "sregs": { + "cs": { + "base": 1, "limit": 2, "selector": 3, "type_": 4, @@ -1014,6 +1016,7 @@ mod schema_pin { "output_data_size": 2, "heap_size": 3, "code_size": 4, + "code_virt_base": 0, "init_data_size": 5, "init_data_permissions": null, "scratch_size": 8, @@ -1057,6 +1060,7 @@ mod schema_pin { "output_data_size": 2, "heap_size": 3, "code_size": 4, + "code_virt_base": 0, "init_data_size": 5, "init_data_permissions": null, "scratch_size": 8, diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs index c3eac0a063..ffa6e7c27d 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs @@ -594,7 +594,6 @@ impl Snapshot { stack_top_gva: self.stack_top_gva, entrypoint_addr, original_entrypoint_addr: self.original_entrypoint, - code_virt_base: self.layout.get_guest_code_gva() as u64, sregs: *sregs, #[cfg(target_arch = "x86_64")] msrs: self @@ -607,6 +606,7 @@ impl Snapshot { output_data_size: l.output_data_size(), heap_size: l.heap_size(), code_size: l.code_size(), + code_virt_base: l.get_guest_code_gva() as u64, init_data_size: l.init_data_size(), init_data_permissions: l.init_data_permissions().map(|f| f.bits()), scratch_size: l.get_scratch_size(), @@ -816,10 +816,10 @@ impl Snapshot { cfg.layout.init_data_size, init_data_perms, )?; - let code_gva = if cfg.code_virt_base == 0 { + let code_gva = if cfg.layout.code_virt_base == 0 { layout.get_guest_code_gpa() as u64 } else { - cfg.code_virt_base + cfg.layout.code_virt_base }; layout.set_code_gva(code_gva)?; // `snapshot_size` and `pt_size` are independent fields. diff --git a/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs b/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs index 08893dfec1..018a327743 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs @@ -2046,7 +2046,7 @@ fn original_entrypoint_addr_zero_rejected() { 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_virt_base = cfg["layout"]["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 beyond_code = code_virt_base + code_size.next_multiple_of(page_size); From c3b189736384d3443abac18bfd0bd935e541b76e Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:45:07 +0000 Subject: [PATCH 6/7] Fix ELF relative relocation load bias Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: cshung <3410332+cshung@users.noreply.github.com> --- src/hyperlight_host/src/mem/elf.rs | 96 +++++++++++++++++-- src/hyperlight_host/src/mem/exe.rs | 4 +- .../src/sandbox/snapshot/file/config.rs | 3 - .../src/sandbox/snapshot/mod.rs | 2 +- src/hyperlight_host/tests/integration_test.rs | 1 - 5 files changed, 93 insertions(+), 13 deletions(-) diff --git a/src/hyperlight_host/src/mem/elf.rs b/src/hyperlight_host/src/mem/elf.rs index d18b847bfd..7b2fa0a2fe 100644 --- a/src/hyperlight_host/src/mem/elf.rs +++ b/src/hyperlight_host/src/mem/elf.rs @@ -15,6 +15,53 @@ use goblin::elf64::program_header::PT_LOAD; use super::exe::LoadInfo; use crate::{Result, log_then_return, new_error}; +fn apply_relative_relocation( + name: &str, + relocation_va: u64, + addend: i64, + base_va: u64, + load_gva: u64, + target: &mut [u8], +) -> Result<()> { + let offset = relocation_va.checked_sub(base_va).ok_or_else(|| { + new_error!( + "{} target VA ({:#x}) is below ELF base VA ({:#x})", + name, + relocation_va, + base_va + ) + })?; + let offset: usize = offset.try_into()?; + let end = offset + .checked_add(size_of::()) + .ok_or_else(|| new_error!("{} target offset overflow", name))?; + let target_len = target.len(); + let destination = target.get_mut(offset..end).ok_or_else(|| { + new_error!( + "{} target range [{:#x}, {:#x}) exceeds loaded image size ({:#x})", + name, + offset, + end, + target_len + ) + })?; + + let load_bias = i128::from(load_gva) - i128::from(base_va); + let value = i128::from(addend) + .checked_add(load_bias) + .and_then(|value| u64::try_from(value).ok()) + .ok_or_else(|| { + new_error!( + "{} result does not fit in u64: addend ({:#x}) + load bias ({:#x})", + name, + addend, + load_bias + ) + })?; + destination.copy_from_slice(&value.to_le_bytes()); + Ok(()) +} + #[cfg(feature = "mem_profile")] struct ResolvedSectionHeader { name: String, @@ -189,7 +236,7 @@ impl ElfInfo { .unwrap(); (max_phdr.p_vaddr + max_phdr.p_memsz - self.get_base_va()) as usize } - pub(crate) fn load_at(self, load_addr: usize, target: &mut [u8]) -> Result { + pub(crate) fn load_at(self, load_gva: u64, target: &mut [u8]) -> Result { let base_va = self.get_base_va(); for phdr in self.phdrs.iter().filter(|phdr| phdr.p_type == PT_LOAD) { let start_va = (phdr.p_vaddr - base_va) as usize; @@ -208,8 +255,14 @@ impl ElfInfo { match r.r_type { R_AARCH64_RELATIVE => { let addend = get_addend("R_AARCH64_RELATIVE", r)?; - target[r.r_offset as usize..r.r_offset as usize + 8] - .copy_from_slice(&(load_addr as i64 + addend).to_le_bytes()); + apply_relative_relocation( + "R_AARCH64_RELATIVE", + r.r_offset, + addend, + base_va, + load_gva, + target, + )?; } R_AARCH64_NONE => {} _ => { @@ -220,8 +273,14 @@ impl ElfInfo { match r.r_type { R_X86_64_RELATIVE => { let addend = get_addend("R_X86_64_RELATIVE", r)?; - target[r.r_offset as usize..r.r_offset as usize + 8] - .copy_from_slice(&(load_addr as i64 + addend).to_le_bytes()); + apply_relative_relocation( + "R_X86_64_RELATIVE", + r.r_offset, + addend, + base_va, + load_gva, + target, + )?; } R_X86_64_NONE => {} _ => { @@ -236,7 +295,7 @@ impl ElfInfo { Ok(LoadInfo { info: Arc::new(UnwindInfo { payload: self.payload, - load_addr: load_addr as u64, + load_addr: load_gva, va_size, base_svma, shdrs: self.shdrs, @@ -248,3 +307,28 @@ impl ElfInfo { } } } + +#[cfg(test)] +mod tests { + use super::apply_relative_relocation; + + #[test] + fn relative_relocation_uses_link_base() { + let mut target = [0u8; 16]; + + apply_relative_relocation("R_RELATIVE", 0x1008, 0x1010, 0x1000, 0x3000, &mut target) + .unwrap(); + + assert_eq!(u64::from_le_bytes(target[8..].try_into().unwrap()), 0x3010); + } + + #[test] + fn relative_relocation_supports_negative_load_bias() { + let mut target = [0u8; 8]; + + apply_relative_relocation("R_RELATIVE", 0x1000, 0x1010, 0x1000, 0x800, &mut target) + .unwrap(); + + assert_eq!(u64::from_le_bytes(target), 0x810); + } +} diff --git a/src/hyperlight_host/src/mem/exe.rs b/src/hyperlight_host/src/mem/exe.rs index 3e8ec5173b..e2818f70bc 100644 --- a/src/hyperlight_host/src/mem/exe.rs +++ b/src/hyperlight_host/src/mem/exe.rs @@ -106,9 +106,9 @@ impl ExeInfo { // copying into target, but the PE loader chooses to apply // relocations in its owned representation of the PE contents, // which requires it to be &mut. - pub fn load(self, load_addr: usize, target: &mut [u8]) -> Result { + pub fn load(self, load_gva: u64, target: &mut [u8]) -> Result { match self { - ExeInfo::Elf(elf) => elf.load_at(load_addr, target), + ExeInfo::Elf(elf) => elf.load_at(load_gva, target), } } } diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs index 9cef68445d..2844a3a3a6 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs @@ -773,7 +773,6 @@ mod tests { stack_top_gva: 0x2000, entrypoint_addr: SandboxMemoryLayout::BASE_ADDRESS as u64, original_entrypoint_addr: SandboxMemoryLayout::BASE_ADDRESS as u64, - code_virt_base: 0, sregs: distinct_sregs(), #[cfg(target_arch = "x86_64")] msrs: Vec::new(), @@ -858,7 +857,6 @@ mod schema_pin { "stack_top_gva": 3735928559, "entrypoint_addr": 8192, "original_entrypoint_addr": 4096, - "code_virt_base": 0, "sregs": { "cs": { "base": 1, @@ -1046,7 +1044,6 @@ mod schema_pin { "stack_top_gva": 3735928559, "entrypoint_addr": 8192, "original_entrypoint_addr": 4096, - "code_virt_base": 0, "sregs": { "tcr_el1": 1, "mair_el1": 2, diff --git a/src/hyperlight_host/src/sandbox/snapshot/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/mod.rs index a23fcde1a1..c122db9789 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/mod.rs @@ -333,7 +333,7 @@ impl Snapshot { let mut memory = vec![0; layout.get_memory_size()?]; let load_info = exe_info.load( - load_addr.try_into()?, + layout.get_guest_code_gva() as u64, &mut memory[layout.guest_code_offset()..], )?; diff --git a/src/hyperlight_host/tests/integration_test.rs b/src/hyperlight_host/tests/integration_test.rs index ca4174fadc..2d05b48067 100644 --- a/src/hyperlight_host/tests/integration_test.rs +++ b/src/hyperlight_host/tests/integration_test.rs @@ -7,7 +7,6 @@ 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, SandboxBuilder, UninitializedSandbox}; use hyperlight_testing::simplelogger::{LOGGER, SimpleLogger}; use serial_test::serial; From 658860ca370b279c66170eb74dde4142c9086278 Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:44:53 -0700 Subject: [PATCH 7/7] feat: enable ASLR for PIE guest binaries Add pick_aslr_address() that selects a random page-aligned virtual base for PIE code regions within 47-bit canonical user space. For PIE guests, Snapshot::new() calls this instead of identity-mapping code at the physical load address, then passes the result to set_code_gva(). Non-PIE binaries continue using their declared ELF base VA. 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/hyperlight_vm/aarch64.rs | 2 +- src/hyperlight_host/src/hypervisor/mod.rs | 4 +-- src/hyperlight_host/src/mem/layout.rs | 28 +++++++++++++++++-- .../src/sandbox/initialized_multi_use.rs | 7 +++-- .../src/sandbox/snapshot/mod.rs | 9 +++++- 5 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs index 4ced7f9e9f..75d0870421 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs @@ -12,7 +12,7 @@ use super::{ #[cfg(hvf)] use crate::hypervisor::HvfInterruptHandle; use crate::hypervisor::InterruptHandleImpl; -#[cfg(any(kvm, mshv3))] +#[cfg(target_os = "linux")] use crate::hypervisor::LinuxInterruptHandle; #[cfg(gdb)] use crate::hypervisor::gdb::{DebugCommChannel, DebugMsg, DebugResponse}; diff --git a/src/hyperlight_host/src/hypervisor/mod.rs b/src/hyperlight_host/src/hypervisor/mod.rs index aee561b477..cc7dc981ce 100644 --- a/src/hyperlight_host/src/hypervisor/mod.rs +++ b/src/hyperlight_host/src/hypervisor/mod.rs @@ -508,8 +508,8 @@ pub(crate) mod tests { )?; // Set up required parameters for initialise - let peb_addr = RawPtr::from(0x1000u64); // Dummy PEB address - let seed = 12345u64; // Random seed + let peb_addr = RawPtr::from(0x230000u64); + let seed = 1234567890u64; let host_funcs = Arc::new(Mutex::new(FunctionRegistry::default())); let guest_max_log_level = Some(tracing_core::LevelFilter::ERROR); diff --git a/src/hyperlight_host/src/mem/layout.rs b/src/hyperlight_host/src/mem/layout.rs index 00d697b838..9b9b7386c6 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -327,7 +327,7 @@ impl SandboxMemoryLayout { output_data_size, heap_size, code_size, - code_gva, + code_gva: _, init_data_size, init_data_permissions, scratch_size, @@ -338,7 +338,6 @@ impl SandboxMemoryLayout { && *output_data_size == other.output_data_size && *heap_size == other.heap_size && *code_size == other.code_size - && *code_gva == other.code_gva && *init_data_size == other.init_data_size && *init_data_permissions == other.init_data_permissions && *scratch_size == other.scratch_size @@ -604,6 +603,30 @@ impl SandboxMemoryLayout { Ok(()) } + /// Pick a random page-aligned virtual address for ASLR. + /// + /// The address is chosen within 47-bit canonical user space: + /// lower bound 16 MiB (above identity-mapped layout regions), + /// upper bound accounts for `loaded_size` so the mapping fits. + pub(crate) fn pick_aslr_address(loaded_size: u64) -> Result { + use rand::RngExt; + let code_size_pages = loaded_size.div_ceil(PAGE_SIZE as u64); + let min_page = 0x1000_u64; // 0x1000 * PAGE_SIZE = 0x1000000 (16 MiB) + 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 mut rng = rand::rng(); + let page_number = rng.random_range(min_page..max_page); + page_number + .checked_mul(PAGE_SIZE as u64) + .ok_or_else(|| new_error!("ASLR page number overflow")) + } + #[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] @@ -902,7 +925,6 @@ mod tests { |l| l.output_data_size += PAGE_SIZE, |l| l.heap_size += PAGE_SIZE, |l| l.code_size += PAGE_SIZE, - |l| l.code_gva += PAGE_SIZE, |l| l.init_data_size += PAGE_SIZE, |l| l.scratch_size += PAGE_SIZE, |l| { diff --git a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs index dc55986d0e..7eb8143b17 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -2298,9 +2298,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); diff --git a/src/hyperlight_host/src/sandbox/snapshot/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/mod.rs index c122db9789..9c0923fa91 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/mod.rs @@ -326,7 +326,14 @@ impl Snapshot { let entrypoint_va: u64 = exe_info.entrypoint().into(); let is_pie = exe_info.is_pie(); - let code_gva = if is_pie { load_addr } else { base_va }; + let code_gva = if is_pie { + SandboxMemoryLayout::pick_aslr_address(exe_info.loaded_size() as u64)? + } else if base_va == load_addr { + // PIE binary at default load address (identity-mapped) + load_addr + } else { + base_va + }; layout.set_code_gva(code_gva)?; let regions = layout.get_memory_regions()?;