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`
This commit is contained in:
zach
2023-06-01 09:37:42 -07:00
committed by GitHub
parent 3bdf4ef0d0
commit 360df45e1a
8 changed files with 87 additions and 40 deletions

View File

@@ -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

View File

@@ -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));

View File

@@ -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(

View File

@@ -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,
}

View File

@@ -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<u32>,
}
/// 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<Self, Error> {
pub(crate) fn new(
manifest: &Manifest,
wasi: bool,
available_pages: Option<u32>,
) -> Result<Self, Error> {
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,
})
}

View File

@@ -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);
}

View File

@@ -55,6 +55,56 @@ fn profiling_strategy() -> ProfilingStrategy {
}
}
fn calculate_available_memory(
available_pages: &mut Option<u32>,
modules: &BTreeMap<String, Module>,
) -> 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);

View File

@@ -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;