diff --git a/.github/workflows/dep_build_guests.yml b/.github/workflows/dep_build_guests.yml index 251852dd5..984810cdf 100644 --- a/.github/workflows/dep_build_guests.yml +++ b/.github/workflows/dep_build_guests.yml @@ -74,6 +74,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 }} 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 401897425..0fda7fff2 100644 --- a/Justfile +++ b/Justfile @@ -45,7 +45,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-cargo-hyperlight: cargo install --locked cargo-hyperlight @@ -69,6 +69,20 @@ build-rust-guests target=default-target features="": (witguest-wit) (ensure-carg 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. +# Uses cargo hyperlight with non-PIE link flags and a separate target-dir +# to avoid clobbering the normal PIE guest artifacts. +build-rust-guests-non-pie target=default-target: (ensure-cargo-hyperlight) + {{ if os() == "windows" { "$env:RUSTFLAGS='-C relocation-model=static -C link-args=--no-pie -C link-args=--image-base=0x1000000';" } else { "" } }} cd src/tests/rust_guests/simpleguest && {{ if os() == "windows" { "" } else { "RUSTFLAGS='-C relocation-model=static -C link-args=--no-pie -C link-args=--image-base=0x1000000'" } }} cargo hyperlight build --target-dir ../target-non-pie --profile={{ if target == "debug" { "dev" } else { target } }} + +non_pie_guests_target := "src/tests/rust_guests/target-non-pie/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/mem/elf.rs b/src/hyperlight_host/src/mem/elf.rs index 16e506eac..5f533d0cc 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")] @@ -45,6 +46,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, @@ -146,6 +149,7 @@ impl ElfInfo { .collect(), entry: elf.entry, relocs, + is_pie: elf.header.e_type == ET_DYN, guest_bin_version, }) } @@ -171,6 +175,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 97874ae6e..7649846ce 100644 --- a/src/hyperlight_host/src/mem/exe.rs +++ b/src/hyperlight_host/src/mem/exe.rs @@ -88,6 +88,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/sandbox/snapshot/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/mod.rs index 3d1afcb9c..69b90b3ba 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/mod.rs @@ -28,7 +28,9 @@ use crate::Result; use crate::hypervisor::regs::CommonSpecialRegisters; use crate::mem::exe::{ExeInfo, LoadInfo}; use crate::mem::layout::SandboxMemoryLayout; -use crate::mem::memory_region::{GuestMemoryRegion, MemoryRegion, MemoryRegionFlags}; +use crate::mem::memory_region::{ + GuestMemoryRegion, MemoryRegion, MemoryRegionFlags, MemoryRegionType, +}; use crate::mem::mgr::{GuestPageTableBuffer, SnapshotSharedMemory}; use crate::mem::shared_mem::{ReadonlySharedMemory, SharedMemory}; use crate::sandbox::SandboxConfiguration; @@ -307,6 +309,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(); + + // Determine the virtual base address for the code region. + // For non-PIE binaries (ET_EXEC), the code should appear at the + // ELF's declared virtual address. For PIE binaries (ET_DYN), + // we use the physical load address (identity mapping). + let code_virt_base = if !is_pie { base_va } else { load_addr }; let mut memory = vec![0; layout.get_memory_size()?]; @@ -323,6 +333,29 @@ impl Snapshot { // Set up page table entries for the snapshot let pt_buf = GuestPageTableBuffer::new(layout.get_pt_base_gpa() as usize); + // Verify the non-PIE code mapping does not conflict with other mappings. + // PIE uses identity mapping so the code region can't conflict by definition. + if !is_pie { + let code_virt_end = code_virt_base + loaded_size; + for rgn in layout.get_memory_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(crate::new_error!( + "Code mapping [{:#x}, {:#x}) conflicts with {:?} region [{:#x}, {:#x})", + code_virt_base, + code_virt_end, + rgn.region_type, + rgn_start, + rgn_end, + )); + } + } + } + // 1. Map the (ideally readonly) pages of snapshot data for rgn in layout.get_memory_regions_::(())?.iter() { let readable = rgn.flags.contains(MemoryRegionFlags::READ); @@ -340,9 +373,25 @@ impl Snapshot { executable, }) }; + + // For the code region, use code_virt_base as the GVA. + // For non-PIE this is the ELF's declared base VA (non-identity mapping). + // For PIE this should equal the GPA (identity mapping). + let virt_base = if rgn.region_type == MemoryRegionType::Code { + if is_pie { + assert_eq!( + code_virt_base, rgn.guest_region.start as u64, + "PIE code region should be identity-mapped" + ); + } + code_virt_base + } else { + rgn.guest_region.start as u64 + }; + let mapping = Mapping { phys_base: rgn.guest_region.start as u64, - virt_base: rgn.guest_region.start as u64, + virt_base, len: rgn.guest_region.len() as u64, kind, user_accessible: false, @@ -361,13 +410,21 @@ impl Snapshot { - hyperlight_common::layout::SCRATCH_TOP_EXN_STACK_OFFSET + 1; + 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 + ) + })?; + Ok(Self { memory: ReadonlySharedMemory::from_bytes(&memory, layout.snapshot_size)?, layout, load_info, stack_top_gva: exn_stack_top_gva, sregs: None, - entrypoint: NextAction::Initialise(load_addr + entrypoint_va - base_va), + entrypoint: NextAction::Initialise(code_virt_base + entrypoint_offset), snapshot_generation: 0, host_functions: HostFunctionDetails { host_functions: None, diff --git a/src/hyperlight_host/tests/integration_test.rs b/src/hyperlight_host/tests/integration_test.rs index 6b5a7f8e3..703913c93 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; @@ -1872,3 +1872,17 @@ fn hw_timer_interrupts() { ); }); } + +#[test] +#[serial] +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), 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 aa3af3237..b2d1d160d 100644 --- a/src/hyperlight_testing/src/lib.rs +++ b/src/hyperlight_testing/src/lib.rs @@ -88,6 +88,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"