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
12 changes: 12 additions & 0 deletions docs/how-to-build-a-hyperlight-guest-binary.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,15 @@ latest release page that contain: the `hyperlight_guest.h` header and the
C API library.
The `hyperlight_guest.h` header contains the corresponding APIs to register
guest functions and call host functions from within the guest.

## Version compatibility

Guest binaries built with `hyperlight-guest-bin` automatically embed the crate
version in a custom ELF section (`.hyperlight_guest_bin_version`). When the host
loads a guest binary, it checks this version and rejects the binary if it does
not match the host's version of `hyperlight-host`.

Hyperlight currently provides no backwards compatibility guarantees for guest
binaries — the guest and host crate versions must match exactly. If you see a
`GuestBinVersionMismatch` error, rebuild the guest binary with a matching
version of `hyperlight-guest-bin`.
22 changes: 22 additions & 0 deletions src/hyperlight_common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,25 @@ pub mod func;

// cbindgen:ignore
pub mod vmem;

/// Returns the ELF section name used to embed the hyperlight-guest-bin
/// version in guest binaries as a string literal.
///
/// This is a macro (rather than a `const`) so that it can be used in contexts
/// that require string literals, such as `global_asm!` / `concat!` invocations.
#[macro_export]
macro_rules! hyperlight_guest_bin_version_section {
() => {
".hyperlight_guest_bin_version"
};
}

/// The ELF section name used to embed the hyperlight-guest-bin version in guest binaries.
///
/// Guest binaries built with `hyperlight-guest-bin` automatically include a
/// section with this name containing the crate version they were compiled
/// against. The host reads this section at load time to verify ABI
/// compatibility.
///
/// This constant is derived from the [`hyperlight_guest_bin_version_section!`] macro.
pub const HYPERLIGHT_GUEST_BIN_VERSION_SECTION: &str = hyperlight_guest_bin_version_section!();
13 changes: 13 additions & 0 deletions src/hyperlight_guest_bin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,19 @@ pub static mut GUEST_HANDLE: GuestHandle = GuestHandle::new();
pub(crate) static mut REGISTERED_GUEST_FUNCTIONS: GuestFunctionRegister =
GuestFunctionRegister::new();

// Embed the hyperlight-guest-bin crate version in a dedicated ELF section so
// the host can verify ABI compatibility at load time. The section name is
// defined by [`hyperlight_common::hyperlight_guest_bin_version_section!`].
core::arch::global_asm!(concat!(
".pushsection ",
hyperlight_common::hyperlight_guest_bin_version_section!(),
",\"\",@progbits\n",
".asciz \"",
env!("CARGO_PKG_VERSION"),
"\"\n",
".popsection",
));

/// The size of one page in the host OS, which may have some impacts
/// on how buffers for host consumption should be aligned. Code only
/// working with the guest page tables should use
Expand Down
15 changes: 15 additions & 0 deletions src/hyperlight_host/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,20 @@ pub enum HyperlightError {
#[error("The guest offset {0} is invalid.")]
GuestOffsetIsInvalid(usize),

/// The guest binary was built with a different hyperlight-guest-bin version than the host expects.
/// Hyperlight currently provides no backwards compatibility guarantees for guest binaries,
/// so the guest and host versions must match exactly. This might change in the future.
#[error(
"Guest binary was built with hyperlight-guest-bin {guest_bin_version}, \
but the host is running hyperlight {host_version}"
)]
GuestBinVersionMismatch {
/// Version of hyperlight-guest-bin the guest was compiled against.
guest_bin_version: String,
/// Version of hyperlight-host.
host_version: String,
},

/// A Host function was called by the guest but it was not registered.
#[error("HostFunction {0} was not found")]
HostFunctionNotFound(String),
Expand Down Expand Up @@ -345,6 +359,7 @@ impl HyperlightError {
| HyperlightError::Error(_)
| HyperlightError::FailedToGetValueFromParameter()
| HyperlightError::FieldIsMissingInGuestLogData(_)
| HyperlightError::GuestBinVersionMismatch { .. }
| HyperlightError::GuestError(_, _)
| HyperlightError::GuestExecutionHungOnHostFunctionCall()
| HyperlightError::GuestFunctionCallAlreadyInProgress()
Expand Down
35 changes: 35 additions & 0 deletions src/hyperlight_host/src/mem/elf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ pub(crate) struct ElfInfo {
shdrs: Vec<ResolvedSectionHeader>,
entry: u64,
relocs: Vec<Reloc>,
/// 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>,
}

#[cfg(feature = "mem_profile")]
Expand Down Expand Up @@ -120,6 +123,15 @@ impl ElfInfo {
{
log_then_return!("ELF must have at least one PT_LOAD header");
}

// Look for the hyperlight version section embedded by
// hyperlight-guest-bin.
let guest_bin_version = Self::read_section_as_string(
&elf,
bytes,
hyperlight_common::HYPERLIGHT_GUEST_BIN_VERSION_SECTION,
);

Ok(ElfInfo {
payload: bytes.to_vec(),
phdrs: elf.program_headers,
Expand All @@ -138,11 +150,34 @@ impl ElfInfo {
.collect(),
entry: elf.entry,
relocs,
guest_bin_version,
})
}

/// Read an ELF section by name and return its contents as a UTF-8 string.
/// Returns `None` if the section is missing, out of bounds, or not valid UTF-8.
fn read_section_as_string(elf: &Elf, bytes: &[u8], section_name: &str) -> Option<String> {
let sh = elf
.section_headers
.iter()
.find(|sh| elf.shdr_strtab.get_at(sh.sh_name) == Some(section_name))?;
let start = sh.sh_offset as usize;
let end = start.checked_add(sh.sh_size as usize)?;
let section_bytes = bytes.get(start..end)?;
let s = core::str::from_utf8(section_bytes).ok()?;
Some(s.trim_end_matches('\0').to_string())
}

pub(crate) fn entrypoint_va(&self) -> u64 {
self.entry
}

/// 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> {
self.guest_bin_version.as_deref()
}

pub(crate) fn get_base_va(&self) -> u64 {
#[allow(clippy::unwrap_used)] // guaranteed not to panic because of the check in new()
let min_phdr = self
Expand Down
116 changes: 116 additions & 0 deletions src/hyperlight_host/src/mem/exe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ impl ExeInfo {
ExeInfo::Elf(elf) => elf.get_va_size(),
}
}

/// Returns the hyperlight version string embedded in the guest binary, if
/// the binary was built with a version of `hyperlight-guest-bin` that
/// supports version tagging.
pub fn guest_bin_version(&self) -> Option<&str> {
match self {
ExeInfo::Elf(elf) => elf.guest_bin_version(),
}
}
// todo: this doesn't morally need to be &mut self, since we're
// copying into target, but the PE loader chooses to apply
// relocations in its owned representation of the PE contents,
Expand All @@ -103,3 +112,110 @@ impl ExeInfo {
}
}
}

