Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/dep_build_guests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
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
16 changes: 15 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions src/hyperlight_host/src/mem/elf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -45,6 +46,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 @@ -146,6 +149,7 @@ impl ElfInfo {
.collect(),
entry: elf.entry,
relocs,
is_pie: elf.header.e_type == ET_DYN,
guest_bin_version,
})
}
Expand All @@ -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> {
Expand Down
6 changes: 6 additions & 0 deletions src/hyperlight_host/src/mem/exe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
63 changes: 60 additions & 3 deletions src/hyperlight_host/src/sandbox/snapshot/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()?];

Expand All @@ -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_::<GuestMemoryRegion>(())?.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_::<GuestMemoryRegion>(())?.iter() {
let readable = rgn.flags.contains(MemoryRegionFlags::READ);
Expand All @@ -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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably we should have some check that the executable mapping here does not conflict with any other mappings.

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
};
Comment thread
cshung marked this conversation as resolved.

let mapping = Mapping {
phys_base: rgn.guest_region.start as u64,
virt_base: rgn.guest_region.start as u64,
virt_base,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach probably works for now, especially with executables where all the segments are contiguous. One other thing that we could do while we are already touching this code is switch to mapping the ELF a bit more "properly", by copying every section copied by a segment into the snapshot contiguously (similarly to how it is being done now, but without using phdr.p_vaddr, so that there is no risk of large gaps appearing for no reason) and then making one of these vmem::map calls for each segment (~= PT_LOAD program header). In addition to being more efficient if we encounter a file with gaps between segments (at least a few kilobytes of gaps often exist due to page-aligning the phdr VAs), this would allow us to have better default permissions in the guest page tables by using the permission information in phdr.p_flags. Probably the way to do that would be to remove this mapping of this region entirely and change the exe interface in exe.rs/implementation in elf.rs to replace load/load_at with functions that take a mapping operations and do the mapping as they walk over the phdrs. That would be my slight preference/how I would do it, but I will leave it up to you whether or not to pursue it, since it is a nontrivially-larger change.

(Unrelated, not-really-review note: the need to fix up the physical vs virtual discontinuity is continuing a trend of several things that makes me slightly question the actual utility of SandboxMemoryLayout::get_memory_regions. It might be worth thinking about whether it makes sense to get rid of that / some more bits of SandboxMemoryLayout at some point, but that's definitely not a discussion for this PR).

len: rgn.guest_region.len() as u64,
kind,
user_accessible: false,
Expand All @@ -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,
Expand Down
16 changes: 15 additions & 1 deletion src/hyperlight_host/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
31 changes: 31 additions & 0 deletions src/hyperlight_testing/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,37 @@ pub fn dummy_guest_as_string() -> Result<String> {
.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<String> {
Comment thread
cshung marked this conversation as resolved.
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"
Expand Down
Loading