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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
* Fix symbol resolution in guest core dumps for sandboxes created from snapshots by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/1618
* Reject malformed OCI snapshot metadata and non-regular artifact files during load.
* Reset XCR0 during x86 snapshot restore.
* Reseed guest libc `rand()` and `random()` after restoring a snapshot to avoid multiple sandboxes sharing PRNG state.

## [v0.16.0] - 2026-06-26

Expand Down
1 change: 1 addition & 0 deletions src/hyperlight_common/src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub const SCRATCH_TOP_SIZE_OFFSET: u64 = 0x08;
pub const SCRATCH_TOP_ALLOCATOR_OFFSET: u64 = 0x10;
pub const SCRATCH_TOP_SNAPSHOT_PT_GPA_BASE_OFFSET: u64 = 0x18;
pub const SCRATCH_TOP_SNAPSHOT_GENERATION_OFFSET: u64 = 0x20;
pub const SCRATCH_TOP_LIBC_RNG_SEED_OFFSET: u64 = 0x28;
pub const SCRATCH_TOP_EXN_STACK_OFFSET: u64 = 0x30;

pub fn scratch_base_gpa(size: usize) -> u64 {
Expand Down
4 changes: 4 additions & 0 deletions src/hyperlight_guest/src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,8 @@ pub fn snapshot_generation_gva() -> *mut u64 {
use hyperlight_common::layout::{SCRATCH_TOP_GVA, SCRATCH_TOP_SNAPSHOT_GENERATION_OFFSET};
(SCRATCH_TOP_GVA as u64 - SCRATCH_TOP_SNAPSHOT_GENERATION_OFFSET + 1) as *mut u64
}
pub fn libc_rng_seed_gva() -> *mut u64 {
use hyperlight_common::layout::{SCRATCH_TOP_GVA, SCRATCH_TOP_LIBC_RNG_SEED_OFFSET};
(SCRATCH_TOP_GVA as u64 - SCRATCH_TOP_LIBC_RNG_SEED_OFFSET + 1) as *mut u64
}
pub use arch::{scratch_base_gpa, scratch_base_gva};
4 changes: 4 additions & 0 deletions src/hyperlight_guest_bin/src/guest_function/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ pub(crate) fn internal_dispatch_function() {
.try_pop_shared_input_data_into::<FunctionCall>()
.expect("Function call deserialization failed");

// Reseed the libc PRNG if requested by the host.
#[cfg(feature = "libc")]
crate::refresh_libc_rng();

let res = call_guest_function(function_call);

match res {
Expand Down
17 changes: 17 additions & 0 deletions src/hyperlight_guest_bin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,23 @@ unsafe extern "C" {
fn srand(seed: u32);
}

#[cfg(feature = "libc")]
pub(crate) fn refresh_libc_rng() {
let seed_ptr = hyperlight_guest::layout::libc_rng_seed_gva();
// SAFETY: The host maps this aligned u64 scratch slot for the guest's
// lifetime and writes it only while the guest is stopped.
let request = unsafe { seed_ptr.read_volatile() };
Comment thread
jprendes marked this conversation as resolved.
if request >> 32 != 0 {
// SAFETY: The scratch slot has the validity and exclusivity described
// above. The libc feature provides srand with a u32 seed.
unsafe {
srand(request as u32);
// clear request u32 and zero u32 seed
seed_ptr.write_volatile(0u64);
}
}
}

#[tracing::instrument(skip_all, parent = tracing::Span::current(), level= "Trace")]
extern "C" fn hyperlight_main_default() {
// no-op
Expand Down
9 changes: 9 additions & 0 deletions src/hyperlight_host/src/mem/mgr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,15 @@ impl SandboxMemoryManager<HostSharedMemory> {
self.scratch_mem.write::<u64>(base_offset, value)
}

pub(crate) fn request_libc_rng_reseed(&mut self, seed: u32) -> Result<()> {
// Zero means no request. The upper half marks a pending request, and
// the lower half contains the complete u32 seed.
self.update_scratch_bookkeeping_item(
hyperlight_common::layout::SCRATCH_TOP_LIBC_RNG_SEED_OFFSET,
(1_u64 << 32) | u64::from(seed),
)
}

fn update_scratch_bookkeeping(&mut self) -> Result<()> {
use hyperlight_common::layout::*;
let scratch_size = self.scratch_mem.mem_size();
Expand Down
7 changes: 7 additions & 0 deletions src/hyperlight_host/src/sandbox/initialized_multi_use.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,10 @@ impl MultiUseSandbox {
vm.initialise(peb_addr, seed, &mut hshm, &host_funcs, None)
.map_err(crate::hypervisor::hyperlight_vm::HyperlightVmError::Initialize)?;

if matches!(snapshot.next_action(), super::snapshot::NextAction::Call(_)) {
hshm.request_libc_rng_reseed(seed as u32)?;
}

// If the snapshot was taken from an already-initialized guest
// (NextAction::Call), apply the captured special registers so
// the guest resumes in the correct CPU state.
Expand Down Expand Up @@ -573,6 +577,9 @@ impl MultiUseSandbox {
.map_err(HyperlightVmError::UnmapRegion)?;
}

self.mem_mgr
.request_libc_rng_reseed(rand::random::<u32>())?;

// The restored snapshot is now our most current snapshot
self.snapshot = Some(snapshot.clone());

Expand Down
124 changes: 123 additions & 1 deletion src/hyperlight_host/src/sandbox/snapshot/file_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ limitations under the License.

use std::sync::Arc;

use hyperlight_testing::simple_guest_as_pathbuf;
use hyperlight_testing::{c_simple_guest_as_pathbuf, simple_guest_as_pathbuf};
use serde_json::Value;
use sha2::{Digest as _, Sha256};

use crate::func::Registerable;
use crate::mem::layout::SandboxMemoryLayout;
use crate::mem::shared_mem::SharedMemory as _;
use crate::sandbox::snapshot::{OciDigest, OciReference, OciTag, Snapshot};
use crate::{GuestBinary, HostFunctions, MultiUseSandbox, UninitializedSandbox};

Expand All @@ -37,6 +38,33 @@ fn create_test_sandbox() -> MultiUseSandbox {
.unwrap()
}

fn create_c_test_sandbox() -> MultiUseSandbox {
let path = c_simple_guest_as_pathbuf();
UninitializedSandbox::new(GuestBinary::FilePath(path), None)
.unwrap()
.evolve()
.unwrap()
}

fn random_sequence(sandbox: &mut MultiUseSandbox) -> [i32; 4] {
std::array::from_fn(|_| sandbox.call("NextRandom", ()).unwrap())
}

fn random_long_sequence(sandbox: &mut MultiUseSandbox) -> [i64; 4] {
std::array::from_fn(|_| sandbox.call("NextRandomLong", ()).unwrap())
}

fn libc_rng_reseed_request(sandbox: &MultiUseSandbox) -> u64 {
let scratch_size = sandbox.mem_mgr.scratch_mem.mem_size();
sandbox
.mem_mgr
.scratch_mem
.read::<u64>(
scratch_size - hyperlight_common::layout::SCRATCH_TOP_LIBC_RNG_SEED_OFFSET as usize,
)
.unwrap()
}

fn create_snapshot() -> Arc<Snapshot> {
let mut sbox = create_test_sandbox();
sbox.snapshot().unwrap()
Expand Down Expand Up @@ -111,6 +139,7 @@ fn from_snapshot_in_memory_pre_init() {
.unwrap();
let mut sbox =
MultiUseSandbox::from_snapshot(Arc::new(snap), HostFunctions::default(), None).unwrap();
assert_eq!(libc_rng_reseed_request(&sbox), 0);
let result: i32 = sbox.call("GetStatic", ()).unwrap();
assert_eq!(result, 0);
}
Expand Down Expand Up @@ -3175,6 +3204,99 @@ fn from_snapshot_silently_ignores_layout_overrides() {
assert_eq!(new_snap.layout().get_scratch_size(), original_scratch);
}

#[test]
fn random_guest_libc_rng_reseeds_from_snapshot() {
let mut sandbox = create_c_test_sandbox();
let snapshot = sandbox.snapshot().unwrap();

let mut first =
MultiUseSandbox::from_snapshot(snapshot.clone(), HostFunctions::default(), None).unwrap();
let mut second =
MultiUseSandbox::from_snapshot(snapshot, HostFunctions::default(), None).unwrap();

assert_ne!(random_sequence(&mut first), random_sequence(&mut second));
}

#[test]
fn guest_libc_rng_random_reseeds_from_snapshot() {
let mut sandbox = create_c_test_sandbox();
let snapshot = sandbox.snapshot().unwrap();

let mut first =
MultiUseSandbox::from_snapshot(snapshot.clone(), HostFunctions::default(), None).unwrap();
let mut second =
MultiUseSandbox::from_snapshot(snapshot, HostFunctions::default(), None).unwrap();

assert_ne!(
random_long_sequence(&mut first),
random_long_sequence(&mut second)
);
}

#[test]
fn fresh_random_guest_libc_rng_sequences_differ() {
let mut first = create_c_test_sandbox();
let mut second = create_c_test_sandbox();

assert_ne!(random_sequence(&mut first), random_sequence(&mut second));
}

#[test]
fn libc_rng_reseed_request_encodes_all_u32_seeds() {
let mut sandbox = create_c_test_sandbox();
for seed in [0, u32::MAX] {
sandbox.mem_mgr.request_libc_rng_reseed(seed).unwrap();
assert_eq!(
libc_rng_reseed_request(&sandbox),
(1_u64 << 32) | u64::from(seed)
);
}
}

#[test]
fn guest_consumes_libc_rng_reseed_request_once() {
let mut sandbox = create_c_test_sandbox();
let snapshot = sandbox.snapshot().unwrap();
sandbox.restore(snapshot).unwrap();
assert_ne!(libc_rng_reseed_request(&sandbox), 0);

sandbox.call::<i32>("NextRandom", ()).unwrap();
assert_eq!(libc_rng_reseed_request(&sandbox), 0);
sandbox.call::<i32>("NextRandom", ()).unwrap();
assert_eq!(libc_rng_reseed_request(&sandbox), 0);
}

#[test]
fn guest_libc_rng_reseeds_on_every_restore() {
let mut sandbox = create_c_test_sandbox();
let snapshot = sandbox.snapshot().unwrap();
sandbox.restore(snapshot.clone()).unwrap();
let first = random_sequence(&mut sandbox);
sandbox.restore(snapshot).unwrap();
let second = random_sequence(&mut sandbox);

assert_ne!(first, second);
}

#[test]
fn persisted_guest_libc_rng_snapshot_reseeds_each_instance() {
let mut sandbox = create_c_test_sandbox();
let snapshot = sandbox.snapshot().unwrap();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("snapshot");
snapshot
.save(&path, &OciTag::new("latest").unwrap())
.unwrap();

let loaded = Arc::new(Snapshot::checked_load(&path, OciTag::new("latest").unwrap()).unwrap());
let mut first =
MultiUseSandbox::from_snapshot(loaded.clone(), HostFunctions::default(), None).unwrap();
let mut second =
MultiUseSandbox::from_snapshot(loaded, HostFunctions::default(), None).unwrap();

assert_ne!(random_sequence(&mut first), random_sequence(&mut second));
}

/// `from_snapshot` honors `guest_core_dump=true` so that
/// `generate_crashdump_to_dir` writes a file.
#[test]
Expand Down
8 changes: 8 additions & 0 deletions src/tests/c_guests/c_simpleguest/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ float echo_float(float f) { return f; }

double echo_double(double d) { return d; }

int next_random(void) { return rand(); }

long next_random_long(void) { return random(); }

hl_Vec *set_byte_array_to_zero(const hl_FunctionCall* params) {
hl_Vec input = params->parameters[0].value.VecBytes;
uint8_t *x = malloc(input.len);
Expand Down Expand Up @@ -359,6 +363,8 @@ HYPERLIGHT_WRAP_FUNCTION(print_ten_args, Int, 10, String, Int, Long, String, Str
HYPERLIGHT_WRAP_FUNCTION(print_eleven_args, Int, 11, String, Int, Long, String, String, Bool, Bool, UInt, ULong, Int, Float)
HYPERLIGHT_WRAP_FUNCTION(echo_float, Float, 1, Float)
HYPERLIGHT_WRAP_FUNCTION(echo_double, Double, 1, Double)
HYPERLIGHT_WRAP_FUNCTION(next_random, Int, 0)
HYPERLIGHT_WRAP_FUNCTION(next_random_long, Long, 0)
HYPERLIGHT_WRAP_FUNCTION(set_static, Int, 0)
// HYPERLIGHT_WRAP_FUNCTION(get_size_prefixed_buffer, Int, 1, VecBytes) is not valid for functions that return VecBytes
HYPERLIGHT_WRAP_FUNCTION(guest_abort_with_msg, Int, 2, Int, String)
Expand Down Expand Up @@ -398,6 +404,8 @@ void hyperlight_main(void)
HYPERLIGHT_REGISTER_FUNCTION("PrintElevenArgs", print_eleven_args);
HYPERLIGHT_REGISTER_FUNCTION("EchoFloat", echo_float);
HYPERLIGHT_REGISTER_FUNCTION("EchoDouble", echo_double);
HYPERLIGHT_REGISTER_FUNCTION("NextRandom", next_random);
HYPERLIGHT_REGISTER_FUNCTION("NextRandomLong", next_random_long);
HYPERLIGHT_REGISTER_FUNCTION("SetStatic", set_static);
// HYPERLIGHT_REGISTER_FUNCTION macro does not work for functions that return VecBytes,
// so we use hl_register_function_definition directly
Expand Down
Loading