diff --git a/.github/workflows/dep_build_guests.yml b/.github/workflows/dep_build_guests.yml index 41cb58738..3e2fafff1 100644 --- a/.github/workflows/dep_build_guests.yml +++ b/.github/workflows/dep_build_guests.yml @@ -54,7 +54,7 @@ jobs: run: | sudo chown -R $(id -u):$(id -g) /opt/cargo || true - # cargo-hyperlight builds a custom sysroot for x86_64-hyperlight-none target. + # cargo-hyperlight builds a custom sysroot for the Hyperlight guest target. # rust-cache cleans "anything not a dependency" from target dirs, removing the sysroot. # We cache sysroot separately to avoid rebuilding it (~10s) on every run. - name: Sysroot cache @@ -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,11 @@ jobs: just build-rust-guests ${{ inputs.config }} just move-rust-guests ${{ inputs.config }} + - name: Build non-PIE Rust guests + 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 }} @@ -108,4 +114,3 @@ jobs: path: src/tests/c_guests/bin/${{ inputs.config }}/ retention-days: 1 if-no-files-found: error - diff --git a/.gitignore b/.gitignore index 3aae7b792..27126b8de 100644 --- a/.gitignore +++ b/.gitignore @@ -454,6 +454,7 @@ $RECYCLE.BIN/ # Rust build artifacts **/**target +**/**target-non-pie libhyperlight_host.so libhyperlight_host.d hyperlight_host.dll diff --git a/Justfile b/Justfile index f69d88c41..7beb31626 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 {{ 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/" + 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" } }} + 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/hyperlight_vm/x86_64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs index 372483e3c..86a576c18 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs @@ -930,7 +930,7 @@ mod tests { use crate::hypervisor::regs::{CommonSegmentRegister, CommonTableRegister, MXCSR_DEFAULT}; use crate::hypervisor::virtual_machine::VirtualMachine; use crate::mem::layout::SandboxMemoryLayout; - use crate::mem::memory_region::{GuestMemoryRegion, MemoryRegionFlags}; + use crate::mem::memory_region::MemoryRegionFlags; use crate::mem::mgr::{GuestPageTableBuffer, SandboxMemoryManager}; use crate::mem::ptr::RawPtr; use crate::mem::shared_mem::{ExclusiveSharedMemory, ReadonlySharedMemory}; @@ -1494,16 +1494,12 @@ 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_::(()) - .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); 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/elf.rs b/src/hyperlight_host/src/mem/elf.rs index 7309412f1..aba4e6cb2 100644 --- a/src/hyperlight_host/src/mem/elf.rs +++ b/src/hyperlight_host/src/mem/elf.rs @@ -17,6 +17,7 @@ limitations under the License. #[cfg(feature = "mem_profile")] use std::sync::Arc; +use goblin::elf::header::ET_DYN; #[cfg(target_arch = "aarch64")] use goblin::elf::reloc::{R_AARCH64_NONE, R_AARCH64_RELATIVE}; #[cfg(target_arch = "x86_64")] @@ -42,6 +43,8 @@ pub(crate) struct ElfInfo { shdrs: Vec, entry: u64, relocs: Vec, + /// Whether this is a position-independent executable (ET_DYN). + is_pie: bool, /// The hyperlight version string embedded by `hyperlight-guest-bin`, if /// present. Used to detect version/ABI mismatches between guest and host. guest_bin_version: Option, @@ -143,6 +146,7 @@ impl ElfInfo { .collect(), entry: elf.entry, relocs, + is_pie: elf.header.e_type == ET_DYN, guest_bin_version, }) } @@ -168,6 +172,11 @@ impl ElfInfo { self.entry } + /// Returns whether this is a position-independent executable (ET_DYN). + pub(crate) fn is_pie(&self) -> bool { + self.is_pie + } + /// Returns the hyperlight version string embedded in the guest binary, if /// present. Used to detect version/ABI mismatches between guest and host. pub(crate) fn guest_bin_version(&self) -> Option<&str> { diff --git a/src/hyperlight_host/src/mem/exe.rs b/src/hyperlight_host/src/mem/exe.rs index 9ee316642..17789ad4e 100644 --- a/src/hyperlight_host/src/mem/exe.rs +++ b/src/hyperlight_host/src/mem/exe.rs @@ -89,6 +89,12 @@ impl ExeInfo { ExeInfo::Elf(elf) => Offset::from(elf.entrypoint_va()), } } + /// Returns whether this is a position-independent executable (ET_DYN). + pub fn is_pie(&self) -> bool { + match self { + ExeInfo::Elf(elf) => elf.is_pie(), + } + } /// Returns the base virtual address of the loaded binary (lowest PT_LOAD p_vaddr). pub fn base_va(&self) -> u64 { match self { diff --git a/src/hyperlight_host/src/mem/layout.rs b/src/hyperlight_host/src/mem/layout.rs index 6422b9b11..ea8b120fd 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -66,10 +66,10 @@ use std::mem::size_of; use hyperlight_common::mem::{HyperlightPEB, PAGE_SIZE_USIZE}; use tracing::{Span, instrument}; -use super::memory_region::MemoryRegionType::{Code, Heap, InitData, Peb}; +use super::memory_region::MemoryRegionType::{self, Code, Heap, InitData, Peb}; use super::memory_region::{ - DEFAULT_GUEST_BLOB_MEM_FLAGS, MemoryRegion, MemoryRegion_, MemoryRegionFlags, MemoryRegionKind, - MemoryRegionVecBuilder, + DEFAULT_GUEST_BLOB_MEM_FLAGS, GuestMemoryRegion, MemoryRegion, MemoryRegion_, + MemoryRegionFlags, MemoryRegionVecBuilder, }; #[cfg(readable_shared_mem)] use super::shared_mem::HostSharedMemory; @@ -469,11 +469,8 @@ impl SandboxMemoryLayout { /// Returns the memory regions associated with this memory layout, /// suitable for passing to a hypervisor for mapping into memory - pub(crate) fn get_memory_regions_( - &self, - host_base: K::HostBaseType, - ) -> Result>> { - let mut builder = MemoryRegionVecBuilder::new(Self::BASE_ADDRESS, host_base); + pub(crate) fn get_memory_regions(&self) -> Result>> { + let mut builder = MemoryRegionVecBuilder::new(Self::BASE_ADDRESS, Self::BASE_ADDRESS); // code let peb_offset = builder.push_page_aligned( @@ -555,6 +552,72 @@ 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.checked_add(loaded_size).ok_or_else(|| { + new_error!( + "Code mapping overflow: base {:#x} + size {:#x}", + code_virt_base, + loaded_size + ) + })?; + for rgn in regions.iter() { + if rgn.region_type == MemoryRegionType::Code { + continue; + } + let rgn_start = rgn.guest_region.start as u64; + let rgn_end = rgn_start + 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, + )); + } + } + } + + // 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); + } + } + + Ok((code_virt_base, regions)) + } + #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] pub(crate) fn write_init_data(&self, out: &mut [u8], bytes: &[u8]) -> Result<()> { out[self.init_data_offset()..self.init_data_offset() + self.init_data_size] diff --git a/src/hyperlight_host/src/mem/memory_region.rs b/src/hyperlight_host/src/mem/memory_region.rs index 5d839647a..227de835f 100644 --- a/src/hyperlight_host/src/mem/memory_region.rs +++ b/src/hyperlight_host/src/mem/memory_region.rs @@ -282,20 +282,26 @@ impl MemoryRegionKind for HostGuestMemoryRegion { pub(crate) struct GuestMemoryRegion {} impl MemoryRegionKind for GuestMemoryRegion { - type HostBaseType = (); + type HostBaseType = usize; - fn add(_base: Self::HostBaseType, _size: usize) -> Self::HostBaseType {} + fn add(base: Self::HostBaseType, size: usize) -> Self::HostBaseType { + base + size + } } /// represents a single memory region inside the guest. All memory within a region has /// the same memory permissions #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct MemoryRegion_ { - /// the range of guest memory 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 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, @@ -367,8 +373,9 @@ 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, host_region: last_region.host_region.end..host_end, flags, region_type, diff --git a/src/hyperlight_host/src/mem/mgr.rs b/src/hyperlight_host/src/mem/mgr.rs index c93f1cac1..90395015d 100644 --- a/src/hyperlight_host/src/mem/mgr.rs +++ b/src/hyperlight_host/src/mem/mgr.rs @@ -147,6 +147,10 @@ pub(crate) struct SandboxMemoryManager { /// preserved across the `Initialise` -> `Call` transition so it /// can fill `AT_ENTRY` in guest core dumps. 0 if unknown. pub(crate) original_entrypoint: u64, + /// Virtual base address of the code region. + /// For PIE binaries this equals the physical load address (identity-mapped). + /// For non-PIE binaries this is the ELF-declared base VA. + pub(crate) code_virt_base: u64, /// Buffer for accumulating guest abort messages pub(crate) abort_buffer: Vec, /// Generation counter: how many snapshots have been taken from @@ -288,6 +292,7 @@ where scratch_mem, next_action, original_entrypoint: 0, + code_virt_base: 0, abort_buffer: Vec::new(), snapshot_count: 0, } @@ -323,6 +328,7 @@ where #[cfg(target_arch = "x86_64")] msrs, next_action, + self.code_virt_base, self.original_entrypoint, self.snapshot_count, host_functions, @@ -338,6 +344,7 @@ impl SandboxMemoryManager { let next_action = s.next_action(); let mut mgr = Self::new(layout, shared_mem, scratch_mem, next_action); mgr.original_entrypoint = s.original_entrypoint(); + mgr.code_virt_base = s.code_virt_base; // Inherit the snapshot's generation number for the same // reason `restore_snapshot` does: the guest-visible counter // reflects "which snapshot is the sandbox currently a clone @@ -370,6 +377,7 @@ impl SandboxMemoryManager { layout: self.layout, next_action: self.next_action, original_entrypoint: self.original_entrypoint, + code_virt_base: self.code_virt_base, abort_buffer: self.abort_buffer, snapshot_count: self.snapshot_count, }; @@ -379,6 +387,7 @@ impl SandboxMemoryManager { layout: self.layout, next_action: self.next_action, original_entrypoint: self.original_entrypoint, + code_virt_base: self.code_virt_base, abort_buffer: Vec::new(), // Guest doesn't need abort buffer snapshot_count: self.snapshot_count, }; @@ -519,6 +528,7 @@ impl SandboxMemoryManager { // Carry the guest ELF entry point across restore so crashdumps // report the restored image's entry. self.original_entrypoint = snapshot.original_entrypoint(); + self.code_virt_base = snapshot.code_virt_base; self.update_scratch_bookkeeping()?; Ok((gsnapshot, gscratch)) diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs index 01c2f501b..5c2da1bac 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs @@ -187,6 +187,12 @@ pub(super) struct OciSnapshotConfig { /// treats as unknown. #[serde(default)] pub(super) original_entrypoint_addr: u64, + /// Virtual base address of the code region. For PIE guests this equals + /// the physical load address; for non-PIE guests it is the ELF-declared + /// base VA. Optional: older snapshots deserialize to `0`, meaning + /// identity-mapped (VA == GPA). + #[serde(default)] + pub(super) code_virt_base: u64, /// Special registers captured from the paused vCPU, restored /// verbatim when resuming the call. pub(super) sregs: CommonSpecialRegisters, @@ -478,13 +484,19 @@ impl OciSnapshotConfig { } // The saved dispatch entrypoint must be in the executable code - // region. Code occupies the page-rounded prefix of the snapshot. - let code_lo = SandboxMemoryLayout::BASE_ADDRESS as u64; + // region. For non-PIE or ASLR guests the code region's virtual + // base differs from the physical load address. + let code_lo = if self.code_virt_base != 0 { + self.code_virt_base + } else { + SandboxMemoryLayout::BASE_ADDRESS as u64 + }; let code_hi = code_lo .checked_add(self.layout.code_size.next_multiple_of(PAGE_SIZE) as u64) .ok_or_else(|| { crate::new_error!( - "snapshot layout overflow: BASE_ADDRESS + code_size ({}) does not fit in u64", + "snapshot layout overflow: code_virt_base ({:#x}) + code_size ({}) does not fit in u64", + code_lo, self.layout.code_size ) })?; @@ -505,25 +517,16 @@ impl OciSnapshotConfig { } // ELF entry point GVA for `AT_ENTRY` in core dumps. 0 means - // unknown. Any other value must point inside the snapshot - // region, like `entrypoint_addr`. - let snapshot_hi = code_lo - .checked_add(self.layout.snapshot_size as u64) - .ok_or_else(|| { - crate::new_error!( - "snapshot layout overflow: BASE_ADDRESS + snapshot_size ({}) does not fit in u64", - self.layout.snapshot_size - ) - })?; + // unknown. Any other value must point inside the code region, + // like `entrypoint_addr`. if self.original_entrypoint_addr != 0 - && (self.original_entrypoint_addr < code_lo - || self.original_entrypoint_addr >= snapshot_hi) + && (self.original_entrypoint_addr < code_lo || self.original_entrypoint_addr >= code_hi) { return Err(crate::new_error!( - "snapshot original entrypoint addr {:#x} is outside the snapshot region [{:#x}, {:#x})", + "snapshot original entrypoint addr {:#x} is outside the code region [{:#x}, {:#x})", self.original_entrypoint_addr, code_lo, - snapshot_hi + code_hi )); } @@ -763,6 +766,7 @@ mod tests { stack_top_gva: 0x2000, entrypoint_addr: SandboxMemoryLayout::BASE_ADDRESS as u64, original_entrypoint_addr: 0, + code_virt_base: 0, sregs: distinct_sregs(), #[cfg(target_arch = "x86_64")] msrs: Vec::new(), @@ -846,6 +850,7 @@ mod schema_pin { "stack_top_gva": 3735928559, "entrypoint_addr": 8192, "original_entrypoint_addr": 0, + "code_virt_base": 0, "sregs": { "cs": { "base": 1, @@ -1032,6 +1037,7 @@ mod schema_pin { "stack_top_gva": 3735928559, "entrypoint_addr": 8192, "original_entrypoint_addr": 0, + "code_virt_base": 0, "sregs": { "tcr_el1": 1, "mair_el1": 2, diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs index 2a0f00d2f..064d15a80 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs @@ -610,6 +610,7 @@ impl Snapshot { stack_top_gva: self.stack_top_gva, entrypoint_addr, original_entrypoint_addr: self.original_entrypoint, + code_virt_base: self.code_virt_base, sregs: *sregs, #[cfg(target_arch = "x86_64")] msrs: self @@ -907,6 +908,7 @@ impl Snapshot { msrs: Some(cfg.msrs), next_action, original_entrypoint: cfg.original_entrypoint_addr, + code_virt_base: cfg.code_virt_base, snapshot_generation, host_functions, }) diff --git a/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs b/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs index 4e0604c86..23a7c0b62 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs @@ -25,7 +25,6 @@ use serde_json::Value; use sha2::{Digest as _, Sha256}; use crate::func::Registerable; -use crate::mem::layout::SandboxMemoryLayout; use crate::sandbox::snapshot::{OciDigest, OciReference, OciTag, Snapshot}; use crate::{GuestBinary, HostFunctions, MultiUseSandbox, UninitializedSandbox}; @@ -2037,11 +2036,11 @@ fn original_entrypoint_addr_zero_accepted() { fn entrypoint_addr_outside_code_rejected() { let (_dir, path) = save_for_mutation(); rewrite_config(&path, |cfg| { + let code_virt_base = cfg["code_virt_base"].as_u64().unwrap(); let code_size = cfg["layout"]["code_size"].as_u64().unwrap(); let page_size = hyperlight_common::vmem::PAGE_SIZE as u64; - let peb_addr = - SandboxMemoryLayout::BASE_ADDRESS as u64 + code_size.next_multiple_of(page_size); - cfg["entrypoint_addr"] = Value::from(peb_addr); + let beyond_code = code_virt_base + code_size.next_multiple_of(page_size); + cfg["entrypoint_addr"] = Value::from(beyond_code); }); let err = unwrap_err_snapshot(Snapshot::checked_load( &path, @@ -2954,6 +2953,24 @@ fn save_returns_manifest_digest_that_loads() { assert_eq!(loaded.snapshot_generation(), expected_gen); } +/// `code_virt_base` must survive a save/load round-trip so GDB and +/// tracing can resolve symbols for non-PIE (or ASLR) guests after +/// restoring from a file snapshot. +#[test] +fn round_trip_preserves_code_virt_base() { + let snap = create_snapshot(); + // Default PIE guest is identity-mapped, so code_virt_base should + // equal get_guest_code_address (i.e. the GPA of the code region). + let original = snap.code_virt_base; + assert_ne!(original, 0, "fixture must have a non-zero code_virt_base"); + + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("layout"); + snap.save(&path, &OciTag::new("latest").unwrap()).unwrap(); + let loaded = Snapshot::checked_load(&path, OciTag::new("latest").unwrap()).unwrap(); + assert_eq!(loaded.code_virt_base, original); +} + /// The returned digest is the sha256 of the manifest blob, matching the /// digest recorded for that tag's manifest descriptor in `index.json`. #[test] diff --git a/src/hyperlight_host/src/sandbox/snapshot/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/mod.rs index e1578e2e7..67dd9a771 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/mod.rs @@ -36,7 +36,7 @@ use crate::hypervisor::regs::CommonSpecialRegisters; use crate::hypervisor::regs::MsrEntry; use crate::mem::exe::{ExeInfo, LoadInfo}; use crate::mem::layout::SandboxMemoryLayout; -use crate::mem::memory_region::{GuestMemoryRegion, MemoryRegion, MemoryRegionFlags}; +use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; use crate::mem::mgr::{GuestPageTableBuffer, SnapshotSharedMemory}; use crate::mem::shared_mem::{ReadonlySharedMemory, SharedMemory}; use crate::sandbox::SandboxConfiguration; @@ -101,8 +101,13 @@ pub struct Snapshot { /// The next action that should be performed on this snapshot next_action: NextAction, + /// Virtual base address of the code region. + /// For PIE binaries this equals the physical load address (identity-mapped). + /// For non-PIE binaries this is the ELF-declared base VA. + pub(crate) code_virt_base: u64, + /// Guest virtual address of the guest binary's ELF entry point - /// (`load_addr + e_entry - base_va`). Unlike `next_action`, which + /// (`code_virt_base + e_entry - base_va`). Unlike `next_action`, which /// transitions to `Call(dispatch_addr)` once the guest has run, /// this preserves the original entry across that transition. Used /// to fill `AT_ENTRY` in guest core dumps so a debugger can @@ -337,6 +342,14 @@ impl Snapshot { let load_addr = layout.get_guest_code_address() as u64; let base_va = exe_info.base_va(); let entrypoint_va: u64 = exe_info.entrypoint().into(); + let loaded_size = exe_info.loaded_size() as u64; + let is_pie = exe_info.is_pie(); + + // Get the memory regions with the Code region's guest_virt_addr + // already set to the correct virtual base (identity-mapped for PIE, + // ELF-declared VA for non-PIE), and validate no overlap conflicts. + let (code_virt_base, regions) = + layout.get_guest_regions_with_code_va(is_pie, base_va, loaded_size)?; let mut memory = vec![0; layout.get_memory_size()?]; @@ -354,7 +367,7 @@ impl Snapshot { let pt_buf = GuestPageTableBuffer::new(layout.get_pt_base_gpa() as usize); // 1. Map the (ideally readonly) pages of snapshot data - for rgn in layout.get_memory_regions_::(())?.iter() { + for rgn in regions.iter() { let readable = rgn.flags.contains(MemoryRegionFlags::READ); let executable = rgn.flags.contains(MemoryRegionFlags::EXECUTE); let writable = rgn.flags.contains(MemoryRegionFlags::WRITE); @@ -370,8 +383,9 @@ impl Snapshot { executable, }) }; + 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, @@ -390,7 +404,15 @@ impl Snapshot { - hyperlight_common::layout::SCRATCH_TOP_EXN_STACK_OFFSET + 1; - let entrypoint_gva = load_addr + entrypoint_va - base_va; + let entrypoint_offset = entrypoint_va.checked_sub(base_va).ok_or_else(|| { + crate::new_error!( + "ELF entrypoint VA ({:#x}) is below base VA ({:#x})", + entrypoint_va, + base_va + ) + })?; + + let entrypoint_gva = code_virt_base + entrypoint_offset; Ok(Self { memory: ReadonlySharedMemory::from_bytes(&memory, layout.snapshot_size())?, @@ -401,6 +423,7 @@ impl Snapshot { #[cfg(target_arch = "x86_64")] msrs: None, next_action: NextAction::Initialise(entrypoint_gva), + code_virt_base, original_entrypoint: entrypoint_gva, snapshot_generation: 0, host_functions: HostFunctionDetails { @@ -429,6 +452,7 @@ impl Snapshot { sregs: CommonSpecialRegisters, #[cfg(target_arch = "x86_64")] msrs: Vec, next_action: NextAction, + code_virt_base: u64, original_entrypoint: u64, snapshot_generation: u64, host_functions: HostFunctionDetails, @@ -585,6 +609,7 @@ impl Snapshot { #[cfg(target_arch = "x86_64")] msrs: Some(msrs), next_action, + code_virt_base, original_entrypoint, snapshot_generation, host_functions, @@ -802,6 +827,7 @@ mod tests { #[cfg(target_arch = "x86_64")] Vec::new(), super::NextAction::None, + 0, // code_virt_base 0, 1, HostFunctionDetails::default(), @@ -822,6 +848,7 @@ mod tests { #[cfg(target_arch = "x86_64")] Vec::new(), super::NextAction::None, + 0, // code_virt_base 0, 2, HostFunctionDetails::default(), diff --git a/src/hyperlight_host/tests/integration_test.rs b/src/hyperlight_host/tests/integration_test.rs index b449ea68d..f89268813 100644 --- a/src/hyperlight_host/tests/integration_test.rs +++ b/src/hyperlight_host/tests/integration_test.rs @@ -21,7 +21,7 @@ use std::time::Duration; use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode; use hyperlight_common::log_level::GuestLogFilter; use hyperlight_host::sandbox::SandboxConfiguration; -use hyperlight_host::{HyperlightError, MultiUseSandbox}; +use hyperlight_host::{HyperlightError, MultiUseSandbox, UninitializedSandbox}; use hyperlight_testing::simplelogger::{LOGGER, SimpleLogger}; use serial_test::serial; use tracing_core::LevelFilter; @@ -1890,3 +1890,17 @@ fn hw_timer_interrupts() { ); }); } + +#[test] +fn non_pie_guest_hello_world() { + let path = + hyperlight_testing::simple_guest_non_pie_as_string().expect("non-PIE guest not found"); + let sandbox = + UninitializedSandbox::new(hyperlight_host::GuestBinary::FilePath(path.into()), None) + .unwrap(); + let mut multi_use_sandbox: MultiUseSandbox = sandbox.evolve().unwrap(); + let result: i32 = multi_use_sandbox + .call("PrintOutput", "Hello from non-PIE guest!\n".to_string()) + .unwrap(); + assert_eq!(result, 26); +} diff --git a/src/hyperlight_testing/src/lib.rs b/src/hyperlight_testing/src/lib.rs index 4c7afb971..4e5548769 100644 --- a/src/hyperlight_testing/src/lib.rs +++ b/src/hyperlight_testing/src/lib.rs @@ -103,6 +103,37 @@ pub fn dummy_guest_as_string() -> Result { .ok_or_else(|| anyhow!("couldn't convert dummy guest PathBuf to string")) } +/// Get a fully qualified OS-specific path to the non-PIE simpleguest elf binary +pub fn simple_guest_non_pie_as_string() -> Result { + let buf = rust_guest_non_pie_as_pathbuf("simpleguest"); + buf.to_str() + .map(|s| s.to_string()) + .ok_or_else(|| anyhow!("couldn't convert non-PIE simple guest PathBuf to string")) +} + +/// Get a new `PathBuf` to a specified non-PIE Rust guest +/// $REPO_ROOT/src/tests/rust_guests/bin/${profile}/non_pie/ +fn rust_guest_non_pie_as_pathbuf(guest: &str) -> PathBuf { + let build_dir_selector = if cfg!(debug_assertions) { + "debug" + } else { + "release" + }; + + join_to_path( + MANIFEST_DIR, + vec![ + "..", + "tests", + "rust_guests", + "bin", + build_dir_selector, + "non_pie", + guest, + ], + ) +} + pub fn c_guest_as_pathbuf(guest: &str) -> PathBuf { let build_dir_selector = if cfg!(debug_assertions) { "debug"