#[cfg(test)]
mod tests {
use hyperlight_testing::{dummy_guest_as_string, simple_guest_as_string};

use super::ExeInfo;

/// Read the simpleguest binary and patch its version section to `"0.0.0"`.
fn simpleguest_with_patched_version() -> Vec<u8> {
let path = simple_guest_as_string().expect("failed to locate simpleguest");
let mut bytes = std::fs::read(path).expect("failed to read simpleguest");

let elf = goblin::elf::Elf::parse(&bytes).expect("failed to parse ELF");
let sh = elf
.section_headers
.iter()
.find(|sh| {
elf.shdr_strtab.get_at(sh.sh_name)
== Some(hyperlight_common::HYPERLIGHT_GUEST_BIN_VERSION_SECTION)
})
.expect("version section should exist");

let start = sh.sh_offset as usize;
let fake_version = b"0.0.0\0";
assert!(
fake_version.len() <= sh.sh_size as usize,
"fake version must fit in the existing section"
);
bytes[start..start + fake_version.len()].copy_from_slice(fake_version);
bytes
}

#[test]
fn exe_info_exposes_guest_bin_version() {
let path = simple_guest_as_string().expect("failed to locate simpleguest");
let info = ExeInfo::from_file(&path).expect("failed to load ELF");

let version = info
.guest_bin_version()
.expect("simpleguest should have a version section");
assert_eq!(version, env!("CARGO_PKG_VERSION"));
}

#[test]
fn dummyguest_has_no_version_section() {
let path = dummy_guest_as_string().expect("failed to locate dummyguest");
let info = ExeInfo::from_file(&path).expect("failed to load ELF");

assert!(
info.guest_bin_version().is_none(),
"dummyguest should not have a version section"
);
}

/// Patch the version section in-memory to simulate a version mismatch.
#[test]
fn patched_version_reports_mismatch() {
let bytes = simpleguest_with_patched_version();

let info = ExeInfo::from_buf(&bytes).expect("failed to load patched ELF");
assert_eq!(info.guest_bin_version(), Some("0.0.0"));
assert_ne!(
info.guest_bin_version().unwrap(),
env!("CARGO_PKG_VERSION"),
"patched version should differ from host version"
);
}

/// Load an unpatched simpleguest through `Snapshot::from_env` and verify
/// that it succeeds when the embedded version matches the host version.
#[test]
fn from_env_accepts_matching_version() {
let path = simple_guest_as_string().expect("failed to locate simpleguest");

let result = crate::sandbox::snapshot::Snapshot::from_env(
crate::GuestBinary::FilePath(path),
crate::sandbox::SandboxConfiguration::default(),
);

assert!(result.is_ok(), "should accept matching version");
}

/// Load a patched guest binary through `Snapshot::from_env` and verify
/// that a version mismatch produces `GuestBinVersionMismatch`.
#[test]
fn from_env_rejects_version_mismatch() {
let bytes = simpleguest_with_patched_version();

let result = crate::sandbox::snapshot::Snapshot::from_env(
crate::GuestBinary::Buffer(&bytes),
crate::sandbox::SandboxConfiguration::default(),
);

assert!(result.is_err(), "should reject mismatched version");
let err = result.err().expect("already checked is_err");
assert!(
matches!(
err,
crate::HyperlightError::GuestBinVersionMismatch {
ref guest_bin_version,
ref host_version,
} if guest_bin_version == "0.0.0" && host_version == env!("CARGO_PKG_VERSION")
),
"expected GuestBinVersionMismatch, got: {err}"
);
}
}
11 changes: 11 additions & 0 deletions src/hyperlight_host/src/sandbox/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,17 @@ impl Snapshot {
GuestBinary::Buffer(buffer) => ExeInfo::from_buf(buffer)?,
};

// Check guest/host version compatibility.
let host_version = env!("CARGO_PKG_VERSION");
if let Some(v) = exe_info.guest_bin_version()
&& v != host_version
{
return Err(crate::HyperlightError::GuestBinVersionMismatch {
guest_bin_version: v.to_string(),
host_version: host_version.to_string(),
});
}

let guest_blob_size = blob.as_ref().map(|b| b.data.len()).unwrap_or(0);
let guest_blob_mem_flags = blob.as_ref().map(|b| b.permissions);

Expand Down
Loading