Skip to content

Commit 550d328

Browse files
authored
Explicitly error out on host-guest version mismatch (#1252)
* Explicitly error on guest-host version mismatch Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> * refactor: adopt gABI-conformant note layout and rename section - Rename section to .note.hyperlight-version - Replace Aligned4 wrapper with plain [u8; N] arrays - Add padded_name_size/padded_desc_size helpers for 8-byte alignment per ELFCLASS64 gABI Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> * Add 8-byte alignment to struct Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> --------- Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
1 parent c9a1a4c commit 550d328

File tree

8 files changed

+368
-0
lines changed

8 files changed

+368
-0
lines changed

docs/how-to-build-a-hyperlight-guest-binary.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,15 @@ latest release page that contain: the `hyperlight_guest.h` header and the
3030
C API library.
3131
The `hyperlight_guest.h` header contains the corresponding APIs to register
3232
guest functions and call host functions from within the guest.
33+
34+
## Version compatibility
35+
36+
Guest binaries built with `hyperlight-guest-bin` automatically embed the crate
37+
version in an ELF note section (`.note.hyperlight-version`). When the host
38+
loads a guest binary, it checks this version and rejects the binary if it does
39+
not match the host's version of `hyperlight-host`.
40+
41+
Hyperlight currently provides no backwards compatibility guarantees for guest
42+
binaries — the guest and host crate versions must match exactly. If you see a
43+
`GuestBinVersionMismatch` error, rebuild the guest binary with a matching
44+
version of `hyperlight-guest-bin`.

src/hyperlight_common/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,6 @@ pub mod func;
4747

4848
// cbindgen:ignore
4949
pub mod vmem;
50+
51+
/// ELF note types for embedding hyperlight version metadata in guest binaries.
52+
pub mod version_note;
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
Copyright 2025 The Hyperlight Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
//! ELF note types for embedding hyperlight version metadata in guest binaries.
18+
//!
19+
//! Guest binaries built with `hyperlight-guest-bin` include a `.note.hyperlight-version`
20+
//! ELF note section containing the crate version they were compiled against.
21+
//! The host reads this section at load time to verify ABI compatibility.
22+
23+
/// The ELF note section name used to embed the hyperlight-guest-bin version in guest binaries.
24+
pub const HYPERLIGHT_VERSION_SECTION: &str = ".note.hyperlight-version";
25+
26+
/// The owner name used in the ELF note header for hyperlight version metadata.
27+
pub const HYPERLIGHT_NOTE_NAME: &str = "Hyperlight";
28+
29+
/// The note type value used in the ELF note header for hyperlight version metadata.
30+
pub const HYPERLIGHT_NOTE_TYPE: u32 = 1;
31+
32+
/// Size of the ELF note header (namesz + descsz + type, each u32).
33+
const NOTE_HEADER_SIZE: usize = 3 * size_of::<u32>();
34+
35+
/// Compute the padded size of the name field for a 64-bit ELF note.
36+
///
37+
/// The name must be padded so that the descriptor starts at an 8-byte
38+
/// aligned offset from the start of the note entry:
39+
/// `(NOTE_HEADER_SIZE + padded_name) % 8 == 0`.
40+
pub const fn padded_name_size(name_len_with_nul: usize) -> usize {
41+
let desc_offset = NOTE_HEADER_SIZE + name_len_with_nul;
42+
let padding = (8 - (desc_offset % 8)) % 8;
43+
name_len_with_nul + padding
44+
}
45+
46+
/// Compute the padded size of the descriptor field for a 64-bit ELF note.
47+
///
48+
/// The descriptor must be padded so that the next note entry starts at
49+
/// an 8-byte aligned offset: `padded_desc % 8 == 0`.
50+
pub const fn padded_desc_size(desc_len_with_nul: usize) -> usize {
51+
let padding = (8 - (desc_len_with_nul % 8)) % 8;
52+
desc_len_with_nul + padding
53+
}
54+
55+
/// An ELF note structure suitable for embedding in a `#[link_section]` static.
56+
///
57+
/// Follows the System V gABI note format as specified in
58+
/// <https://www.sco.com/developers/gabi/latest/ch5.pheader.html#note_section>.
59+
///
60+
/// `NAME_SZ` and `DESC_SZ` are the **padded** sizes of the name and descriptor
61+
/// arrays (including null terminator and alignment padding). Use
62+
/// [`padded_name_size`] and [`padded_desc_size`] to compute them from
63+
/// `str.len() + 1` (the null-terminated length).
64+
///
65+
/// The constructor enforces these constraints with compile-time assertions.
66+
#[repr(C, align(8))]
67+
pub struct ElfNote<const NAME_SZ: usize, const DESC_SZ: usize> {
68+
namesz: u32,
69+
descsz: u32,
70+
n_type: u32,
71+
// NAME_SZ includes the null terminator and padding to align `desc`
72+
// to an 8-byte boundary. Must equal `padded_name_size(namesz)`.
73+
// Enforced at compile time by `new()`.
74+
name: [u8; NAME_SZ],
75+
// DESC_SZ includes the null terminator and padding so the total
76+
// note size is a multiple of 8. Must equal `padded_desc_size(descsz)`.
77+
// Enforced at compile time by `new()`.
78+
desc: [u8; DESC_SZ],
79+
}
80+
81+
// SAFETY: ElfNote contains only plain data (`u32` and `[u8; N]`).
82+
// Required because ElfNote is used in a `static` (for `#[link_section]`),
83+
// and `static` values must be `Sync`.
84+
unsafe impl<const N: usize, const D: usize> Sync for ElfNote<N, D> {}
85+
86+
impl<const NAME_SZ: usize, const DESC_SZ: usize> ElfNote<NAME_SZ, DESC_SZ> {
87+
/// Create a new ELF note from a name string, descriptor string, and type.
88+
///
89+
/// # Panics
90+
///
91+
/// Panics at compile time if `NAME_SZ` or `DESC_SZ` don't match
92+
/// `padded_name_size(name.len() + 1)` or `padded_desc_size(desc.len() + 1)`.
93+
pub const fn new(name: &str, desc: &str, n_type: u32) -> Self {
94+
// NAME_SZ and DESC_SZ must match the padded sizes.
95+
assert!(
96+
NAME_SZ == padded_name_size(name.len() + 1),
97+
"NAME_SZ must equal padded_name_size(name.len() + 1)"
98+
);
99+
assert!(
100+
DESC_SZ == padded_desc_size(desc.len() + 1),
101+
"DESC_SZ must equal padded_desc_size(desc.len() + 1)"
102+
);
103+
104+
// desc must start at an 8-byte aligned offset from the note start.
105+
assert!(
106+
core::mem::offset_of!(Self, desc) % 8 == 0,
107+
"desc is not 8-byte aligned"
108+
);
109+
110+
// Total note size must be a multiple of 8 for next-entry alignment.
111+
assert!(
112+
size_of::<Self>() % 8 == 0,
113+
"total note size is not 8-byte aligned"
114+
);
115+
116+
Self {
117+
namesz: (name.len() + 1) as u32,
118+
descsz: (desc.len() + 1) as u32,
119+
n_type,
120+
name: pad_str_to_array(name),
121+
desc: pad_str_to_array(desc),
122+
}
123+
}
124+
}
125+
126+
/// Copy a string into a zero-initialised byte array at compile time.
127+
const fn pad_str_to_array<const N: usize>(s: &str) -> [u8; N] {
128+
let bytes = s.as_bytes();
129+
let mut result = [0u8; N];
130+
let mut i = 0;
131+
while i < bytes.len() {
132+
result[i] = bytes[i];
133+
i += 1;
134+
}
135+
result
136+
}

src/hyperlight_guest_bin/src/lib.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,25 @@ pub static mut GUEST_HANDLE: GuestHandle = GuestHandle::new();
119119
pub(crate) static mut REGISTERED_GUEST_FUNCTIONS: GuestFunctionRegister<GuestFunc> =
120120
GuestFunctionRegister::new();
121121

122+
const VERSION_STR: &str = env!("CARGO_PKG_VERSION");
123+
124+
// Embed the hyperlight-guest-bin crate version as a proper ELF note so the
125+
// host can verify ABI compatibility at load time.
126+
#[used]
127+
#[unsafe(link_section = ".note.hyperlight-version")]
128+
static HYPERLIGHT_VERSION_NOTE: hyperlight_common::version_note::ElfNote<
129+
{
130+
hyperlight_common::version_note::padded_name_size(
131+
hyperlight_common::version_note::HYPERLIGHT_NOTE_NAME.len() + 1,
132+
)
133+
},
134+
{ hyperlight_common::version_note::padded_desc_size(VERSION_STR.len() + 1) },
135+
> = hyperlight_common::version_note::ElfNote::new(
136+
hyperlight_common::version_note::HYPERLIGHT_NOTE_NAME,
137+
VERSION_STR,
138+
hyperlight_common::version_note::HYPERLIGHT_NOTE_TYPE,
139+
);
140+
122141
/// The size of one page in the host OS, which may have some impacts
123142
/// on how buffers for host consumption should be aligned. Code only
124143
/// working with the guest page tables should use

src/hyperlight_host/src/error.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,20 @@ pub enum HyperlightError {
108108
#[error("The guest offset {0} is invalid.")]
109109
GuestOffsetIsInvalid(usize),
110110

111+
/// The guest binary was built with a different hyperlight-guest-bin version than the host expects.
112+
/// Hyperlight currently provides no backwards compatibility guarantees for guest binaries,
113+
/// so the guest and host versions must match exactly. This might change in the future.
114+
#[error(
115+
"Guest binary was built with hyperlight-guest-bin {guest_bin_version}, \
116+
but the host is running hyperlight {host_version}"
117+
)]
118+
GuestBinVersionMismatch {
119+
/// Version of hyperlight-guest-bin the guest was compiled against.
120+
guest_bin_version: String,
121+
/// Version of hyperlight-host.
122+
host_version: String,
123+
},
124+
111125
/// A Host function was called by the guest but it was not registered.
112126
#[error("HostFunction {0} was not found")]
113127
HostFunctionNotFound(String),
@@ -350,6 +364,7 @@ impl HyperlightError {
350364
| HyperlightError::Error(_)
351365
| HyperlightError::FailedToGetValueFromParameter()
352366
| HyperlightError::FieldIsMissingInGuestLogData(_)
367+
| HyperlightError::GuestBinVersionMismatch { .. }
353368
| HyperlightError::GuestError(_, _)
354369
| HyperlightError::GuestExecutionHungOnHostFunctionCall()
355370
| HyperlightError::GuestFunctionCallAlreadyInProgress()

src/hyperlight_host/src/mem/elf.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ pub(crate) struct ElfInfo {
4545
shdrs: Vec<ResolvedSectionHeader>,
4646
entry: u64,
4747
relocs: Vec<Reloc>,
48+
/// The hyperlight version string embedded by `hyperlight-guest-bin`, if
49+
/// present. Used to detect version/ABI mismatches between guest and host.
50+
guest_bin_version: Option<String>,
4851
}
4952

5053
#[cfg(feature = "mem_profile")]
@@ -120,6 +123,11 @@ impl ElfInfo {
120123
{
121124
log_then_return!("ELF must have at least one PT_LOAD header");
122125
}
126+
127+
// Look for the hyperlight version note embedded by
128+
// hyperlight-guest-bin.
129+
let guest_bin_version = Self::read_version_note(&elf, bytes);
130+
123131
Ok(ElfInfo {
124132
payload: bytes.to_vec(),
125133
phdrs: elf.program_headers,
@@ -138,11 +146,37 @@ impl ElfInfo {
138146
.collect(),
139147
entry: elf.entry,
140148
relocs,
149+
guest_bin_version,
141150
})
142151
}
152+
153+
/// Read the hyperlight version note from the ELF binary
154+
fn read_version_note<'a>(elf: &Elf<'a>, bytes: &'a [u8]) -> Option<String> {
155+
use hyperlight_common::version_note::{
156+
HYPERLIGHT_NOTE_NAME, HYPERLIGHT_NOTE_TYPE, HYPERLIGHT_VERSION_SECTION,
157+
};
158+
159+
let notes = elf.iter_note_sections(bytes, Some(HYPERLIGHT_VERSION_SECTION))?;
160+
for note in notes {
161+
let Ok(note) = note else { continue };
162+
if note.name == HYPERLIGHT_NOTE_NAME && note.n_type == HYPERLIGHT_NOTE_TYPE {
163+
let desc = core::str::from_utf8(note.desc).ok()?;
164+
return Some(desc.trim_end_matches('\0').to_string());
165+
}
166+
}
167+
None
168+
}
169+
143170
pub(crate) fn entrypoint_va(&self) -> u64 {
144171
self.entry
145172
}
173+
174+
/// Returns the hyperlight version string embedded in the guest binary, if
175+
/// present. Used to detect version/ABI mismatches between guest and host.
176+
pub(crate) fn guest_bin_version(&self) -> Option<&str> {
177+
self.guest_bin_version.as_deref()
178+
}
179+
146180
pub(crate) fn get_base_va(&self) -> u64 {
147181
#[allow(clippy::unwrap_used)] // guaranteed not to panic because of the check in new()
148182
let min_phdr = self

0 commit comments

Comments
 (0)