From 911dab9fdc1a5ba27fcd328531940ba445c3d80d Mon Sep 17 00:00:00 2001 From: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:16:22 -0800 Subject: [PATCH] Explicitly error out on host-guest version mismatch Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> --- .../how-to-build-a-hyperlight-guest-binary.md | 12 ++ src/hyperlight_common/src/lib.rs | 22 ++++ src/hyperlight_guest_bin/src/lib.rs | 13 ++ src/hyperlight_host/src/error.rs | 15 +++ src/hyperlight_host/src/mem/elf.rs | 35 ++++++ src/hyperlight_host/src/mem/exe.rs | 116 ++++++++++++++++++ src/hyperlight_host/src/sandbox/snapshot.rs | 11 ++ 7 files changed, 224 insertions(+) diff --git a/docs/how-to-build-a-hyperlight-guest-binary.md b/docs/how-to-build-a-hyperlight-guest-binary.md index a76e43d4b..3aba267c6 100644 --- a/docs/how-to-build-a-hyperlight-guest-binary.md +++ b/docs/how-to-build-a-hyperlight-guest-binary.md @@ -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`. diff --git a/src/hyperlight_common/src/lib.rs b/src/hyperlight_common/src/lib.rs index 478aeef8b..c4c00031e 100644 --- a/src/hyperlight_common/src/lib.rs +++ b/src/hyperlight_common/src/lib.rs @@ -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!(); diff --git a/src/hyperlight_guest_bin/src/lib.rs b/src/hyperlight_guest_bin/src/lib.rs index 69ffc37af..306bb9dd8 100644 --- a/src/hyperlight_guest_bin/src/lib.rs +++ b/src/hyperlight_guest_bin/src/lib.rs @@ -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 diff --git a/src/hyperlight_host/src/error.rs b/src/hyperlight_host/src/error.rs index 18fb62c74..c813f19c6 100644 --- a/src/hyperlight_host/src/error.rs +++ b/src/hyperlight_host/src/error.rs @@ -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), @@ -345,6 +359,7 @@ impl HyperlightError { | HyperlightError::Error(_) | HyperlightError::FailedToGetValueFromParameter() | HyperlightError::FieldIsMissingInGuestLogData(_) + | HyperlightError::GuestBinVersionMismatch { .. } | HyperlightError::GuestError(_, _) | HyperlightError::GuestExecutionHungOnHostFunctionCall() | HyperlightError::GuestFunctionCallAlreadyInProgress() diff --git a/src/hyperlight_host/src/mem/elf.rs b/src/hyperlight_host/src/mem/elf.rs index 62ee0e904..c7af1ec74 100644 --- a/src/hyperlight_host/src/mem/elf.rs +++ b/src/hyperlight_host/src/mem/elf.rs @@ -45,6 +45,9 @@ pub(crate) struct ElfInfo { shdrs: Vec, entry: u64, relocs: Vec, + /// 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, } #[cfg(feature = "mem_profile")] @@ -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, @@ -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 { + 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 diff --git a/src/hyperlight_host/src/mem/exe.rs b/src/hyperlight_host/src/mem/exe.rs index 95a21ed31..f94b42287 100644 --- a/src/hyperlight_host/src/mem/exe.rs +++ b/src/hyperlight_host/src/mem/exe.rs @@ -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, @@ -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 { + 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}" + ); + } +} diff --git a/src/hyperlight_host/src/sandbox/snapshot.rs b/src/hyperlight_host/src/sandbox/snapshot.rs index 20cd046ad..7350f1cb2 100644 --- a/src/hyperlight_host/src/sandbox/snapshot.rs +++ b/src/hyperlight_host/src/sandbox/snapshot.rs @@ -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);