Skip to content
Merged
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 an ELF note section (`.note.hyperlight-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`.
3 changes: 3 additions & 0 deletions src/hyperlight_common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ pub mod func;

// cbindgen:ignore
pub mod vmem;

/// ELF note types for embedding hyperlight version metadata in guest binaries.
pub mod version_note;
136 changes: 136 additions & 0 deletions src/hyperlight_common/src/version_note.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
Copyright 2025 The Hyperlight Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

//! ELF note types for embedding hyperlight version metadata in guest binaries.
//!
//! Guest binaries built with `hyperlight-guest-bin` include a `.note.hyperlight-version`
//! ELF note section containing the crate version they were compiled against.
//! The host reads this section at load time to verify ABI compatibility.

/// The ELF note section name used to embed the hyperlight-guest-bin version in guest binaries.
pub const HYPERLIGHT_VERSION_SECTION: &str = ".note.hyperlight-version";

/// The owner name used in the ELF note header for hyperlight version metadata.
pub const HYPERLIGHT_NOTE_NAME: &str = "Hyperlight";

/// The note type value used in the ELF note header for hyperlight version metadata.
pub const HYPERLIGHT_NOTE_TYPE: u32 = 1;

/// Size of the ELF note header (namesz + descsz + type, each u32).
const NOTE_HEADER_SIZE: usize = 3 * size_of::<u32>();

/// Compute the padded size of the name field for a 64-bit ELF note.
///
/// The name must be padded so that the descriptor starts at an 8-byte
/// aligned offset from the start of the note entry:
/// `(NOTE_HEADER_SIZE + padded_name) % 8 == 0`.
pub const fn padded_name_size(name_len_with_nul: usize) -> usize {
let desc_offset = NOTE_HEADER_SIZE + name_len_with_nul;
let padding = (8 - (desc_offset % 8)) % 8;
name_len_with_nul + padding
}

/// Compute the padded size of the descriptor field for a 64-bit ELF note.
///
/// The descriptor must be padded so that the next note entry starts at
/// an 8-byte aligned offset: `padded_desc % 8 == 0`.
pub const fn padded_desc_size(desc_len_with_nul: usize) -> usize {
let padding = (8 - (desc_len_with_nul % 8)) % 8;
desc_len_with_nul + padding
}

/// An ELF note structure suitable for embedding in a `#[link_section]` static.
///
/// Follows the System V gABI note format as specified in
/// <https://www.sco.com/developers/gabi/latest/ch5.pheader.html#note_section>.
///
/// `NAME_SZ` and `DESC_SZ` are the **padded** sizes of the name and descriptor
/// arrays (including null terminator and alignment padding). Use
/// [`padded_name_size`] and [`padded_desc_size`] to compute them from
/// `str.len() + 1` (the null-terminated length).
///
/// The constructor enforces these constraints with compile-time assertions.
#[repr(C, align(8))]
pub struct ElfNote<const NAME_SZ: usize, const DESC_SZ: usize> {
namesz: u32,
descsz: u32,
n_type: u32,
// NAME_SZ includes the null terminator and padding to align `desc`
// to an 8-byte boundary. Must equal `padded_name_size(namesz)`.
// Enforced at compile time by `new()`.
name: [u8; NAME_SZ],
// DESC_SZ includes the null terminator and padding so the total
// note size is a multiple of 8. Must equal `padded_desc_size(descsz)`.
// Enforced at compile time by `new()`.
desc: [u8; DESC_SZ],
}

// SAFETY: ElfNote contains only plain data (`u32` and `[u8; N]`).
// Required because ElfNote is used in a `static` (for `#[link_section]`),
// and `static` values must be `Sync`.
unsafe impl<const N: usize, const D: usize> Sync for ElfNote<N, D> {}

impl<const NAME_SZ: usize, const DESC_SZ: usize> ElfNote<NAME_SZ, DESC_SZ> {
/// Create a new ELF note from a name string, descriptor string, and type.
///
/// # Panics
///
/// Panics at compile time if `NAME_SZ` or `DESC_SZ` don't match
/// `padded_name_size(name.len() + 1)` or `padded_desc_size(desc.len() + 1)`.
pub const fn new(name: &str, desc: &str, n_type: u32) -> Self {
// NAME_SZ and DESC_SZ must match the padded sizes.
assert!(
NAME_SZ == padded_name_size(name.len() + 1),
"NAME_SZ must equal padded_name_size(name.len() + 1)"
);
assert!(
DESC_SZ == padded_desc_size(desc.len() + 1),
"DESC_SZ must equal padded_desc_size(desc.len() + 1)"
);

// desc must start at an 8-byte aligned offset from the note start.
assert!(
core::mem::offset_of!(Self, desc) % 8 == 0,
"desc is not 8-byte aligned"
);

// Total note size must be a multiple of 8 for next-entry alignment.
assert!(
size_of::<Self>() % 8 == 0,
"total note size is not 8-byte aligned"
);

Self {
namesz: (name.len() + 1) as u32,
descsz: (desc.len() + 1) as u32,
n_type,
name: pad_str_to_array(name),
desc: pad_str_to_array(desc),
}
}
}

/// Copy a string into a zero-initialised byte array at compile time.
const fn pad_str_to_array<const N: usize>(s: &str) -> [u8; N] {
let bytes = s.as_bytes();
let mut result = [0u8; N];
let mut i = 0;
while i < bytes.len() {
result[i] = bytes[i];
i += 1;
}
result
}
19 changes: 19 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,25 @@ pub static mut GUEST_HANDLE: GuestHandle = GuestHandle::new();
pub(crate) static mut REGISTERED_GUEST_FUNCTIONS: GuestFunctionRegister =
GuestFunctionRegister::new();

const VERSION_STR: &str = env!("CARGO_PKG_VERSION");

// Embed the hyperlight-guest-bin crate version as a proper ELF note so the
// host can verify ABI compatibility at load time.
#[used]
#[unsafe(link_section = ".note.hyperlight-version")]
static HYPERLIGHT_VERSION_NOTE: hyperlight_common::version_note::ElfNote<
{
hyperlight_common::version_note::padded_name_size(
hyperlight_common::version_note::HYPERLIGHT_NOTE_NAME.len() + 1,
)
},
{ hyperlight_common::version_note::padded_desc_size(VERSION_STR.len() + 1) },
> = hyperlight_common::version_note::ElfNote::new(
hyperlight_common::version_note::HYPERLIGHT_NOTE_NAME,
VERSION_STR,
hyperlight_common::version_note::HYPERLIGHT_NOTE_TYPE,
);

/// 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
34 changes: 34 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,11 @@ impl ElfInfo {
{
log_then_return!("ELF must have at least one PT_LOAD header");
}

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

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

/// Read the hyperlight version note from the ELF binary
fn read_version_note<'a>(elf: &Elf<'a>, bytes: &'a [u8]) -> Option<String> {
use hyperlight_common::version_note::{
HYPERLIGHT_NOTE_NAME, HYPERLIGHT_NOTE_TYPE, HYPERLIGHT_VERSION_SECTION,
};

let notes = elf.iter_note_sections(bytes, Some(HYPERLIGHT_VERSION_SECTION))?;
for note in notes {
let Ok(note) = note else { continue };
if note.name == HYPERLIGHT_NOTE_NAME && note.n_type == HYPERLIGHT_NOTE_TYPE {
let desc = core::str::from_utf8(note.desc).ok()?;
return Some(desc.trim_end_matches('\0').to_string());
}
}
None
}

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
Loading