Skip to content

Commit 911dab9

Browse files
committed
Explicitly error out on host-guest version mismatch
Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
1 parent ac309a1 commit 911dab9

7 files changed

Lines changed: 224 additions & 0 deletions

File tree

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 a custom ELF section (`.hyperlight_guest_bin_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: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,25 @@ pub mod func;
4444

4545
// cbindgen:ignore
4646
pub mod vmem;
47+
48+
/// Returns the ELF section name used to embed the hyperlight-guest-bin
49+
/// version in guest binaries as a string literal.
50+
///
51+
/// This is a macro (rather than a `const`) so that it can be used in contexts
52+
/// that require string literals, such as `global_asm!` / `concat!` invocations.
53+
#[macro_export]
54+
macro_rules! hyperlight_guest_bin_version_section {
55+
() => {
56+
".hyperlight_guest_bin_version"
57+
};
58+
}
59+
60+
/// The ELF section name used to embed the hyperlight-guest-bin version in guest binaries.
61+
///
62+
/// Guest binaries built with `hyperlight-guest-bin` automatically include a
63+
/// section with this name containing the crate version they were compiled
64+
/// against. The host reads this section at load time to verify ABI
65+
/// compatibility.
66+
///
67+
/// This constant is derived from the [`hyperlight_guest_bin_version_section!`] macro.
68+
pub const HYPERLIGHT_GUEST_BIN_VERSION_SECTION: &str = hyperlight_guest_bin_version_section!();

src/hyperlight_guest_bin/src/lib.rs

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

122+
// Embed the hyperlight-guest-bin crate version in a dedicated ELF section so
123+
// the host can verify ABI compatibility at load time. The section name is
124+
// defined by [`hyperlight_common::hyperlight_guest_bin_version_section!`].
125+
core::arch::global_asm!(concat!(
126+
".pushsection ",
127+
hyperlight_common::hyperlight_guest_bin_version_section!(),
128+
",\"\",@progbits\n",
129+
".asciz \"",
130+
env!("CARGO_PKG_VERSION"),
131+
"\"\n",
132+
".popsection",
133+
));
134+
122135
/// The size of one page in the host OS, which may have some impacts
123136
/// on how buffers for host consumption should be aligned. Code only
124137
/// 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),
@@ -345,6 +359,7 @@ impl HyperlightError {
345359
| HyperlightError::Error(_)
346360
| HyperlightError::FailedToGetValueFromParameter()
347361
| HyperlightError::FieldIsMissingInGuestLogData(_)
362+
| HyperlightError::GuestBinVersionMismatch { .. }
348363
| HyperlightError::GuestError(_, _)
349364
| HyperlightError::GuestExecutionHungOnHostFunctionCall()
350365
| HyperlightError::GuestFunctionCallAlreadyInProgress()

src/hyperlight_host/src/mem/elf.rs

Lines changed: 35 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,15 @@ impl ElfInfo {
120123
{
121124
log_then_return!("ELF must have at least one PT_LOAD header");
122125
}
126+
127+
// Look for the hyperlight version section embedded by
128+
// hyperlight-guest-bin.
129+
let guest_bin_version = Self::read_section_as_string(
130+
&elf,
131+
bytes,
132+
hyperlight_common::HYPERLIGHT_GUEST_BIN_VERSION_SECTION,
133+
);
134+
123135
Ok(ElfInfo {
124136
payload: bytes.to_vec(),
125137
phdrs: elf.program_headers,
@@ -138,11 +150,34 @@ impl ElfInfo {
138150
.collect(),
139151
entry: elf.entry,
140152
relocs,
153+
guest_bin_version,
141154
})
142155
}
156+
157+
/// Read an ELF section by name and return its contents as a UTF-8 string.
158+
/// Returns `None` if the section is missing, out of bounds, or not valid UTF-8.
159+
fn read_section_as_string(elf: &Elf, bytes: &[u8], section_name: &str) -> Option<String> {
160+
let sh = elf
161+
.section_headers
162+
.iter()
163+
.find(|sh| elf.shdr_strtab.get_at(sh.sh_name) == Some(section_name))?;
164+
let start = sh.sh_offset as usize;
165+
let end = start.checked_add(sh.sh_size as usize)?;
166+
let section_bytes = bytes.get(start..end)?;
167+
let s = core::str::from_utf8(section_bytes).ok()?;
168+
Some(s.trim_end_matches('\0').to_string())
169+
}
170+
143171
pub(crate) fn entrypoint_va(&self) -> u64 {
144172
self.entry
145173
}
174+
175+
/// Returns the hyperlight version string embedded in the guest binary, if
176+
/// present. Used to detect version/ABI mismatches between guest and host.
177+
pub(crate) fn guest_bin_version(&self) -> Option<&str> {
178+
self.guest_bin_version.as_deref()
179+
}
180+
146181
pub(crate) fn get_base_va(&self) -> u64 {
147182
#[allow(clippy::unwrap_used)] // guaranteed not to panic because of the check in new()
148183
let min_phdr = self

src/hyperlight_host/src/mem/exe.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ impl ExeInfo {
9393
ExeInfo::Elf(elf) => elf.get_va_size(),
9494
}
9595
}
96+
97+
/// Returns the hyperlight version string embedded in the guest binary, if
98+
/// the binary was built with a version of `hyperlight-guest-bin` that
99+
/// supports version tagging.
100+
pub fn guest_bin_version(&self) -> Option<&str> {
101+
match self {
102+
ExeInfo::Elf(elf) => elf.guest_bin_version(),
103+
}
104+
}
96105
// todo: this doesn't morally need to be &mut self, since we're
97106
// copying into target, but the PE loader chooses to apply
98107
// relocations in its owned representation of the PE contents,
@@ -103,3 +112,110 @@ impl ExeInfo {
103112
}
104113
}
105114
}
115+
116+
#[cfg(test)]
117+
mod tests {
118+
use hyperlight_testing::{dummy_guest_as_string, simple_guest_as_string};
119+
120+
use super::ExeInfo;
121+
122+
/// Read the simpleguest binary and patch its version section to `"0.0.0"`.
123+
fn simpleguest_with_patched_version() -> Vec<u8> {
124+
let path = simple_guest_as_string().expect("failed to locate simpleguest");
125+
let mut bytes = std::fs::read(path).expect("failed to read simpleguest");
126+
127+
let elf = goblin::elf::Elf::parse(&bytes).expect("failed to parse ELF");
128+
let sh = elf
129+
.section_headers
130+
.iter()
131+
.find(|sh| {
132+
elf.shdr_strtab.get_at(sh.sh_name)
133+
== Some(hyperlight_common::HYPERLIGHT_GUEST_BIN_VERSION_SECTION)
134+
})
135+
.expect("version section should exist");
136+
137+
let start = sh.sh_offset as usize;
138+
let fake_version = b"0.0.0\0";
139+
assert!(
140+
fake_version.len() <= sh.sh_size as usize,
141+
"fake version must fit in the existing section"
142+
);
143+
bytes[start..start + fake_version.len()].copy_from_slice(fake_version);
144+
bytes
145+
}
146+
147+
#[test]
148+
fn exe_info_exposes_guest_bin_version() {
149+
let path = simple_guest_as_string().expect("failed to locate simpleguest");
150+
let info = ExeInfo::from_file(&path).expect("failed to load ELF");
151+
152+
let version = info
153+
.guest_bin_version()
154+
.expect("simpleguest should have a version section");
155+
assert_eq!(version, env!("CARGO_PKG_VERSION"));
156+
}
157+
158+
#[test]
159+
fn dummyguest_has_no_version_section() {
160+
let path = dummy_guest_as_string().expect("failed to locate dummyguest");
161+
let info = ExeInfo::from_file(&path).expect("failed to load ELF");
162+
163+
assert!(
164+
info.guest_bin_version().is_none(),
165+
"dummyguest should not have a version section"
166+
);
167+
}
168+
169+
/// Patch the version section in-memory to simulate a version mismatch.
170+
#[test]
171+
fn patched_version_reports_mismatch() {
172+
let bytes = simpleguest_with_patched_version();
173+
174+
let info = ExeInfo::from_buf(&bytes).expect("failed to load patched ELF");
175+
assert_eq!(info.guest_bin_version(), Some("0.0.0"));
176+
assert_ne!(
177+
info.guest_bin_version().unwrap(),
178+
env!("CARGO_PKG_VERSION"),
179+
"patched version should differ from host version"
180+
);
181+
}
182+
183+
/// Load an unpatched simpleguest through `Snapshot::from_env` and verify
184+
/// that it succeeds when the embedded version matches the host version.
185+
#[test]
186+
fn from_env_accepts_matching_version() {
187+
let path = simple_guest_as_string().expect("failed to locate simpleguest");
188+
189+
let result = crate::sandbox::snapshot::Snapshot::from_env(
190+
crate::GuestBinary::FilePath(path),
191+
crate::sandbox::SandboxConfiguration::default(),
192+
);
193+
194+
assert!(result.is_ok(), "should accept matching version");
195+
}
196+
197+
/// Load a patched guest binary through `Snapshot::from_env` and verify
198+
/// that a version mismatch produces `GuestBinVersionMismatch`.
199+
#[test]
200+
fn from_env_rejects_version_mismatch() {
201+
let bytes = simpleguest_with_patched_version();
202+
203+
let result = crate::sandbox::snapshot::Snapshot::from_env(
204+
crate::GuestBinary::Buffer(&bytes),
205+
crate::sandbox::SandboxConfiguration::default(),
206+
);
207+
208+
assert!(result.is_err(), "should reject mismatched version");
209+
let err = result.err().expect("already checked is_err");
210+
assert!(
211+
matches!(
212+
err,
213+
crate::HyperlightError::GuestBinVersionMismatch {
214+
ref guest_bin_version,
215+
ref host_version,
216+
} if guest_bin_version == "0.0.0" && host_version == env!("CARGO_PKG_VERSION")
217+
),
218+
"expected GuestBinVersionMismatch, got: {err}"
219+
);
220+
}
221+
}

src/hyperlight_host/src/sandbox/snapshot.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,17 @@ impl Snapshot {
352352
GuestBinary::Buffer(buffer) => ExeInfo::from_buf(buffer)?,
353353
};
354354

355+
// Check guest/host version compatibility.
356+
let host_version = env!("CARGO_PKG_VERSION");
357+
if let Some(v) = exe_info.guest_bin_version()
358+
&& v != host_version
359+
{
360+
return Err(crate::HyperlightError::GuestBinVersionMismatch {
361+
guest_bin_version: v.to_string(),
362+
host_version: host_version.to_string(),
363+
});
364+
}
365+
355366
let guest_blob_size = blob.as_ref().map(|b| b.data.len()).unwrap_or(0);
356367
let guest_blob_mem_flags = blob.as_ref().map(|b| b.permissions);
357368

0 commit comments

Comments
 (0)