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/8] 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/8] 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/8] 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/8] 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/8] 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/8] 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 ebb641b36b58dddd63869934a74d6fae6b369f18 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 7/8] 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/hyperlight_host/src/hypervisor/gdb/mod.rs | 2 +- .../src/hypervisor/hyperlight_vm/aarch64.rs | 2 +- src/hyperlight_host/src/mem/layout.rs | 107 ++++++++++++++---- .../src/sandbox/initialized_multi_use.rs | 17 +-- .../src/sandbox/snapshot/file/config.rs | 7 -- .../src/sandbox/snapshot/mod.rs | 27 ++++- 7 files changed, 118 insertions(+), 45 deletions(-) diff --git a/Justfile b/Justfile index c426140fe5..1c07926e9e 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/gdb/mod.rs b/src/hyperlight_host/src/hypervisor/gdb/mod.rs index 79ca6e083a..c42d894d08 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_gva() as u64 + self.mem_mgr.code_virt_base } /// 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/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/mem/layout.rs b/src/hyperlight_host/src/mem/layout.rs index 00d697b838..e5bc4da1da 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -573,35 +573,95 @@ impl SandboxMemoryLayout { Ok(regions) } - /// 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_gva, - self.code_size - ) - })?; - for region in self.get_memory_regions()? { - if region.region_type == Code { + /// 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, 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`). + /// + /// 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( + &self, + is_pie: bool, + elf_base_va: u64, + loaded_size: u64, + ) -> Result<(u64, Vec>)> { + let code_size_pages = loaded_size.div_ceil(PAGE_SIZE 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 as u64) + .ok_or_else(|| new_error!("ASLR page number overflow"))? + }; + + let mut regions = self.get_memory_regions()?; + + // 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; } - if code_gva < region.guest_region.end && region.guest_region.start < code_end { + 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_gva, - code_end, - region.region_type, - region.guest_region.start, - region.guest_region.end, + "Code mapping [{:#x}, {:#x}) conflicts with {:?} region [{:#x}, {:#x})", + code_virt_base, + code_virt_end, + rgn.region_type, + rgn_start, + rgn_end, )); } } - self.code_gva = code_gva; - Ok(()) + + // 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); + } + } + + 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)) } #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] @@ -727,6 +787,7 @@ impl SandboxMemoryLayout { } /// Guest physical address of the code section. + #[allow(dead_code)] pub(crate) fn get_guest_code_gpa(&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 dc55986d0e..0a03fe264d 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); @@ -2324,7 +2327,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_gva() as u64; + let code_gva = sbox.mem_mgr.code_virt_base; assert_gva_read_matches(&mut sbox, code_gva, 128); } @@ -2334,7 +2337,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_gva() as u64; + let code_gva = sbox.mem_mgr.code_virt_base; assert_gva_read_matches(&mut sbox, code_gva, 4096); } @@ -2344,7 +2347,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_gva() 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; @@ -2360,7 +2363,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_gva() as u64; + let code_gva = sbox.mem_mgr.code_virt_base; assert_gva_read_matches(&mut sbox, code_gva, 4096 * 2); } @@ -2371,7 +2374,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_gva() 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 2844a3a3a6..b9926978d3 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs @@ -505,13 +505,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. It must point // inside the snapshot region, like `entrypoint_addr`. diff --git a/src/hyperlight_host/src/sandbox/snapshot/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/mod.rs index c122db9789..be7fac50ba 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/mod.rs @@ -321,19 +321,20 @@ impl Snapshot { guest_blob_mem_flags, )?; - 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 is_pie = exe_info.is_pie(); - let code_gva = if is_pie { load_addr } else { base_va }; - layout.set_code_gva(code_gva)?; - let regions = layout.get_memory_regions()?; + let (code_virt_base, regions) = layout.get_guest_regions_with_code_va( + is_pie, + base_va, + exe_info.loaded_size() as u64, + )?; let mut memory = vec![0; layout.get_memory_size()?]; let load_info = exe_info.load( - layout.get_guest_code_gva() as u64, + code_virt_base.try_into()?, &mut memory[layout.guest_code_offset()..], )?; @@ -391,7 +392,15 @@ impl Snapshot { ) })?; - let entrypoint_gva = layout.get_guest_code_gva() as u64 + 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())?, @@ -651,6 +660,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 From 8388489389a2798f2fc35633d8a7b55044c6f68a Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:49:18 +0000 Subject: [PATCH 8/8] Fix ARM64 ASLR validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: cshung <3410332+cshung@users.noreply.github.com> --- Justfile | 1 - src/hyperlight_host/src/hypervisor/mod.rs | 4 ++-- src/hyperlight_host/src/sandbox/snapshot/file/config.rs | 7 +++++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Justfile b/Justfile index 1c07926e9e..c426140fe5 100644 --- a/Justfile +++ b/Justfile @@ -76,7 +76,6 @@ 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/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/sandbox/snapshot/file/config.rs b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs index b9926978d3..2844a3a3a6 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs @@ -505,6 +505,13 @@ 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. It must point // inside the snapshot region, like `entrypoint_addr`.