From 360df45e1a6f96d692d0a63c4c204a279ca1f9ac Mon Sep 17 00:00:00 2001 From: zach Date: Thu, 1 Jun 2023 09:37:42 -0700 Subject: [PATCH] fix: require modules to have exported, bounded memory when manifest `memory.max_pages` field is set (#356) - Requires modules compiled to run with manifests that set `max_memory` to have an exported memory with lower and upper bounds - Includes the size of memory exported from modules when calculating available memory for plugins ## How to compile a module with bounded memory You will need to pass `--max-memory=$NUM_BYTES` to wasm-ld. `$NUM_BYTES` must be a multiple of the page size. Here are some examples for supported PDK languages: **C** Pass `-Wl,--max-memory=65536` to your C compiler **Rust**: In a `.cargo/config` file: ```toml [target.wasm32-unknown-unknown] rustflags = ["-Clink-args=--max-memory=65536"] ``` **Haskell** Add the following to the cabal file entry for your `cabal.project` file: ``` package myproject ghc-options: -optl -Wl,--max-memory=65536 ``` **AssemblyScript** Pass `--maximumMemory 65536` to the assemblyscropt compiler **TinyGo**: Create a `target.json` file: ```json { "inherits": [ "wasm" ], "ldflags": [ "--max-memory=65536", ] } ``` and build using `tinygo -target ./target.json` --- .../test/java/org/extism/sdk/PluginTests.java | 6 +- php/example/index.php | 3 +- python/example.py | 2 +- python/tests/test_extism.py | 3 +- runtime/src/internal.rs | 9 ++- runtime/src/memory.rs | 6 +- runtime/src/plugin.rs | 68 +++++++++++++++++-- zig/src/utils.zig | 30 +++----- 8 files changed, 87 insertions(+), 40 deletions(-) diff --git a/java/src/test/java/org/extism/sdk/PluginTests.java b/java/src/test/java/org/extism/sdk/PluginTests.java index 5e91666..3923c85 100644 --- a/java/src/test/java/org/extism/sdk/PluginTests.java +++ b/java/src/test/java/org/extism/sdk/PluginTests.java @@ -20,10 +20,10 @@ public class PluginTests { @Test public void shouldInvokeFunctionWithMemoryOptions() { - //FIXME check whether memory options are effective var manifest = new Manifest(List.of(CODE.pathWasmSource()), new MemoryOptions(0)); - var output = Extism.invokeFunction(manifest, "count_vowels", "Hello World"); - assertThat(output).isEqualTo("{\"count\": 3}"); + assertThrows(ExtismException.class, () -> { + Extism.invokeFunction(manifest, "count_vowels", "Hello World"); + }); } @Test diff --git a/php/example/index.php b/php/example/index.php index 84e6c82..95aa9c7 100644 --- a/php/example/index.php +++ b/php/example/index.php @@ -2,9 +2,8 @@ require_once __DIR__ . '/vendor/autoload.php'; -$ctx = new \Extism\Context(); $wasm = file_get_contents("../../wasm/code.wasm"); -$plugin = new \Extism\Plugin($ctx, $wasm); +$plugin = new \Extism\Plugin($wasm); $output = $plugin->call("count_vowels", "this is an example"); $json = json_decode(pack('C*', ...$output)); diff --git a/python/example.py b/python/example.py index a772a0f..2104527 100644 --- a/python/example.py +++ b/python/example.py @@ -33,7 +33,7 @@ def main(args): ) wasm = wasm_file_path.read_bytes() hash = hashlib.sha256(wasm).hexdigest() - manifest = {"wasm": [{"data": wasm, "hash": hash}], "memory": {"max": 5}} + manifest = {"wasm": [{"data": wasm, "hash": hash}]} functions = [ Function( diff --git a/python/tests/test_extism.py b/python/tests/test_extism.py index f9cffa6..fd964f1 100644 --- a/python/tests/test_extism.py +++ b/python/tests/test_extism.py @@ -115,14 +115,13 @@ class TestExtism(unittest.TestCase): def _manifest(self, functions=False): wasm = self._count_vowels_wasm(functions) hash = hashlib.sha256(wasm).hexdigest() - return {"wasm": [{"data": wasm, "hash": hash}], "memory": {"max_pages": 5}} + return {"wasm": [{"data": wasm, "hash": hash}]} def _loop_manifest(self): wasm = self._infinite_loop_wasm() hash = hashlib.sha256(wasm).hexdigest() return { "wasm": [{"data": wasm, "hash": hash}], - "memory": {"max_pages": 5}, "timeout_ms": 1000, } diff --git a/runtime/src/internal.rs b/runtime/src/internal.rs index bf47a79..93654aa 100644 --- a/runtime/src/internal.rs +++ b/runtime/src/internal.rs @@ -30,6 +30,8 @@ pub struct Internal { /// A pointer to the plugin memory, this should mostly be used from the PDK pub memory: *mut PluginMemory, + + pub(crate) available_pages: Option, } /// InternalExt provides a unified way of acessing `memory`, `store` and `internal` values @@ -67,7 +69,11 @@ pub struct Wasi { } impl Internal { - pub(crate) fn new(manifest: &Manifest, wasi: bool) -> Result { + pub(crate) fn new( + manifest: &Manifest, + wasi: bool, + available_pages: Option, + ) -> Result { let wasi = if wasi { let auth = wasmtime_wasi::ambient_authority(); let mut ctx = wasmtime_wasi::WasiCtxBuilder::new(); @@ -107,6 +113,7 @@ impl Internal { http_status: 0, last_error: std::cell::RefCell::new(None), vars: BTreeMap::new(), + available_pages, }) } diff --git a/runtime/src/memory.rs b/runtime/src/memory.rs index 98d62fe..ac453f1 100644 --- a/runtime/src/memory.rs +++ b/runtime/src/memory.rs @@ -86,12 +86,10 @@ impl PluginMemory { if let Some(store) = self.store.take() { let engine = store.engine().clone(); let internal = store.into_data(); + let pages = internal.available_pages; let mut store = Store::new(&engine, internal); store.epoch_deadline_callback(|_internal| Err(Error::msg("timeout"))); - self.memory = Memory::new( - &mut store, - MemoryType::new(4, self.manifest.as_ref().memory.max_pages), - )?; + self.memory = Memory::new(&mut store, MemoryType::new(2, pages))?; self.store = Some(store); } diff --git a/runtime/src/plugin.rs b/runtime/src/plugin.rs index cbebc14..9d2a15f 100644 --- a/runtime/src/plugin.rs +++ b/runtime/src/plugin.rs @@ -55,6 +55,56 @@ fn profiling_strategy() -> ProfilingStrategy { } } +fn calculate_available_memory( + available_pages: &mut Option, + modules: &BTreeMap, +) -> anyhow::Result<()> { + let available_pages = match available_pages { + Some(p) => p, + None => return Ok(()), + }; + + let max_pages = *available_pages; + let mut fail_memory_check = false; + let mut total_memory_needed = 0; + for (name, module) in modules.iter() { + let mut memories = 0; + for export in module.exports() { + if let Some(memory) = export.ty().memory() { + let memory_max = memory.maximum(); + match memory_max { + None => anyhow::bail!("Unbounded memory in module {name}, when `memory.max_pages` is set in the manifest all modules \ + must have a maximum bound set on an exported memory"), + Some(m) => { + total_memory_needed += m; + if !fail_memory_check { + continue + } + + *available_pages = available_pages.saturating_sub(m as u32); + if *available_pages == 0 { + fail_memory_check = true; + } + }, + } + memories += 1; + } + } + + if memories == 0 { + anyhow::bail!("No memory exported from module {name}, when `memory.max_pages` is set in the manifest all modules must \ + have a maximum bound set on an exported memory"); + } + } + + if fail_memory_check { + anyhow::bail!("Not enough memory configured to run the provided plugin, `memory.max_pages` is set to {max_pages} in the manifest \ + but {total_memory_needed} pages are needed by the plugin"); + } + + Ok(()) +} + impl Plugin { /// Create a new plugin from the given WASM code pub fn new<'a>( @@ -72,14 +122,22 @@ impl Plugin { )?; let mut imports = imports.into_iter(); let (manifest, modules) = Manifest::new(&engine, wasm.as_ref())?; - let mut store = Store::new(&engine, Internal::new(&manifest, with_wasi)?); + + // Calculate how much memory is available based on the value of `max_pages` and the exported + // memory of the modules. An error will be returned if a module doesn't have an exported memory + // or there is no maximum set for a module's exported memory. + let mut available_pages = manifest.as_ref().memory.max_pages; + calculate_available_memory(&mut available_pages, &modules)?; + log::trace!("Available pages: {available_pages:?}"); + + let mut store = Store::new( + &engine, + Internal::new(&manifest, with_wasi, available_pages)?, + ); store.epoch_deadline_callback(|_internal| Err(Error::msg("timeout"))); // Create memory - let memory = Memory::new( - &mut store, - MemoryType::new(4, manifest.as_ref().memory.max_pages), - )?; + let memory = Memory::new(&mut store, MemoryType::new(2, available_pages))?; let mut memory = PluginMemory::new(store, memory, manifest); let mut linker = Linker::new(&engine); diff --git a/zig/src/utils.zig b/zig/src/utils.zig index 9856b67..7578cdc 100644 --- a/zig/src/utils.zig +++ b/zig/src/utils.zig @@ -81,9 +81,7 @@ fn stringify( try out_stream.writeByte('{'); var field_output = false; var child_options = options; - if (child_options.whitespace) |*child_whitespace| { - child_whitespace.indent_level += 1; - } + child_options.whitespace.indent_level += 1; inline for (S.fields) |Field| { // don't include void fields if (Field.type == void) continue; @@ -105,23 +103,17 @@ fn stringify( } else { try out_stream.writeByte(','); } - if (child_options.whitespace) |child_whitespace| { - try child_whitespace.outputIndent(out_stream); - } + try child_options.whitespace.outputIndent(out_stream); try json.encodeJsonString(Field.name, options, out_stream); try out_stream.writeByte(':'); - if (child_options.whitespace) |child_whitespace| { - if (child_whitespace.separator) { - try out_stream.writeByte(' '); - } + if (child_options.whitespace.separator) { + try out_stream.writeByte(' '); } try stringify(@field(value, Field.name), child_options, out_stream); } } if (field_output) { - if (options.whitespace) |whitespace| { - try whitespace.outputIndent(out_stream); - } + try options.whitespace.outputIndent(out_stream); } try out_stream.writeByte('}'); return; @@ -148,22 +140,16 @@ fn stringify( try out_stream.writeByte('['); var child_options = options; - if (child_options.whitespace) |*whitespace| { - whitespace.indent_level += 1; - } + child_options.whitespace.indent_level += 1; for (value, 0..) |x, i| { if (i != 0) { try out_stream.writeByte(','); } - if (child_options.whitespace) |child_whitespace| { - try child_whitespace.outputIndent(out_stream); - } + try child_options.whitespace.outputIndent(out_stream); try stringify(x, child_options, out_stream); } if (value.len != 0) { - if (options.whitespace) |whitespace| { - try whitespace.outputIndent(out_stream); - } + try options.whitespace.outputIndent(out_stream); } try out_stream.writeByte(']'); return;