mirror of
https://github.com/extism/extism.git
synced 2026-01-09 13:57:55 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user