diff --git a/Cargo.lock b/Cargo.lock index 7fe9f69..79aeecc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -255,9 +255,9 @@ dependencies = [ [[package]] name = "buddy_system_allocator" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672b945a3e4f4f40bfd4cd5ee07df9e796a42254ce7cd6d2599ad969244c44a" +checksum = "c1af5a01adeeade54c9f5060300227a8ee64c05b6376f28e9b8ee3b72dd2056f" dependencies = [ "spin", ] @@ -1145,23 +1145,23 @@ dependencies = [ [[package]] name = "gdbstub" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf845b08f7c2ef3b5ad19f80779d43ae20d278652b91bb80adda65baf2d8ed6" +checksum = "5bafc7e33650ab9f05dcc16325f05d56b8d10393114e31a19a353b86fa60cfe7" dependencies = [ "bitflags 2.11.0", "cfg-if", "log", "managed", "num-traits", - "paste", + "pastey", ] [[package]] name = "gdbstub_arch" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22dde0e1b68787036ccedd0b1ff6f953527a0e807e571fbe898975203027278f" +checksum = "6c02bfe7bd65f42bcda751456869dfa1eb2bd1c36e309b9ec27f4888d41cf258" dependencies = [ "gdbstub", "num-traits", @@ -1329,12 +1329,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - [[package]] name = "http" version = "1.3.1" @@ -1461,9 +1455,9 @@ dependencies = [ [[package]] name = "hyperlight-common" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4187a7affab21aa49e498781595648437ac22cfdf94753c3200eddf89f7ab98c" +checksum = "be2d385987047c64fdb5c95e05ef3b6bd9431001a8fe694abb29b29e99913167" dependencies = [ "anyhow", "flatbuffers", @@ -1476,9 +1470,9 @@ dependencies = [ [[package]] name = "hyperlight-component-macro" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f814ad0745e14be12779361222d63a82928c0c6ce27612c0c89ed925d8957433" +checksum = "fa7ae5cd7c8e5f5b925b07bce45be853ac0be0e6d4e8b2afd17ad8c16aa135c4" dependencies = [ "env_logger", "hyperlight-component-util", @@ -1487,14 +1481,14 @@ dependencies = [ "proc-macro2", "quote", "syn", - "wasmparser 0.245.1", + "wasmparser 0.246.1", ] [[package]] name = "hyperlight-component-util" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dbaeeecb5097a74d57193aae73701af724295375b681177025e9ce278ed4798" +checksum = "522ece4723e1c0a81355c07131d6ba5c39c9b4bbc66d8965893957a32a90c029" dependencies = [ "itertools 0.14.0", "prettyplease", @@ -1502,14 +1496,14 @@ dependencies = [ "quote", "syn", "tracing", - "wasmparser 0.245.1", + "wasmparser 0.246.1", ] [[package]] name = "hyperlight-guest" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e6132c20b413ba14ad6967c3c941702276773d3bc749569e746e3d1df3dff3" +checksum = "6ee2682d284171d2bd9e80e3e543de310356c1d8ec65e88028b3e08048d15d3d" dependencies = [ "anyhow", "flatbuffers", @@ -1521,9 +1515,9 @@ dependencies = [ [[package]] name = "hyperlight-guest-bin" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84470d8d2f9647a02f6bf5287f4083937df6452cacac028cd772f3796c6e2f3" +checksum = "3740817f3be98d18edb50eec985461baca0eba5b1f73d0f475b7a539d812e750" dependencies = [ "buddy_system_allocator", "cc", @@ -1542,9 +1536,9 @@ dependencies = [ [[package]] name = "hyperlight-guest-macro" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "440596a4204f3c60da9e11da838bcd280bf350b2087361a0ebf69c088bb76423" +checksum = "a4f88cbcde1ca986e4f14a85ea0a436659edcb6eb63e8a9d14413ca5cdc8ea86" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1554,9 +1548,9 @@ dependencies = [ [[package]] name = "hyperlight-guest-tracing" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f2c1db4d357e8ff0e0f9e26432b38e079d7d7949daa063effeb25bbdce50212" +checksum = "d14e9c34c508ecfba1283a552c3eb118d8ee82d863e2e91377a6059f06cf84dd" dependencies = [ "hyperlight-common", "spin", @@ -1566,9 +1560,9 @@ dependencies = [ [[package]] name = "hyperlight-host" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05a62d79d2871137b0feea8db77e222289594766e2d6902ef0bdb0d65d8edda6" +checksum = "65c2f9b5a13324c5dd786bf77a7531216207684da1cefc19fb9b697d08346ac0" dependencies = [ "anyhow", "bitflags 2.11.0", @@ -1597,7 +1591,6 @@ dependencies = [ "rand 0.10.0", "rust-embed", "serde_json", - "sha256", "termcolor", "thiserror 2.0.18", "tracing", @@ -2478,10 +2471,10 @@ dependencies = [ ] [[package]] -name = "paste" -version = "1.0.15" +name = "pastey" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" [[package]] name = "percent-encoding" @@ -3275,19 +3268,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha256" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f880fc8562bdeb709793f00eb42a2ad0e672c4f883bbe59122b926eca935c8f6" -dependencies = [ - "async-trait", - "bytes", - "hex", - "sha2", - "tokio", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -4098,9 +4078,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.245.1" +version = "0.246.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" +checksum = "2d991c35d79bf8336dc1cd632ed4aacf0dc5fac4bc466c670625b037b972bb9c" dependencies = [ "bitflags 2.11.0", "hashbrown 0.16.1", diff --git a/Cargo.toml b/Cargo.toml index d02c239..b8ccd6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,11 +13,11 @@ repository = "https://github.com/hyperlight-dev/hyperlight-wasm" readme = "README.md" [workspace.dependencies] -hyperlight-common = { version = "0.13.1", default-features = false } -hyperlight-component-macro = { version = "0.13.1" } -hyperlight-component-util = { version = "0.13.1" } -hyperlight-guest = { version = "0.13.1" } -hyperlight-guest-bin = { version = "0.13.1", features = [ "printf" ] } -hyperlight-host = { version = "0.13.1", default-features = false, features = ["executable_heap"] } +hyperlight-common = { version = "0.14.0", default-features = false } +hyperlight-component-macro = { version = "0.14.0" } +hyperlight-component-util = { version = "0.14.0" } +hyperlight-guest = { version = "0.14.0" } +hyperlight-guest-bin = { version = "0.14.0", features = [ "printf" ] } +hyperlight-host = { version = "0.14.0", default-features = false, features = ["executable_heap"] } hyperlight-wasm-macro = { version = "0.13.1", path = "src/hyperlight_wasm_macro" } hyperlight-wasm-runtime = { version = "0.13.1", path = "src/hyperlight_wasm_runtime" } diff --git a/src/hyperlight_wasm/src/sandbox/loaded_wasm_sandbox.rs b/src/hyperlight_wasm/src/sandbox/loaded_wasm_sandbox.rs index 9f42647..59ccfa5 100644 --- a/src/hyperlight_wasm/src/sandbox/loaded_wasm_sandbox.rs +++ b/src/hyperlight_wasm/src/sandbox/loaded_wasm_sandbox.rs @@ -112,11 +112,14 @@ impl LoadedWasmSandbox { } } - /// Unload the wasm module and return a `WasmSandbox` that can be used to load another module. + /// Unload the wasm module and return a `WasmSandbox` that can be + /// used to load another module. /// - /// This method internally calls [`restore()`](Self::restore) to reset the sandbox to its - /// pre-module state, which also clears any poisoned state. This means `unload_module()` - /// can be called on a poisoned sandbox to recover it. + /// This method defers calling [`restore()`](Self::restore) to + /// reset the sandbox to its pre-module state until a new module + /// is loaded. However, the sandbox will always be restored when a + /// new module is loaded, so a poisoned sandbox can be recovered + /// by unloading and reloading a module. pub fn unload_module(mut self) -> Result { let sandbox = self .inner diff --git a/src/hyperlight_wasm/src/sandbox/wasm_sandbox.rs b/src/hyperlight_wasm/src/sandbox/wasm_sandbox.rs index b468bd0..a47f566 100644 --- a/src/hyperlight_wasm/src/sandbox/wasm_sandbox.rs +++ b/src/hyperlight_wasm/src/sandbox/wasm_sandbox.rs @@ -27,6 +27,141 @@ use crate::sandbox::metrics::{ METRIC_ACTIVE_WASM_SANDBOXES, METRIC_SANDBOX_LOADS, METRIC_TOTAL_WASM_SANDBOXES, }; +// All the logic around when to restore is nicely encapsulated here, +// so that it would be harder for a `WasmSandbox` to end up in an +// un-restored state. +mod backing_sandbox { + use super::*; + #[derive(Debug)] + pub(super) enum BackingSandbox { + /// A sandbox which has a clean copy of the runtime in it + Clean(MultiUseSandbox), + /// A sandbox which has had a wasm component/module loaded into + /// it, but has not yet run any code from that + Loaded(MultiUseSandbox), + /// A sandbox which came from a `LoadedWasmSandbox`, and + /// therefore presumably has run user code + Dirty(MultiUseSandbox), + /// A non-existent sandbox, used as an internal implementation + /// detail of a few methods. + Missing, + } + impl BackingSandbox { + pub(super) fn clean(&mut self, snapshot: Arc) -> Result<()> { + *self = match std::mem::replace(self, BackingSandbox::Missing) { + BackingSandbox::Clean(x) => BackingSandbox::Clean(x), + BackingSandbox::Loaded(_) => { + return Err(new_error!( + "internal invariant violation: cleaning loaded backing sandbox" + )); + } + BackingSandbox::Dirty(mut x) => { + x.restore(snapshot)?; + BackingSandbox::Clean(x) + } + BackingSandbox::Missing => { + return Err(new_error!( + "internal invariant violation: cleaning missing backing sandbox" + )); + } + }; + Ok(()) + } + pub(super) fn load_via_restore(&mut self, snapshot: Arc) -> Result<()> { + *self = match std::mem::replace(self, BackingSandbox::Missing) { + BackingSandbox::Clean(mut x) | BackingSandbox::Dirty(mut x) => { + x.restore(snapshot)?; + BackingSandbox::Loaded(x) + } + BackingSandbox::Loaded(_) => { + return Err(new_error!( + "internal invariant violation: loading loaded backing sandbox" + )); + } + BackingSandbox::Missing => { + return Err(new_error!( + "internal invariant violation: loading missing backing sandbox" + )); + } + }; + Ok(()) + } + pub(super) fn load_via_fn( + &mut self, + load: impl FnOnce(&mut MultiUseSandbox) -> Result<()>, + ) -> Result<()> { + *self = match std::mem::replace(self, BackingSandbox::Missing) { + BackingSandbox::Clean(mut x) => { + load(&mut x)?; + BackingSandbox::Loaded(x) + } + _ => { + return Err(new_error!( + "internal invariant violation: loading non-clean backing sandbox" + )); + } + }; + Ok(()) + } + pub(super) fn get_loaded(&mut self) -> Result { + match std::mem::replace(self, BackingSandbox::Missing) { + BackingSandbox::Loaded(x) => Ok(x), + _ => Err(new_error!( + "internal invariant violation: encountered non-loaded backing sandbox" + )), + } + } + } + + #[cfg(test)] + mod tests { + use super::super::tests::*; + use super::*; + #[test] + fn test_backing_sandbox_use_marks_dirty() -> Result<()> { + let mut sb = SandboxBuilder::new().build()?; + sb.register( + "GetTimeSinceBootMicrosecond", + get_time_since_boot_microsecond, + )?; + let sb = sb.load_runtime()?; + let lb = sb.load_module(get_test_file_path("RunWasm.aot")?)?; + let sb = lb.unload_module()?; + assert!(matches!(sb.inner, super::BackingSandbox::Dirty(_))); + Ok(()) + } + + #[test] + fn test_dirty_backing_sandbox_cannot_be_loaded_via_fn() -> Result<()> { + let mut sb = SandboxBuilder::new().build()?; + sb.register( + "GetTimeSinceBootMicrosecond", + get_time_since_boot_microsecond, + )?; + let sb = sb.load_runtime()?; + let lb = sb.load_module(get_test_file_path("RunWasm.aot")?)?; + let mut sb = lb.unload_module()?; + assert!(sb.inner.load_via_fn(|_| Ok(())).is_err()); + Ok(()) + } + + #[test] + fn test_dirty_backing_sandbox_cannot_be_gotten_as_loaded() -> Result<()> { + let mut sb = SandboxBuilder::new().build()?; + sb.register( + "GetTimeSinceBootMicrosecond", + get_time_since_boot_microsecond, + )?; + let sb = sb.load_runtime()?; + let lb = sb.load_module(get_test_file_path("RunWasm.aot")?)?; + let mut sb = lb.unload_module()?; + assert!(sb.inner.get_loaded().is_err()); + Ok(()) + } + } +} +use backing_sandbox::*; + /// A sandbox with just the Wasm engine loaded into memory. `WasmSandbox`es /// are not yet ready to execute guest functions. /// @@ -37,7 +172,7 @@ pub struct WasmSandbox { // inner is an Option as we need to take ownership of it // We implement drop on the WasmSandbox to decrement the count of Sandboxes when it is dropped // because of this we cannot implement drop without making inner an Option (alternatively we could make MultiUseSandbox Copy but that would introduce other issues) - inner: Option, + inner: BackingSandbox, // Snapshot of state of an initial WasmSandbox (runtime loaded, but no guest module code loaded). // Used for LoadedWasmSandbox to be able restore state back to WasmSandbox snapshot: Option>, @@ -54,7 +189,7 @@ impl WasmSandbox { metrics::gauge!(METRIC_ACTIVE_WASM_SANDBOXES).increment(1); metrics::counter!(METRIC_TOTAL_WASM_SANDBOXES).increment(1); Ok(WasmSandbox { - inner: Some(inner), + inner: BackingSandbox::Clean(inner), snapshot: Some(snapshot), }) } @@ -64,35 +199,49 @@ impl WasmSandbox { /// the snapshot has already been created in that case. /// Expects a snapshot of the state where wasm runtime is loaded, but no guest module code is loaded. pub(super) fn new_from_loaded( - mut loaded: MultiUseSandbox, + loaded: MultiUseSandbox, snapshot: Arc, ) -> Result { - loaded.restore(snapshot.clone())?; metrics::gauge!(METRIC_ACTIVE_WASM_SANDBOXES).increment(1); metrics::counter!(METRIC_TOTAL_WASM_SANDBOXES).increment(1); Ok(WasmSandbox { - inner: Some(loaded), + inner: BackingSandbox::Dirty(loaded), snapshot: Some(snapshot), }) } + fn clean_inner(&mut self) -> Result<()> { + let snapshot = self.snapshot.as_ref().ok_or(new_error!( + "internal invariant violation: Snapshot is missing" + ))?; + self.inner.clean(snapshot.clone()) + } + /// Load a Wasm module at the given path into the sandbox and return a `LoadedWasmSandbox` /// able to execute code in the loaded Wasm Module. /// /// Before you can call guest functions in the sandbox, you must call /// this function and use the returned value to call guest functions. pub fn load_module(mut self, file: impl AsRef) -> Result { - let inner = self - .inner - .as_mut() - .ok_or_else(|| new_error!("WasmSandbox is None"))?; - - if let Ok(len) = inner.map_file_cow(file.as_ref(), MAPPED_BINARY_VA, None) { - inner.call::<()>("LoadWasmModulePhys", (MAPPED_BINARY_VA, len))?; - } else { - let wasm_bytes = std::fs::read(file)?; - load_wasm_module_from_bytes(inner, wasm_bytes)?; - } + self.clean_inner()?; + + self.inner.load_via_fn(|inner| { + if let Ok(len) = inner.map_file_cow(file.as_ref(), MAPPED_BINARY_VA, None) { + inner.call::<()>("LoadWasmModulePhys", (MAPPED_BINARY_VA, len))?; + } else { + let wasm_bytes = std::fs::read(file)?; + load_wasm_module_from_bytes(inner, wasm_bytes)?; + } + Ok(()) + })?; + + self.finalize_module_load() + } + + /// Load a Wasm module by restoring a Hyperlight snapshot taken + /// from a `LoadedWasmSandbox`. + pub fn load_from_snapshot(mut self, snapshot: Arc) -> Result { + self.inner.load_via_restore(snapshot)?; self.finalize_module_load() } @@ -114,24 +263,25 @@ impl WasmSandbox { base: *mut libc::c_void, len: usize, ) -> Result { - let inner = self - .inner - .as_mut() - .ok_or_else(|| new_error!("WasmSandbox is None"))?; - - let guest_base: usize = MAPPED_BINARY_VA as usize; - let rgn = MemoryRegion { - host_region: base as usize..base.wrapping_add(len) as usize, - guest_region: guest_base..guest_base + len, - flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE, - region_type: MemoryRegionType::Heap, - }; - if let Ok(()) = unsafe { inner.map_region(&rgn) } { - inner.call::<()>("LoadWasmModulePhys", (MAPPED_BINARY_VA, len as u64))?; - } else { - let wasm_bytes = unsafe { std::slice::from_raw_parts(base as *const u8, len).to_vec() }; - load_wasm_module_from_bytes(inner, wasm_bytes)?; - } + self.clean_inner()?; + + self.inner.load_via_fn(|inner| { + let guest_base: usize = MAPPED_BINARY_VA as usize; + let rgn = MemoryRegion { + host_region: base as usize..base.wrapping_add(len) as usize, + guest_region: guest_base..guest_base + len, + flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE, + region_type: MemoryRegionType::Heap, + }; + if let Ok(()) = unsafe { inner.map_region(&rgn) } { + inner.call::<()>("LoadWasmModulePhys", (MAPPED_BINARY_VA, len as u64))?; + } else { + let wasm_bytes = + unsafe { std::slice::from_raw_parts(base as *const u8, len).to_vec() }; + load_wasm_module_from_bytes(inner, wasm_bytes)?; + } + Ok(()) + })?; self.finalize_module_load() } @@ -142,13 +292,11 @@ impl WasmSandbox { /// Before you can call guest functions in the sandbox, you must call /// this function and use the returned value to call guest functions. pub fn load_module_from_buffer(mut self, buffer: &[u8]) -> Result { - let inner = self - .inner - .as_mut() - .ok_or_else(|| new_error!("WasmSandbox is None"))?; + self.clean_inner()?; // TODO: get rid of this clone - load_wasm_module_from_bytes(inner, buffer.to_vec())?; + self.inner + .load_via_fn(|inner| load_wasm_module_from_bytes(inner, buffer.to_vec()))?; self.finalize_module_load() } @@ -156,12 +304,14 @@ impl WasmSandbox { /// Helper function to finalize module loading and create LoadedWasmSandbox fn finalize_module_load(mut self) -> Result { metrics::counter!(METRIC_SANDBOX_LOADS).increment(1); - match (self.inner.take(), self.snapshot.take()) { - (Some(sandbox), Some(snapshot)) => LoadedWasmSandbox::new(sandbox, snapshot), - _ => Err(new_error!( - "WasmSandbox/snapshot is None, cannot load module" - )), - } + + let sandbox = self.inner.get_loaded()?; + + let snapshot = self.snapshot.take().ok_or(new_error!( + "internal invariant violation: Snapshot is missing" + ))?; + + LoadedWasmSandbox::new(sandbox, snapshot) } } @@ -199,7 +349,7 @@ mod tests { use hyperlight_host::{HyperlightError, is_hypervisor_present}; use super::*; - use crate::sandbox::sandbox_builder::SandboxBuilder; + pub(super) use crate::sandbox::sandbox_builder::SandboxBuilder; #[test] fn test_new_sandbox() -> Result<()> { @@ -207,7 +357,7 @@ mod tests { Ok(()) } - fn get_time_since_boot_microsecond() -> Result { + pub(super) fn get_time_since_boot_microsecond() -> Result { let res = std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH)? .as_micros(); @@ -473,6 +623,46 @@ mod tests { } } + #[test] + fn test_load_from_snapshot() { + let mut sandbox = SandboxBuilder::new().build().unwrap(); + sandbox + .register( + "GetTimeSinceBootMicrosecond", + get_time_since_boot_microsecond, + ) + .unwrap(); + let sb = sandbox.load_runtime().unwrap(); + + let helloworld_wasm = get_test_file_path("HelloWorld.aot").unwrap(); + let runwasm_wasm = get_test_file_path("RunWasm.aot").unwrap(); + + // load one module, and make sure that a function in it + // can be called + let mut lb1 = sb.load_module(helloworld_wasm).unwrap(); + let result: i32 = lb1 + .call_guest_function("HelloWorld", "Message from Rust Test".to_string()) + .unwrap(); + assert_eq!(result, 0); + let snapshot = lb1.snapshot().unwrap(); + + // load another module, and make sure that a function in + // it can be called + let sb = lb1.unload_module().unwrap(); + let mut lb2 = sb.load_module(runwasm_wasm).unwrap(); + let result: i32 = lb2.call_guest_function("CalcFib", 10i32).unwrap(); + assert_eq!(result, 55); + + // reload the first module via snapshot, and make sure the + // original function can be called again + let sb = lb2.unload_module().unwrap(); + let mut lb3 = sb.load_from_snapshot(snapshot).unwrap(); + let result: i32 = lb3 + .call_guest_function("HelloWorld", "Message from Rust Test".to_string()) + .unwrap(); + assert_eq!(result, 0); + } + #[test] fn test_load_module_buffer() { let sandboxes = get_test_wasm_sandboxes().unwrap(); @@ -496,7 +686,7 @@ mod tests { } } - fn get_test_file_path(filename: &str) -> Result { + pub(super) fn get_test_file_path(filename: &str) -> Result { #[cfg(debug_assertions)] let config = "debug"; #[cfg(not(debug_assertions))] diff --git a/src/hyperlight_wasm_runtime/src/platform.rs b/src/hyperlight_wasm_runtime/src/platform.rs index ff6b660..c2e6982 100644 --- a/src/hyperlight_wasm_runtime/src/platform.rs +++ b/src/hyperlight_wasm_runtime/src/platform.rs @@ -172,32 +172,128 @@ pub extern "C" fn wasmtime_init_traps(handler: wasmtime_trap_handler_t) -> i32 { 0 } -// The wasmtime_memory_image APIs are not yet supported. +// Copy a VA range to a new VA. Old and new VA, and len, must be +// page-aligned. +fn copy_va_mapping(base: *const u8, len: usize, to_va: *mut u8, remap_original: bool) { + debug_assert!((base as usize).is_multiple_of(vmem::PAGE_SIZE)); + debug_assert!(len.is_multiple_of(vmem::PAGE_SIZE)); + // TODO: all this barrier code is amd64 specific. It should be + // refactored to use some better architecture-independent APIs. + // + // On amd64, "upgrades" including the first time that a a valid + // translation exists for a VA, only need a light (serialising + // instruction) barrier. Since invlpg is also a barrier, we don't + // even need that, if we did do a downgrade remap just before. + let mut needs_first_valid_exposure_barrier = false; + + // TODO: make this more efficient by directly exposing the ability + // to traverse an entire VA range in + // hyperlight_guest_bin::paging::virt_to_phys, and coalescing + // continuous ranges there. + let base_u = base as u64; + let va_page_bases = (base_u..(base_u + len as u64)).step_by(vmem::PAGE_SIZE); + let mappings = va_page_bases.flat_map(paging::virt_to_phys); + for mapping in mappings { + // TODO: Deduplicate with identical logic in hyperlight_host snapshot. + let (new_kind, was_writable) = match mapping.kind { + // Skip unmapped pages, since they will be unmapped in + // both the original and the new copy + vmem::MappingKind::Unmapped => continue, + vmem::MappingKind::Basic(bm) if bm.writable => ( + vmem::MappingKind::Cow(vmem::CowMapping { + readable: bm.readable, + executable: bm.executable, + }), + true, + ), + vmem::MappingKind::Basic(bm) => ( + vmem::MappingKind::Basic(vmem::BasicMapping { + readable: bm.readable, + writable: false, + executable: bm.executable, + }), + false, + ), + vmem::MappingKind::Cow(cm) => (vmem::MappingKind::Cow(cm), false), + }; + let do_downgrade = remap_original && was_writable; + if do_downgrade { + // If necessary, remap the original page as Cow, instead + // of whatever it is now, to ensure that any more writes to + // that region do not change the image base. + // + // TODO: could the table traversal needed for this be fused + // with the table traversal that got the original mapping, + // above? + unsafe { + paging::map_region( + mapping.phys_base, + mapping.virt_base as *mut u8, + vmem::PAGE_SIZE as u64, + new_kind, + ); + } + } + // map the same pages to the new VA + unsafe { + paging::map_region( + mapping.phys_base, + to_va.wrapping_add((mapping.virt_base - base_u) as usize), + vmem::PAGE_SIZE as u64, + new_kind, + ); + } + if do_downgrade { + // Since we have downgraded a page from writable to CoW we + // need to do an invlpg on it. Because invlpg is a + // serialising instruction, we don't need the other + // barrier for the new mapping. + unsafe { + core::arch::asm!("invlpg [{}]", in(reg) mapping.virt_base, options(readonly, nostack, preserves_flags)); + } + needs_first_valid_exposure_barrier = false; + } else { + needs_first_valid_exposure_barrier = true; + } + } + if needs_first_valid_exposure_barrier { + paging::barrier::first_valid_same_ctx(); + } +} + +// Create a copy-on-write memory image from some existing VA range. +// `ptr` and `len` must be page-aligned (which is guaranteed by the +// wasmtime-platform.h interface). #[no_mangle] pub extern "C" fn wasmtime_memory_image_new( - _ptr: *const u8, - _len: usize, + ptr: *const u8, + len: usize, ret: &mut *mut c_void, ) -> i32 { - *ret = core::ptr::null_mut(); + // Choose an arbitrary VA, which we will use as the memory image + // identifier. We will construct the image by mapping a copy of + // the original VA range here, making the original copy CoW as we + // go. + let new_virt = FIRST_VADDR.fetch_add(0x100_0000_0000, Ordering::Relaxed) as *mut u8; + copy_va_mapping(ptr, len, new_virt, true); + *ret = new_virt as *mut c_void; 0 } #[no_mangle] pub extern "C" fn wasmtime_memory_image_map_at( - _image: *mut c_void, - _addr: *mut u8, - _len: usize, + image: *mut c_void, + addr: *mut u8, + len: usize, ) -> i32 { - /* This should never be called because wasmtime_memory_image_new - * returns NULL */ - panic!("wasmtime_memory_image_map_at"); + copy_va_mapping(image as *mut u8, len, addr, false); + 0 } #[no_mangle] pub extern "C" fn wasmtime_memory_image_free(_image: *mut c_void) { - /* This should never be called because wasmtime_memory_image_new - * returns NULL */ + /* This should never be called in practice, because we simply + * restore the snapshot rather than actually unload/destroy instances */ panic!("wasmtime_memory_image_free"); }