diff --git a/CHANGELOG.md b/CHANGELOG.md index 516928815..792d5565c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Prerelease] - Unreleased ### Added +* Add `LibcRngReseedPolicy` and + `SandboxConfiguration::set_libc_rng_reseed_policy` for configuring the guest + libc PRNG. ### Changed * **Breaking:** Guest MSR state is now saved and restored across snapshots. diff --git a/docs/snapshot-oci-format.md b/docs/snapshot-oci-format.md index 971b3c868..bb7ae2717 100644 --- a/docs/snapshot-oci-format.md +++ b/docs/snapshot-oci-format.md @@ -31,15 +31,20 @@ Three blob kinds per tag: * **manifest** (`application/vnd.oci.image.manifest.v1+json`). Tiny JSON pointer record selected via `index.json`. References one config and one layer by digest. -* **config** (`application/vnd.hyperlight.snapshot.config.v1+json`). The +* **config** (`application/vnd.hyperlight.snapshot.config.v2+json`). The snapshot descriptor: arch, hypervisor, CPU vendor, ABI version, - resume address and captured registers, memory layout, registered - host functions, snapshot generation counter. Loaded eagerly and - fully parsed. + resume address and captured registers, libc PRNG reseed policy, + memory layout, registered host functions, snapshot generation counter. + Loaded eagerly and fully parsed. * **layer / memory** (`application/vnd.hyperlight.snapshot.memory.v1`). The raw guest memory image, exactly `memory_size` bytes. mmap'd on restore. +The libc PRNG setting applies only to `rand()` and `random()` from +Hyperlight's bundled libc. Rust and custom RNG implementations must manage +their own state across snapshot restores. This setting is not a source of +cryptographic entropy. + Blob filenames are the sha256 of the blob bytes, so identical blobs across tags are stored once. diff --git a/docs/snapshot-versioning.md b/docs/snapshot-versioning.md index f855c1576..8d171ba01 100644 --- a/docs/snapshot-versioning.md +++ b/docs/snapshot-versioning.md @@ -23,8 +23,8 @@ A snapshot carries three independently evolvable version markers: `MT_SNAPSHOT_CURRENT`. This is the on-wire format of the snapshot blob: framing, section ordering, alignment, dirty/zero-page elision, anything about how the bytes are packed inside the OCI layer. -* **Config schema**, `MT_CONFIG_V1` - (`application/vnd.hyperlight.snapshot.config.v1+json`), aliased as +* **Config schema**, `MT_CONFIG_V2` + (`application/vnd.hyperlight.snapshot.config.v2+json`), aliased as `MT_CONFIG_CURRENT`. This is the JSON shape of the config blob: field names, types, required vs optional, the descriptors the loader needs in order to reconstruct the sandbox (memory sizes, buffer 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..6729f5e56 100644 --- a/src/hyperlight_guest_bin/src/guest_function/call.rs +++ b/src/hyperlight_guest_bin/src/guest_function/call.rs @@ -86,6 +86,10 @@ pub(crate) fn call_guest_function(function_call: FunctionCall) -> Result } pub(crate) fn internal_dispatch_function() { + // Reseed the libc PRNG if requested by the host. + #[cfg(feature = "libc")] + crate::refresh_libc_rng(); + // Read the current TSC to report it to the host with the spans/events // This helps calculating the timestamps relative to the guest call #[cfg(all(feature = "trace_guest", target_arch = "x86_64"))] diff --git a/src/hyperlight_guest_bin/src/lib.rs b/src/hyperlight_guest_bin/src/lib.rs index 5df92f647..1f4564b73 100644 --- a/src/hyperlight_guest_bin/src/lib.rs +++ b/src/hyperlight_guest_bin/src/lib.rs @@ -210,6 +210,18 @@ 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(); + let request = unsafe { seed_ptr.read_volatile() }; + if request >> 32 != 0 { + unsafe { + seed_ptr.write_volatile(0); + srand(request as u32); + } + } +} + #[tracing::instrument(skip_all, parent = tracing::Span::current(), level= "Trace")] extern "C" fn hyperlight_main_default() { // no-op @@ -254,8 +266,7 @@ pub(crate) extern "C" fn generic_init( #[cfg(feature = "libc")] unsafe { - let srand_seed = (((peb_address << 8) ^ (_seed >> 4)) >> 32) as u32; - srand(srand_seed); + srand(_seed as u32); } unsafe { diff --git a/src/hyperlight_host/src/mem/mgr.rs b/src/hyperlight_host/src/mem/mgr.rs index 8e4558770..709259e57 100644 --- a/src/hyperlight_host/src/mem/mgr.rs +++ b/src/hyperlight_host/src/mem/mgr.rs @@ -304,6 +304,7 @@ where mapped_regions: Vec, root_pt_gpas: &[u64], rsp_gva: u64, + libc_rng_reseed_policy: crate::sandbox::LibcRngReseedPolicy, sregs: CommonSpecialRegisters, #[cfg(target_arch = "x86_64")] msrs: Vec, next_action: NextAction, @@ -318,6 +319,7 @@ where mapped_regions, root_pt_gpas, rsp_gva, + libc_rng_reseed_policy, sregs, #[cfg(target_arch = "x86_64")] msrs, @@ -530,6 +532,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(); @@ -554,6 +565,7 @@ impl SandboxMemoryManager { SCRATCH_TOP_SNAPSHOT_GENERATION_OFFSET, self.snapshot_count, )?; + self.update_scratch_bookkeeping_item(SCRATCH_TOP_LIBC_RNG_SEED_OFFSET, 0)?; // Initialise the guest input and output data buffers in // scratch memory. TODO: remove the need for this. diff --git a/src/hyperlight_host/src/sandbox/config.rs b/src/hyperlight_host/src/sandbox/config.rs index 442da8415..c6c9ee7f3 100644 --- a/src/hyperlight_host/src/sandbox/config.rs +++ b/src/hyperlight_host/src/sandbox/config.rs @@ -41,6 +41,34 @@ pub enum GuestMsrError { }, } +/// Controls how Hyperlight's guest libc PRNG is seeded during initialization +/// and reseeded after every snapshot restore. +/// +/// This controls `rand()` and `random()` when the guest uses Hyperlight's +/// bundled libc and guest runtime. It has no effect on Rust `rand`, +/// `getrandom`, custom PRNGs, or hardware RNG instructions. It does not +/// provide cryptographic entropy. Other runtimes must manage their own RNG +/// state across snapshot restores. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +#[repr(u8)] +pub enum LibcRngReseedPolicy { + /// Generate a new seed on the host during initialization and after every + /// snapshot restore. + #[default] + Random, + /// Use the supplied seed. + Fixed(u32), +} + +impl LibcRngReseedPolicy { + pub(crate) fn resolve(self) -> u32 { + match self { + Self::Random => rand::random(), + Self::Fixed(seed) => seed, + } + } +} + /// The complete set of configuration needed to create a Sandbox #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[repr(C)] @@ -92,6 +120,8 @@ pub struct SandboxConfiguration { /// Number of valid entries in `guest_msrs`. #[cfg(target_arch = "x86_64")] guest_msrs_count: usize, + /// Guest libc PRNG reseeding behavior. + libc_rng_reseed_policy: LibcRngReseedPolicy, } impl SandboxConfiguration { @@ -137,6 +167,7 @@ impl SandboxConfiguration { scratch_size, interrupt_retry_delay, interrupt_vcpu_sigrtmin_offset, + libc_rng_reseed_policy: LibcRngReseedPolicy::default(), #[cfg(gdb)] guest_debug_info, #[cfg(crashdump)] @@ -299,6 +330,19 @@ impl SandboxConfiguration { self.scratch_size = scratch_size; } + /// Set the guest libc PRNG reseed policy. + /// + /// Snapshots retain this setting. Restoring a snapshot uses its retained + /// setting, not the destination sandbox configuration. The default is + /// [`LibcRngReseedPolicy::Random`]. See [`LibcRngReseedPolicy`] for its scope. + pub fn set_libc_rng_reseed_policy(&mut self, policy: LibcRngReseedPolicy) { + self.libc_rng_reseed_policy = policy; + } + + pub(crate) fn get_libc_rng_reseed_policy(&self) -> LibcRngReseedPolicy { + self.libc_rng_reseed_policy + } + #[cfg(crashdump)] #[instrument(skip_all, parent = Span::current(), level= "Trace")] pub(crate) fn get_guest_core_dump(&self) -> bool { @@ -347,7 +391,22 @@ impl Default for SandboxConfiguration { mod tests { #[cfg(target_arch = "x86_64")] use super::GuestMsrError; - use super::SandboxConfiguration; + use super::{LibcRngReseedPolicy, SandboxConfiguration}; + + #[test] + fn libc_rng_configuration_defaults_and_setters() { + let mut config = SandboxConfiguration::default(); + assert_eq!( + config.get_libc_rng_reseed_policy(), + LibcRngReseedPolicy::Random + ); + + config.set_libc_rng_reseed_policy(LibcRngReseedPolicy::Fixed(0)); + assert_eq!( + config.get_libc_rng_reseed_policy(), + LibcRngReseedPolicy::Fixed(0) + ); + } #[test] #[cfg(target_arch = "x86_64")] diff --git a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs index f455dffa5..b56512def 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -93,6 +93,7 @@ pub struct MultiUseSandbox { /// Given (snapshot_mem, scratch_mem, cr3), returns a list of root GPAs. /// If not set, only CR3 is used as the single root. pt_root_finder: Option, + libc_rng_reseed_policy: crate::sandbox::LibcRngReseedPolicy, } /// Callback for discovering page table roots from guest memory. @@ -118,6 +119,7 @@ impl MultiUseSandbox { host_funcs: Arc>, mgr: SandboxMemoryManager, vm: HyperlightVm, + libc_rng_reseed_policy: crate::sandbox::LibcRngReseedPolicy, ) -> MultiUseSandbox { Self { poisoned: false, @@ -126,6 +128,7 @@ impl MultiUseSandbox { vm, snapshot: None, pt_root_finder: None, + libc_rng_reseed_policy, } } @@ -205,8 +208,6 @@ impl MultiUseSandbox { host_funcs: crate::HostFunctions, config: Option, ) -> Result { - use rand::RngExt; - use crate::mem::ptr::RawPtr; use crate::sandbox::uninitialized_evolve::set_up_hypervisor_partition; @@ -270,16 +271,18 @@ impl MultiUseSandbox { load_info, )?; - let seed = { - let mut rng = rand::rng(); - rng.random::() - }; + let libc_rng_reseed_policy = snapshot.libc_rng_reseed_policy(); + let seed = libc_rng_reseed_policy.resolve(); let peb_addr = RawPtr::from(u64::try_from(hshm.layout.peb_address())?); // noop for NextAction::Call - vm.initialise(peb_addr, seed, &mut hshm, &host_funcs, None) + vm.initialise(peb_addr, u64::from(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)?; + } + // 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. @@ -303,7 +306,7 @@ impl MultiUseSandbox { })?; } - let sbox = MultiUseSandbox::from_uninit(host_funcs, hshm, vm); + let sbox = MultiUseSandbox::from_uninit(host_funcs, hshm, vm, libc_rng_reseed_policy); Ok(sbox) } @@ -392,6 +395,7 @@ impl MultiUseSandbox { mapped_regions_vec, &root_pt_gpas, stack_top_gpa, + self.libc_rng_reseed_policy, sregs, #[cfg(target_arch = "x86_64")] msrs, @@ -425,6 +429,10 @@ impl MultiUseSandbox { /// declare every MSR the snapshot saved, or the restore poisons with an MSR /// mismatch. /// + /// Restore replaces the sandbox's configured + /// [`LibcRngReseedPolicy`](crate::sandbox::LibcRngReseedPolicy) with the + /// snapshot's policy. + /// /// ## Poison State Recovery /// /// This method automatically clears any poison state when successful. This is safe because: @@ -528,6 +536,8 @@ impl MultiUseSandbox { } let (gsnapshot, gscratch) = self.mem_mgr.restore_snapshot(&snapshot)?; + self.mem_mgr + .request_libc_rng_reseed(snapshot.libc_rng_reseed_policy().resolve())?; if let Some(gsnapshot) = gsnapshot { self.vm .update_snapshot_mapping(gsnapshot) @@ -574,6 +584,7 @@ impl MultiUseSandbox { } // The restored snapshot is now our most current snapshot + self.libc_rng_reseed_policy = snapshot.libc_rng_reseed_policy(); self.snapshot = Some(snapshot.clone()); // Clear poison state when successfully restoring from snapshot. diff --git a/src/hyperlight_host/src/sandbox/mod.rs b/src/hyperlight_host/src/sandbox/mod.rs index 822b1e388..e989a5cc7 100644 --- a/src/hyperlight_host/src/sandbox/mod.rs +++ b/src/hyperlight_host/src/sandbox/mod.rs @@ -43,8 +43,8 @@ pub(crate) mod trace; /// Trait used by the macros to paper over the differences between hyperlight and hyperlight-wasm pub use callable::Callable; -/// Re-export for `SandboxConfiguration` type -pub use config::SandboxConfiguration; +/// Re-export for sandbox configuration types +pub use config::{LibcRngReseedPolicy, SandboxConfiguration}; /// Re-export for the `MultiUseSandbox` type pub use initialized_multi_use::{MultiUseSandbox, PtRootFinder}; /// Re-export for `GuestBinary` type diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs index 01c2f501b..d4dd455e9 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs @@ -157,7 +157,7 @@ impl CpuVendor { /// Top-level Hyperlight snapshot config JSON. Lives at /// `blobs/sha256/` with media type -/// `application/vnd.hyperlight.snapshot.config.v1+json`. +/// `application/vnd.hyperlight.snapshot.config.v2+json`. /// /// In OCI terms this is the "image config" blob that the manifest's /// `config` descriptor points to. It describes the accompanying @@ -177,6 +177,8 @@ pub(super) struct OciSnapshotConfig { pub(super) cpu_vendor: CpuVendor, /// Top of the guest stack, in guest virtual address space. pub(super) stack_top_gva: u64, + /// Reseed policy for the guest libc PRNG. + pub(super) libc_rng_reseed_policy: crate::sandbox::LibcRngReseedPolicy, /// Guest virtual address the loader resumes the paused call at. pub(super) entrypoint_addr: u64, /// Guest virtual address of the ELF entry point @@ -761,6 +763,7 @@ mod tests { hypervisor: Hypervisor::Mshv, cpu_vendor: CpuVendor::current(), stack_top_gva: 0x2000, + libc_rng_reseed_policy: crate::sandbox::LibcRngReseedPolicy::Random, entrypoint_addr: SandboxMemoryLayout::BASE_ADDRESS as u64, original_entrypoint_addr: 0, sregs: distinct_sregs(), @@ -792,6 +795,16 @@ mod tests { } } + #[test] + fn missing_libc_rng_reseed_policy_is_rejected() { + let mut json = serde_json::to_value(gating_config()).unwrap(); + json.as_object_mut() + .unwrap() + .remove("libc_rng_reseed_policy"); + + assert!(serde_json::from_value::(json).is_err()); + } + /// A snapshot built for a different architecture is rejected. #[test] fn validate_for_load_rejects_arch_mismatch() { @@ -840,10 +853,11 @@ mod schema_pin { const PINNED_CALL: &str = r#"{ "hyperlight_version": "x.y.z", "arch": "x86_64", - "abi_version": 1, + "abi_version": 2, "hypervisor": "mshv", "cpu_vendor": "intel", "stack_top_gva": 3735928559, + "libc_rng_reseed_policy": "Random", "entrypoint_addr": 8192, "original_entrypoint_addr": 0, "sregs": { @@ -1026,10 +1040,11 @@ mod schema_pin { const PINNED_CALL: &str = r#"{ "hyperlight_version": "x.y.z", "arch": "aarch64", - "abi_version": 1, + "abi_version": 2, "hypervisor": "mshv", "cpu_vendor": "intel", "stack_top_gva": 3735928559, + "libc_rng_reseed_policy": "Random", "entrypoint_addr": 8192, "original_entrypoint_addr": 0, "sregs": { @@ -1086,7 +1101,7 @@ mod schema_pin { assert_eq!( actual_value, pinned_value, "Snapshot config JSON schema changed. If the change can break \ - existing snapshots on disk, bump `MT_CONFIG_V1` in \ + existing snapshots on disk, bump `MT_CONFIG_CURRENT` in \ `super::media_types` and follow `docs/snapshot-versioning.md`. \ Either way, paste the actual output below into the matching \ `PINNED_*`.\n\nactual:\n{actual}" diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/media_types.rs b/src/hyperlight_host/src/sandbox/snapshot/file/media_types.rs index 661bd4a04..f3b4c54f6 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/media_types.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/media_types.rs @@ -17,9 +17,9 @@ limitations under the License. // Media types are versioned by suffix. The writer emits `_CURRENT`. // The loader matches each version explicitly. See // docs/snapshot-versioning.md for how to add a version. -pub(in crate::sandbox::snapshot) const MT_CONFIG_V1: &str = - "application/vnd.hyperlight.snapshot.config.v1+json"; -pub(in crate::sandbox::snapshot) const MT_CONFIG_CURRENT: &str = MT_CONFIG_V1; +pub(in crate::sandbox::snapshot) const MT_CONFIG_V2: &str = + "application/vnd.hyperlight.snapshot.config.v2+json"; +pub(in crate::sandbox::snapshot) const MT_CONFIG_CURRENT: &str = MT_CONFIG_V2; pub(in crate::sandbox::snapshot) const MT_SNAPSHOT_V1: &str = "application/vnd.hyperlight.snapshot.memory.v1"; pub(in crate::sandbox::snapshot) const MT_SNAPSHOT_CURRENT: &str = MT_SNAPSHOT_V1; @@ -27,7 +27,7 @@ pub(in crate::sandbox::snapshot) const MT_SNAPSHOT_CURRENT: &str = MT_SNAPSHOT_V /// ABI version for the snapshot memory blob. Bumped when the /// host-guest contract for the snapshot bytes changes. See /// docs/snapshot-versioning.md. -pub(in crate::sandbox::snapshot) const SNAPSHOT_ABI_VERSION: u32 = 1; +pub(in crate::sandbox::snapshot) const SNAPSHOT_ABI_VERSION: u32 = 2; /// OCI standard annotation key for a manifest's tag inside an image /// index. Set on the manifest descriptor in `index.json`, not on the diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs index 2a0f00d2f..22e3e37d2 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs @@ -39,7 +39,7 @@ use self::media_types::{ ANNOTATION_ARCH, ANNOTATION_CPU, ANNOTATION_HYPERVISOR, ANNOTATION_REF_NAME, }; pub(super) use self::media_types::{ - MT_CONFIG_CURRENT, MT_CONFIG_V1, MT_SNAPSHOT_CURRENT, MT_SNAPSHOT_V1, SNAPSHOT_ABI_VERSION, + MT_CONFIG_CURRENT, MT_CONFIG_V2, MT_SNAPSHOT_CURRENT, MT_SNAPSHOT_V1, SNAPSHOT_ABI_VERSION, }; use self::reference::{OciDigest, OciReference, OciTag}; use super::{NextAction, Snapshot}; @@ -608,6 +608,7 @@ impl Snapshot { .ok_or_else(|| crate::new_error!("no hypervisor available to tag snapshot"))?, cpu_vendor: CpuVendor::current(), stack_top_gva: self.stack_top_gva, + libc_rng_reseed_policy: self.libc_rng_reseed_policy, entrypoint_addr, original_entrypoint_addr: self.original_entrypoint, sregs: *sregs, @@ -747,16 +748,15 @@ impl Snapshot { // digest. let manifest = load_manifest(path, &blobs_dir, reference, verify_blobs)?; let cfg_desc = manifest.config(); - // Loader dispatch on config media type. A future v2 lands - // as a new arm that converts to the in-memory current shape. + // Loader dispatch on config media type. let cfg_media = cfg_desc.media_type().to_string(); match cfg_media.as_str() { - MT_CONFIG_V1 => {} + MT_CONFIG_V2 => {} other => { return Err(crate::new_error!( "unexpected config media type {:?} (supported: {:?})", other, - MT_CONFIG_V1 + MT_CONFIG_V2 )); } } @@ -902,6 +902,7 @@ impl Snapshot { memory, load_info: crate::mem::exe::LoadInfo::dummy(), stack_top_gva: cfg.stack_top_gva, + libc_rng_reseed_policy: cfg.libc_rng_reseed_policy, sregs: Some(cfg.sregs), #[cfg(target_arch = "x86_64")] msrs: Some(cfg.msrs), diff --git a/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs b/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs index 4e0604c86..2cc0e4ba3 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs @@ -20,7 +20,7 @@ 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}; @@ -37,6 +37,30 @@ fn create_test_sandbox() -> MultiUseSandbox { .unwrap() } +fn create_c_test_sandbox() -> MultiUseSandbox { + create_c_test_sandbox_with_config(None) +} + +fn create_c_test_sandbox_with_config( + config: Option, +) -> MultiUseSandbox { + let path = c_simple_guest_as_pathbuf(); + UninitializedSandbox::new(GuestBinary::FilePath(path), config) + .unwrap() + .evolve() + .unwrap() +} + +fn random_sequence(sandbox: &mut MultiUseSandbox) -> [i32; 4] { + std::array::from_fn(|_| sandbox.call("NextRandom", ()).unwrap()) +} + +fn create_c_test_sandbox_with_seed(seed: crate::sandbox::LibcRngReseedPolicy) -> MultiUseSandbox { + let mut config = crate::sandbox::SandboxConfiguration::default(); + config.set_libc_rng_reseed_policy(seed); + create_c_test_sandbox_with_config(Some(config)) +} + fn create_snapshot() -> Arc { let mut sbox = create_test_sandbox(); sbox.snapshot().unwrap() @@ -2286,7 +2310,7 @@ fn manifest_uses_correct_config_and_layer_media_types() { serde_json::from_slice(&std::fs::read(manifest_path(&path)).unwrap()).unwrap(); assert_eq!( manifest["config"]["mediaType"].as_str().unwrap(), - "application/vnd.hyperlight.snapshot.config.v1+json" + "application/vnd.hyperlight.snapshot.config.v2+json" ); assert_eq!(manifest["layers"].as_array().unwrap().len(), 1); assert_eq!( @@ -2298,7 +2322,7 @@ fn manifest_uses_correct_config_and_layer_media_types() { // that falls back to `config.mediaType` sees the same value. assert_eq!( manifest["artifactType"].as_str().unwrap(), - "application/vnd.hyperlight.snapshot.config.v1+json" + "application/vnd.hyperlight.snapshot.config.v2+json" ); } @@ -3175,6 +3199,271 @@ 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 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 initial_guest_libc_rng_accepts_fixed_seed_including_zero() { + use crate::sandbox::{LibcRngReseedPolicy, SandboxConfiguration}; + + let mut zero_config = SandboxConfiguration::default(); + zero_config.set_libc_rng_reseed_policy(LibcRngReseedPolicy::Fixed(0)); + let mut first = create_c_test_sandbox_with_config(Some(zero_config)); + let mut second = create_c_test_sandbox_with_config(Some(zero_config)); + + let zero_sequence = random_sequence(&mut first); + assert_eq!(zero_sequence, random_sequence(&mut second)); + assert_ne!(zero_sequence, random_sequence(&mut first)); + + let mut one_config = SandboxConfiguration::default(); + one_config.set_libc_rng_reseed_policy(LibcRngReseedPolicy::Fixed(1)); + let mut third = create_c_test_sandbox_with_config(Some(one_config)); + + assert_ne!(zero_sequence, random_sequence(&mut third)); + + let mut max_first = create_c_test_sandbox_with_seed(LibcRngReseedPolicy::Fixed(u32::MAX)); + let mut max_second = create_c_test_sandbox_with_seed(LibcRngReseedPolicy::Fixed(u32::MAX)); + assert_eq!( + random_sequence(&mut max_first), + random_sequence(&mut max_second) + ); +} + +#[test] +fn fixed_guest_libc_rng_reseed_policy_is_owned_by_snapshot() { + use crate::sandbox::{LibcRngReseedPolicy, SandboxConfiguration}; + + let mut initial_config = SandboxConfiguration::default(); + initial_config.set_libc_rng_reseed_policy(LibcRngReseedPolicy::Fixed(42)); + let mut sandbox = create_c_test_sandbox_with_config(Some(initial_config)); + let snapshot = sandbox.snapshot().unwrap(); + + let mut conflicting_config = SandboxConfiguration::default(); + conflicting_config.set_libc_rng_reseed_policy(LibcRngReseedPolicy::Random); + let mut first = + MultiUseSandbox::from_snapshot(snapshot.clone(), HostFunctions::default(), None).unwrap(); + let mut second = MultiUseSandbox::from_snapshot( + snapshot, + HostFunctions::default(), + Some(conflicting_config), + ) + .unwrap(); + + let expected = random_sequence(&mut first); + assert_eq!(expected, random_sequence(&mut second)); + assert_ne!(expected, random_sequence(&mut first)); +} + +#[test] +fn random_guest_libc_rng_snapshot_ignores_fixed_from_snapshot_config() { + use crate::sandbox::{LibcRngReseedPolicy, SandboxConfiguration}; + + let mut source = create_c_test_sandbox(); + let snapshot = source.snapshot().unwrap(); + let mut conflicting_config = SandboxConfiguration::default(); + conflicting_config.set_libc_rng_reseed_policy(LibcRngReseedPolicy::Fixed(42)); + + let mut first = MultiUseSandbox::from_snapshot( + snapshot.clone(), + HostFunctions::default(), + Some(conflicting_config), + ) + .unwrap(); + let mut second = MultiUseSandbox::from_snapshot( + snapshot, + HostFunctions::default(), + Some(conflicting_config), + ) + .unwrap(); + + assert_ne!(random_sequence(&mut first), random_sequence(&mut second)); +} + +#[test] +fn random_guest_libc_rng_reseeds_on_restore() { + let mut sandbox = create_c_test_sandbox(); + let snapshot = sandbox.snapshot().unwrap(); + let captured = random_sequence(&mut sandbox); + + sandbox.restore(snapshot).unwrap(); + + assert_ne!(random_sequence(&mut sandbox), captured); +} + +#[test] +fn random_libc_rng_snapshot_overrides_fixed_destination_and_reseeds_every_restore() { + use crate::sandbox::LibcRngReseedPolicy; + + let mut source = create_c_test_sandbox(); + let random_snapshot = source.snapshot().unwrap(); + let mut destination = create_c_test_sandbox_with_seed(LibcRngReseedPolicy::Fixed(42)); + + destination.restore(random_snapshot.clone()).unwrap(); + let first = random_sequence(&mut destination); + destination.restore(random_snapshot).unwrap(); + let second = random_sequence(&mut destination); + + assert_ne!(first, second); + + let inherited = destination.snapshot().unwrap(); + let mut clone = create_c_test_sandbox_with_seed(LibcRngReseedPolicy::Fixed(42)); + clone.restore(inherited.clone()).unwrap(); + let inherited_first = random_sequence(&mut clone); + clone.restore(inherited).unwrap(); + let inherited_second = random_sequence(&mut clone); + + assert_ne!(inherited_first, inherited_second); +} + +#[test] +fn fixed_libc_rng_snapshot_reproduces_sequence_on_every_restore() { + use crate::sandbox::LibcRngReseedPolicy; + + let mut source = create_c_test_sandbox_with_seed(LibcRngReseedPolicy::Fixed(42)); + let fixed_snapshot = source.snapshot().unwrap(); + let mut destination = create_c_test_sandbox(); + + destination.restore(fixed_snapshot.clone()).unwrap(); + let first = random_sequence(&mut destination); + destination.restore(fixed_snapshot).unwrap(); + let second = random_sequence(&mut destination); + + assert_eq!(first, second); +} + +#[test] +fn fixed_libc_rng_restore_uses_entire_u32_seed_range() { + use crate::sandbox::LibcRngReseedPolicy; + + let mut restored_sequences = Vec::new(); + for seed in [0, 42, u32::MAX] { + let seed = LibcRngReseedPolicy::Fixed(seed); + let mut expected_sandbox = create_c_test_sandbox_with_seed(seed); + let expected = random_sequence(&mut expected_sandbox); + + let mut source = create_c_test_sandbox_with_seed(seed); + let snapshot = source.snapshot().unwrap(); + let mut destination = create_c_test_sandbox(); + destination.restore(snapshot).unwrap(); + let restored = random_sequence(&mut destination); + + assert_eq!(restored, expected); + restored_sequences.push(restored); + } + + assert_ne!(restored_sequences[0], restored_sequences[1]); + assert_ne!(restored_sequences[0], restored_sequences[2]); + assert_ne!(restored_sequences[1], restored_sequences[2]); +} + +#[test] +fn restore_adopts_incoming_snapshot_libc_rng_reseed_policy() { + use crate::sandbox::{LibcRngReseedPolicy, SandboxConfiguration}; + + let mut fixed_config = SandboxConfiguration::default(); + fixed_config.set_libc_rng_reseed_policy(LibcRngReseedPolicy::Fixed(0)); + let mut fixed = create_c_test_sandbox_with_config(Some(fixed_config)); + let expected = random_sequence(&mut fixed); + let incoming = fixed.snapshot().unwrap(); + + let mut sandbox = create_c_test_sandbox(); + sandbox.restore(incoming).unwrap(); + assert_eq!(random_sequence(&mut sandbox), expected); + assert_ne!(random_sequence(&mut sandbox), expected); + + let inherited = sandbox.snapshot().unwrap(); + let mut clone = create_c_test_sandbox(); + clone.restore(inherited).unwrap(); + assert_eq!(random_sequence(&mut clone), expected); +} + +#[test] +fn persisted_snapshot_retains_guest_libc_rng_reseed_policy() { + use crate::sandbox::{LibcRngReseedPolicy, SandboxConfiguration}; + + let mut config = SandboxConfiguration::default(); + config.set_libc_rng_reseed_policy(LibcRngReseedPolicy::Fixed(42)); + let mut sandbox = create_c_test_sandbox_with_config(Some(config)); + let snapshot = sandbox.snapshot().unwrap(); + let expected = random_sequence(&mut sandbox); + 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()); + assert_eq!( + loaded.libc_rng_reseed_policy(), + LibcRngReseedPolicy::Fixed(42) + ); + + let mut restored = create_c_test_sandbox(); + restored.restore(loaded).unwrap(); + assert_eq!(random_sequence(&mut restored), expected); +} + +#[test] +fn persisted_random_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)); +} + +#[test] +fn invalid_persisted_libc_rng_reseed_policy_is_rejected() { + for invalid_seed in [ + serde_json::json!("Invalid"), + serde_json::json!({ "Fixed": -1 }), + serde_json::json!({ "Fixed": 4294967296_u64 }), + ] { + let snapshot = create_snapshot(); + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("snapshot"); + snapshot + .save(&path, &OciTag::new("latest").unwrap()) + .unwrap(); + rewrite_config(&path, |config| { + config["libc_rng_reseed_policy"] = invalid_seed; + }); + + unwrap_err_snapshot(Snapshot::checked_load( + &path, + OciTag::new("latest").unwrap(), + )); + } +} + /// `from_snapshot` honors `guest_core_dump=true` so that /// `generate_crashdump_to_dir` writes a file. #[test] diff --git a/src/hyperlight_host/src/sandbox/snapshot/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/mod.rs index e1578e2e7..ad16795ec 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/mod.rs @@ -87,6 +87,9 @@ pub struct Snapshot { /// The address of the top of the guest stack stack_top_gva: u64, + /// Reseed policy for the guest libc PRNG. + libc_rng_reseed_policy: crate::sandbox::LibcRngReseedPolicy, + /// Special register state captured from the vCPU during snapshot. /// None for snapshots created directly from a binary (before /// guest runs). Some for snapshots taken from a running sandbox. @@ -397,6 +400,7 @@ impl Snapshot { layout, load_info, stack_top_gva: exn_stack_top_gva, + libc_rng_reseed_policy: cfg.get_libc_rng_reseed_policy(), sregs: None, #[cfg(target_arch = "x86_64")] msrs: None, @@ -426,6 +430,7 @@ impl Snapshot { regions: Vec, root_pt_gpas: &[u64], stack_top_gva: u64, + libc_rng_reseed_policy: crate::sandbox::LibcRngReseedPolicy, sregs: CommonSpecialRegisters, #[cfg(target_arch = "x86_64")] msrs: Vec, next_action: NextAction, @@ -581,6 +586,7 @@ impl Snapshot { memory: ReadonlySharedMemory::from_bytes(&memory, guest_visible_size)?, load_info, stack_top_gva, + libc_rng_reseed_policy, sregs: Some(sregs), #[cfg(target_arch = "x86_64")] msrs: Some(msrs), @@ -619,6 +625,10 @@ impl Snapshot { self.stack_top_gva } + pub(crate) fn libc_rng_reseed_policy(&self) -> crate::sandbox::LibcRngReseedPolicy { + self.libc_rng_reseed_policy + } + /// Returns the special registers stored in this snapshot. /// Returns None for snapshots created directly from a binary (before preinitialisation). /// Returns Some for snapshots taken from a running sandbox. @@ -798,6 +808,7 @@ mod tests { Vec::new(), &[pt_base], 0, + crate::sandbox::LibcRngReseedPolicy::Random, default_sregs(), #[cfg(target_arch = "x86_64")] Vec::new(), @@ -818,6 +829,7 @@ mod tests { Vec::new(), &[pt_base], 0, + crate::sandbox::LibcRngReseedPolicy::Random, default_sregs(), #[cfg(target_arch = "x86_64")] Vec::new(), diff --git a/src/hyperlight_host/src/sandbox/snapshot/tripwires.rs b/src/hyperlight_host/src/sandbox/snapshot/tripwires.rs index ee04373a8..807402efe 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/tripwires.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/tripwires.rs @@ -28,8 +28,8 @@ use super::file::{ MT_CONFIG_CURRENT, MT_SNAPSHOT_CURRENT, OCI_LAYOUT_VERSION, SNAPSHOT_ABI_VERSION, }; -const EXPECTED_ABI_VERSION: u32 = 1; -const EXPECTED_MT_CONFIG: &str = "application/vnd.hyperlight.snapshot.config.v1+json"; +const EXPECTED_ABI_VERSION: u32 = 2; +const EXPECTED_MT_CONFIG: &str = "application/vnd.hyperlight.snapshot.config.v2+json"; const EXPECTED_MT_SNAPSHOT: &str = "application/vnd.hyperlight.snapshot.memory.v1"; const EXPECTED_OCI_LAYOUT_VERSION: &str = "1.0.0"; diff --git a/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs b/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs index 850f76e1c..eee73cfcf 100644 --- a/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs +++ b/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs @@ -13,7 +13,6 @@ 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. */ -use rand::RngExt; use tracing::{Span, instrument}; use super::SandboxConfiguration; @@ -51,10 +50,7 @@ pub(super) fn evolve_impl_multi_use(u_sbox: UninitializedSandbox) -> Result() - }; + let seed = u_sbox.config.get_libc_rng_reseed_policy().resolve(); let peb_addr = { let peb_u64 = u64::try_from(hshm.layout.peb_address())?; RawPtr::from(peb_u64) @@ -91,14 +87,19 @@ pub(super) fn evolve_impl_multi_use(u_sbox: UninitializedSandbox) -> Resultparameters[0].value.VecBytes; uint8_t *x = malloc(input.len); @@ -359,6 +361,7 @@ 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(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 +401,7 @@ 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("SetStatic", set_static); // HYPERLIGHT_REGISTER_FUNCTION macro does not work for functions that return VecBytes, // so we use hl_register_function_definition directly