Skip to content
9 changes: 7 additions & 2 deletions .github/workflows/dep_build_guests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,15 @@ 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
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
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
Expand All @@ -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 }}
Expand All @@ -108,4 +114,3 @@ jobs:
path: src/tests/c_guests/bin/${{ inputs.config }}/
retention-days: 1
if-no-files-found: error

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ $RECYCLE.BIN/

# Rust build artifacts
**/**target
**/**target-non-pie
libhyperlight_host.so
libhyperlight_host.d
hyperlight_host.dll
Expand Down
18 changes: 17 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/hyperlight_host/src/hypervisor/gdb/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 5 additions & 9 deletions src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -1425,16 +1425,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_::<GuestMemoryRegion>(())
.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 {
Expand Down Expand Up @@ -1480,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();
Expand Down Expand Up @@ -2185,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();

Expand Down
106 changes: 100 additions & 6 deletions src/hyperlight_host/src/mem/elf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -14,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::<u64>())
.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,
Expand All @@ -29,6 +77,8 @@ pub(crate) struct ElfInfo {
shdrs: Vec<ResolvedSectionHeader>,
entry: u64,
relocs: Vec<Reloc>,
/// 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<String>,
Expand Down Expand Up @@ -115,6 +165,7 @@ impl ElfInfo {

let phdrs = std::mem::take(&mut elf.program_headers);
let entry = elf.entry;
let is_pie = elf.header.e_type == ET_DYN;
#[cfg(feature = "mem_profile")]
let shdrs = elf
.section_headers
Expand All @@ -138,6 +189,7 @@ impl ElfInfo {
shdrs,
entry,
relocs,
is_pie,
guest_bin_version,
})
}
Expand All @@ -163,6 +215,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> {
Expand All @@ -188,7 +245,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<LoadInfo> {
pub(crate) fn load_at(self, load_gva: u64, target: &mut [u8]) -> Result<LoadInfo> {
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;
Expand All @@ -207,8 +264,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 => {}
_ => {
Expand All @@ -219,8 +282,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 => {}
_ => {
Expand All @@ -235,7 +304,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,
Expand All @@ -247,3 +316,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);
}
}
10 changes: 8 additions & 2 deletions src/hyperlight_host/src/mem/exe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -100,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<LoadInfo> {
pub fn load(self, load_gva: u64, target: &mut [u8]) -> Result<LoadInfo> {
match self {
ExeInfo::Elf(elf) => elf.load_at(load_addr, target),
ExeInfo::Elf(elf) => elf.load_at(load_gva, target),
}
}
}
Expand Down
Loading
Loading