From 16d12716b1044c2af285558863313ada1c87630c Mon Sep 17 00:00:00 2001 From: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:39:24 -0700 Subject: [PATCH] Reseed libc rand on restore() Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> --- CHANGELOG.md | 1 + src/hyperlight_common/src/layout.rs | 1 + src/hyperlight_guest/src/layout.rs | 4 + .../src/guest_function/call.rs | 4 + src/hyperlight_guest_bin/src/lib.rs | 17 +++ src/hyperlight_host/src/mem/mgr.rs | 9 ++ .../src/sandbox/initialized_multi_use.rs | 7 + .../src/sandbox/snapshot/file_tests.rs | 124 +++++++++++++++++- src/tests/c_guests/c_simpleguest/main.c | 8 ++ 9 files changed, 174 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 516928815..32cd4fec7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/hyperlight_common/src/layout.rs b/src/hyperlight_common/src/layout.rs index bf25a2e0c..d60ea2a73 100644 --- a/src/hyperlight_common/src/layout.rs +++ b/src/hyperlight_common/src/layout.rs @@ -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 { diff --git a/src/hyperlight_guest/src/layout.rs b/src/hyperlight_guest/src/layout.rs index 6d132ae7c..2c2923ef6 100644 --- a/src/hyperlight_guest/src/layout.rs +++ b/src/hyperlight_guest/src/layout.rs @@ -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}; diff --git a/src/hyperlight_guest_bin/src/guest_function/call.rs b/src/hyperlight_guest_bin/src/guest_function/call.rs index 82874c659..c94a74de2 100644 --- a/src/hyperlight_guest_bin/src/guest_function/call.rs +++ b/src/hyperlight_guest_bin/src/guest_function/call.rs @@ -104,6 +104,10 @@ pub(crate) fn internal_dispatch_function() { .try_pop_shared_input_data_into::() .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 { diff --git a/src/hyperlight_guest_bin/src/lib.rs b/src/hyperlight_guest_bin/src/lib.rs index 5df92f647..231151925 100644 --- a/src/hyperlight_guest_bin/src/lib.rs +++ b/src/hyperlight_guest_bin/src/lib.rs @@ -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() }; + 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 diff --git a/src/hyperlight_host/src/mem/mgr.rs b/src/hyperlight_host/src/mem/mgr.rs index 8e4558770..7a67d29a8 100644 --- a/src/hyperlight_host/src/mem/mgr.rs +++ b/src/hyperlight_host/src/mem/mgr.rs @@ -530,6 +530,15 @@ impl SandboxMemoryManager { self.scratch_mem.write::(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(); diff --git a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs index f455dffa5..f02236aa7 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -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. @@ -573,6 +577,9 @@ impl MultiUseSandbox { .map_err(HyperlightVmError::UnmapRegion)?; } + self.mem_mgr + .request_libc_rng_reseed(rand::random::())?; + // The restored snapshot is now our most current snapshot self.snapshot = Some(snapshot.clone()); diff --git a/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs b/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs index 4e0604c86..bad41758a 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs @@ -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}; @@ -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::( + scratch_size - hyperlight_common::layout::SCRATCH_TOP_LIBC_RNG_SEED_OFFSET as usize, + ) + .unwrap() +} + fn create_snapshot() -> Arc { let mut sbox = create_test_sandbox(); sbox.snapshot().unwrap() @@ -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); } @@ -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::("NextRandom", ()).unwrap(); + assert_eq!(libc_rng_reseed_request(&sandbox), 0); + sandbox.call::("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] diff --git a/src/tests/c_guests/c_simpleguest/main.c b/src/tests/c_guests/c_simpleguest/main.c index 91da1973c..91b4b974c 100644 --- a/src/tests/c_guests/c_simpleguest/main.c +++ b/src/tests/c_guests/c_simpleguest/main.c @@ -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); @@ -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) @@ -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