Compare commits

..

4 Commits

Author SHA1 Message Date
zach
5ff515d3a0 feat: allow readonly paths in manifest 2024-02-26 16:10:33 -08:00
zach
10ee81952b chore: update to latest wasmtime 2024-02-26 16:06:10 -08:00
zach
fb3c131a1e fix: require wasmtime 17 or greater 2024-02-26 16:06:10 -08:00
zach
9e586a36d9 feat: use wasi preview2, support wasi tcp/udp for allowed_hosts 2024-02-26 16:06:09 -08:00
14 changed files with 109 additions and 174 deletions

View File

@@ -8,7 +8,7 @@
[![Discord](https://img.shields.io/discord/1011124058408112148?color=%23404eed&label=Community%20Chat&logo=Discord&logoColor=%23404eed)](https://extism.org/discord)
![GitHub Org's stars](https://img.shields.io/github/stars/extism)
![Downloads](https://img.shields.io/crates/d/extism)
![GitHub all releases](https://img.shields.io/github/downloads/extism/extism/total)
![GitHub License](https://img.shields.io/github/license/extism/extism)
![GitHub release (with filter)](https://img.shields.io/github/v/release/extism/extism)

View File

@@ -11,7 +11,7 @@ description = "Traits to make Rust types usable with Extism"
[dependencies]
anyhow = "1.0.75"
base64 = "~0.22"
base64 = "~0.21"
bytemuck = {version = "1.14.0", optional = true }
prost = { version = "0.12.0", optional = true }
protobuf = { version = "3.2.0", optional = true }

View File

@@ -10,7 +10,7 @@ version.workspace = true
[dependencies]
serde = { version = "1", features = ["derive"] }
base64 = "~0.22"
base64 = "~0.21"
schemars = { version = "0.8", optional = true }
serde_json = "1"

View File

@@ -38,8 +38,7 @@
"description": "Memory options",
"default": {
"max_http_response_bytes": null,
"max_pages": null,
"max_var_bytes": null
"max_pages": null
},
"allOf": [
{
@@ -48,7 +47,7 @@
]
},
"timeout_ms": {
"description": "The plugin timeout in milliseconds",
"description": "The plugin timeout, by default this is set to 30s",
"default": null,
"type": [
"integer",
@@ -90,16 +89,6 @@
],
"format": "uint32",
"minimum": 0.0
},
"max_var_bytes": {
"description": "The maximum number of bytes allowed to be used by plugin vars. Setting this to 0 will disable Extism vars. The default value is 1mb.",
"default": 1048576,
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false

View File

@@ -16,40 +16,6 @@ pub struct MemoryOptions {
/// The maximum number of bytes allowed in an HTTP response
#[serde(default)]
pub max_http_response_bytes: Option<u64>,
/// The maximum number of bytes allowed to be used by plugin vars. Setting this to 0
/// will disable Extism vars. The default value is 1mb.
#[serde(default = "default_var_bytes")]
pub max_var_bytes: Option<u64>,
}
impl MemoryOptions {
/// Create an empty `MemoryOptions` value
pub fn new() -> Self {
Default::default()
}
/// Set max pages
pub fn with_max_pages(mut self, pages: u32) -> Self {
self.max_pages = Some(pages);
self
}
/// Set max HTTP response size
pub fn with_max_http_response_bytes(mut self, bytes: u64) -> Self {
self.max_http_response_bytes = Some(bytes);
self
}
/// Set max size of Extism vars
pub fn with_max_var_bytes(mut self, bytes: u64) -> Self {
self.max_var_bytes = Some(bytes);
self
}
}
fn default_var_bytes() -> Option<u64> {
Some(1024 * 1024)
}
/// Generic HTTP request structure
@@ -283,7 +249,13 @@ pub struct Manifest {
#[serde(default)]
pub allowed_paths: Option<BTreeMap<PathBuf, PathBuf>>,
/// The plugin timeout in milliseconds
/// Specifies which paths should be made for reading only when using WASI. This is a mapping from
/// the path on disk to the path it should be available inside the plugin.
/// For example, `".": "/tmp"` would mount the current directory as `/tmp` inside the module
#[serde(default)]
pub allowed_paths_readonly: Option<BTreeMap<PathBuf, PathBuf>>,
/// The plugin timeout
#[serde(default)]
pub timeout_ms: Option<u64>,
}

View File

@@ -9,8 +9,8 @@ repository.workspace = true
version.workspace = true
[dependencies]
wasmtime = ">= 14.0.0, < 18.0.0"
wasmtime-wasi = ">= 14.0.0, < 18.0.0"
wasmtime = ">= 18.0.0, < 19.0.0"
wasmtime-wasi = ">= 18.0.0, < 19.0.0"
anyhow = "1"
serde = {version = "1", features = ["derive"]}
serde_json = "1"

View File

@@ -1,3 +1,6 @@
use wasmtime::component::ResourceTable;
use wasmtime_wasi::preview2::{preview1::WasiPreview1Adapter, DirPerms, FilePerms};
use crate::*;
/// CurrentPlugin stores data that is available to the caller in PDK functions, this should
@@ -18,6 +21,7 @@ pub struct CurrentPlugin {
}
unsafe impl Send for CurrentPlugin {}
unsafe impl Sync for CurrentPlugin {}
pub(crate) struct MemoryLimiter {
bytes_left: usize,
@@ -289,30 +293,62 @@ impl CurrentPlugin {
) -> Result<Self, Error> {
let wasi = if wasi {
let auth = wasmtime_wasi::ambient_authority();
let mut ctx = wasmtime_wasi::WasiCtxBuilder::new();
for (k, v) in manifest.config.iter() {
ctx.env(k, v)?;
}
let mut ctx = wasmtime_wasi::preview2::WasiCtxBuilder::new();
ctx.allow_ip_name_lookup(true);
ctx.allow_tcp(true);
ctx.allow_udp(true);
if let Some(a) = &manifest.allowed_paths {
for (k, v) in a.iter() {
let d = wasmtime_wasi::Dir::open_ambient_dir(k, auth).map_err(|err| {
Error::msg(format!(
"Unable to preopen directory \"{}\": {}",
k.display(),
err.kind()
))
})?;
ctx.preopened_dir(d, v)?;
if k.as_path().is_dir() {
let d = wasmtime_wasi::Dir::open_ambient_dir(k, auth)?;
ctx.preopened_dir(
d,
DirPerms::READ | DirPerms::MUTATE,
FilePerms::READ | FilePerms::WRITE,
v.to_string_lossy(),
);
}
}
}
if let Some(a) = &manifest.allowed_paths_readonly {
for (k, v) in a.iter() {
if k.as_path().is_dir() {
let d = wasmtime_wasi::Dir::open_ambient_dir(k, auth)?;
ctx.preopened_dir(d, DirPerms::READ, FilePerms::READ, v.to_string_lossy());
}
}
}
if let Some(h) = &manifest.allowed_hosts {
let h = h.clone();
ctx.socket_addr_check(move |addr, _kind| {
for host in h.iter() {
let addrs = std::net::ToSocketAddrs::to_socket_addrs(&host);
if let Ok(addrs) = addrs {
for a in addrs.into_iter() {
if addr == &a {
return true;
}
}
}
}
false
});
}
// Enable WASI output, typically used for debugging purposes
if std::env::var("EXTISM_ENABLE_WASI_OUTPUT").is_ok() {
ctx.inherit_stdout().inherit_stderr();
}
Some(Wasi { ctx: ctx.build() })
Some(Wasi {
ctx: ctx.build(),
preview2_table: ResourceTable::new(),
preview1_adapter: WasiPreview1Adapter::new(),
})
} else {
None
};

View File

@@ -71,9 +71,7 @@ pub struct CPtr {
/// UserDataHandle is an untyped version of `UserData` that is stored inside `Function` to keep a live reference.
#[derive(Clone)]
pub(crate) enum UserDataHandle {
#[allow(dead_code)]
C(Arc<CPtr>),
#[allow(dead_code)]
Rust(Arc<std::sync::Mutex<dyn std::any::Any>>),
}

View File

@@ -3,7 +3,29 @@ use crate::*;
/// WASI context
pub struct Wasi {
/// wasi
pub ctx: wasmtime_wasi::WasiCtx,
pub ctx: wasmtime_wasi::preview2::WasiCtx,
pub preview2_table: wasmtime::component::ResourceTable,
pub preview1_adapter: wasmtime_wasi::preview2::preview1::WasiPreview1Adapter,
}
impl wasmtime_wasi::preview2::WasiView for CurrentPlugin {
fn table(&mut self) -> &mut wasmtime::component::ResourceTable {
&mut self.wasi.as_mut().unwrap().preview2_table
}
fn ctx(&mut self) -> &mut wasmtime_wasi::preview2::WasiCtx {
&mut self.wasi.as_mut().unwrap().ctx
}
}
impl wasmtime_wasi::preview2::preview1::WasiPreview1View for CurrentPlugin {
fn adapter(&self) -> &wasmtime_wasi::preview2::preview1::WasiPreview1Adapter {
&self.wasi.as_ref().unwrap().preview1_adapter
}
fn adapter_mut(&mut self) -> &mut wasmtime_wasi::preview2::preview1::WasiPreview1Adapter {
&mut self.wasi.as_mut().unwrap().preview1_adapter
}
}
/// InternalExt provides a unified way of acessing `memory`, `store` and `internal` values

View File

@@ -47,13 +47,9 @@ fn to_module(engine: &Engine, wasm: &extism_manifest::Wasm) -> Result<(String, M
let name = meta.name.as_deref().unwrap_or(MAIN_KEY).to_string();
// Load file
let buf = std::fs::read(path).map_err(|err| {
Error::msg(format!(
"Unable to load Wasm file \"{}\": {}",
path.display(),
err.kind()
))
})?;
let mut buf = Vec::new();
let mut file = std::fs::File::open(path)?;
file.read_to_end(&mut buf)?;
check_hash(&meta.hash, &buf)?;
Ok((name, Module::new(engine, buf)?))

View File

@@ -97,13 +97,19 @@ pub(crate) fn var_set(
) -> Result<(), Error> {
let data: &mut CurrentPlugin = caller.data_mut();
if data.manifest.memory.max_var_bytes.is_some_and(|x| x == 0) {
anyhow::bail!("Vars are disabled by this host")
let mut size = 0;
for v in data.vars.values() {
size += v.len();
}
let voffset = args!(input, 1, i64) as u64;
let key_offs = args!(input, 0, i64) as u64;
// If the store is larger than 100MB then stop adding things
if size > 1024 * 1024 * 100 && voffset != 0 {
return Err(Error::msg("Variable store is full"));
}
let key_offs = args!(input, 0, i64) as u64;
let key = {
let handle = match data.memory_handle(key_offs) {
Some(h) => h,
@@ -126,22 +132,6 @@ pub(crate) fn var_set(
None => anyhow::bail!("invalid handle offset for var value: {voffset}"),
};
let mut size = std::mem::size_of::<String>()
+ std::mem::size_of::<Vec<u8>>()
+ key.len()
+ handle.length as usize;
for (k, v) in data.vars.iter() {
size += k.len();
size += v.len();
size += std::mem::size_of::<String>() + std::mem::size_of::<Vec<u8>>();
}
// If the store is larger than the configured size, or 1mb by default, then stop adding things
if size > data.manifest.memory.max_var_bytes.unwrap_or(1024 * 1024) as usize && voffset != 0 {
return Err(Error::msg("Variable store is full"));
}
let value = data.memory_bytes(handle)?.to_vec();
// Insert the value from memory into the `vars` map
@@ -236,12 +226,11 @@ pub(crate) fn http_request(
Some(res.into_reader())
}
Err(e) => {
let msg = e.to_string();
if let Some(res) = e.into_response() {
data.http_status = res.status();
Some(res.into_reader())
} else {
return Err(Error::msg(msg));
None
}
}
};

View File

@@ -307,9 +307,7 @@ impl Plugin {
// If wasi is enabled then add it to the linker
if with_wasi {
wasmtime_wasi::add_to_linker(&mut linker, |x: &mut CurrentPlugin| {
&mut x.wasi.as_mut().unwrap().ctx
})?;
wasmtime_wasi::preview2::preview1::add_to_linker_sync(&mut linker)?;
}
for f in &mut imports {
@@ -668,12 +666,11 @@ impl Plugin {
// Implements the build of the `call` function, `raw_call` is also used in the SDK
// code
pub(crate) fn raw_call<T: 'static + Sync + Send>(
pub(crate) fn raw_call(
&mut self,
lock: &mut std::sync::MutexGuard<Option<Instance>>,
name: impl AsRef<str>,
input: impl AsRef<[u8]>,
arg: Option<T>,
) -> Result<i32, (Error, i32)> {
let name = name.as_ref();
let input = input.as_ref();
@@ -721,14 +718,7 @@ impl Plugin {
// Call the function
let mut results = vec![wasmtime::Val::null(); n_results];
let args = if func.ty(self.store()).params().count() == 0 {
vec![]
} else {
let r = arg.map(wasmtime::ExternRef::new);
vec![wasmtime::Val::ExternRef(r)]
};
let mut res = func.call(self.store_mut(), args.as_slice(), results.as_mut_slice());
let mut res = func.call(self.store_mut(), &[], results.as_mut_slice());
// Stop timer
self.store
@@ -813,12 +803,8 @@ impl Plugin {
}
let wasi_exit_code = e
.downcast_ref::<wasmtime_wasi::I32Exit>()
.map(|e| e.0)
.or_else(|| {
e.downcast_ref::<wasmtime_wasi::preview2::I32Exit>()
.map(|e| e.0)
});
.downcast_ref::<wasmtime_wasi::preview2::I32Exit>()
.map(|e| e.0);
if let Some(exit_code) = wasi_exit_code {
debug!(
plugin = self.id.to_string(),
@@ -881,21 +867,7 @@ impl Plugin {
let lock = self.instance.clone();
let mut lock = lock.lock().unwrap();
let data = input.to_bytes()?;
self.raw_call::<()>(&mut lock, name, data, None)
.map_err(|e| e.0)
.and_then(move |_| self.output())
}
pub fn call_with_arg<'a, 'b, T: ToBytes<'a>, U: FromBytes<'b>, V: 'static + Send + Sync>(
&'b mut self,
name: impl AsRef<str>,
input: T,
arg: V,
) -> Result<U, Error> {
let lock = self.instance.clone();
let mut lock = lock.lock().unwrap();
let data = input.to_bytes()?;
self.raw_call(&mut lock, name, data, Some(arg))
self.raw_call(&mut lock, name, data)
.map_err(|e| e.0)
.and_then(move |_| self.output())
}
@@ -914,7 +886,7 @@ impl Plugin {
let lock = self.instance.clone();
let mut lock = lock.lock().unwrap();
let data = input.to_bytes().map_err(|e| (e, -1))?;
self.raw_call::<()>(&mut lock, name, data, None)
self.raw_call(&mut lock, name, data)
.and_then(move |_| self.output().map_err(|e| (e, -1)))
}
@@ -1023,7 +995,7 @@ macro_rules! typed_plugin {
impl $name {
$(
pub fn $f<'a, $( $( $lt $( : $clt )? ),+ )? >(&'a mut self, input: $input) -> Result<$output, $crate::Error> {
self.0.call::<_, _>(stringify!($f), input)
self.0.call(stringify!($f), input)
}
)*
}

View File

@@ -389,20 +389,6 @@ pub unsafe extern "C" fn extism_plugin_config(
}
};
let wasi = &mut plugin.current_plugin_mut().wasi;
if let Some(Wasi { ctx, .. }) = wasi {
for (k, v) in json.iter() {
match v {
Some(v) => {
let _ = ctx.push_env(k, v);
}
None => {
let _ = ctx.push_env(k, "");
}
}
}
}
let id = plugin.id;
let config = &mut plugin.current_plugin_mut().manifest.config;
for (k, v) in json.into_iter() {
@@ -485,7 +471,7 @@ pub unsafe extern "C" fn extism_plugin_call(
name
);
let input = std::slice::from_raw_parts(data, data_len as usize);
let res = plugin.raw_call::<()>(&mut lock, name, input, None);
let res = plugin.raw_call(&mut lock, name, input);
match res {
Err((e, rc)) => plugin.return_error(&mut lock, e, rc),
@@ -671,7 +657,7 @@ unsafe fn set_log_buffer(filter: &str) -> Result<(), Error> {
/// Calls the provided callback function for each buffered log line.
/// This is only needed when `extism_log_custom` is used.
pub unsafe extern "C" fn extism_log_drain(handler: ExtismLogDrainFunctionType) {
if let Some(buf) = LOG_BUFFER.as_mut() {
if let Some(buf) = &mut LOG_BUFFER {
if let Ok(mut buf) = buf.buffer.lock() {
for (line, len) in buf.drain(..) {
handler(line.as_ptr(), len as u64);

View File

@@ -1,5 +1,3 @@
use extism_manifest::MemoryOptions;
use crate::*;
use std::{io::Write, time::Instant};
@@ -596,29 +594,6 @@ fn test_manifest_ptr_len() {
assert_eq!(count.get("count").unwrap().as_i64().unwrap(), 1);
}
#[test]
fn test_no_vars() {
let data = br#"
(module
(import "extism:host/env" "var_set" (func $var_set (param i64 i64)))
(import "extism:host/env" "input_offset" (func $input_offset (result i64)))
(func (export "test") (result i32)
(call $input_offset)
(call $input_offset)
(call $var_set)
(i32.const 0)
)
)
"#;
let manifest = Manifest::new([Wasm::data(data)])
.with_memory_options(MemoryOptions::new().with_max_var_bytes(1));
let mut plugin = Plugin::new(manifest, [], true).unwrap();
let output: Result<(), Error> = plugin.call("test", b"A".repeat(1024));
assert!(output.is_err());
let output: Result<(), Error> = plugin.call("test", vec![]);
assert!(output.is_ok());
}
#[test]
fn test_linking() {
let manifest = Manifest::new([