Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0202878

Browse files
Check returns in untyped host functions (#27)
--------- Signed-off-by: Henry Gressmann <[email protected]> Co-authored-by: Henry Gressmann <[email protected]>
1 parent 5797ee7 commit 0202878

File tree

8 files changed

+211
-8
lines changed

8 files changed

+211
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
### Fixed
2020

2121
- Fixed archive **no_std** support which was broken in the previous release, and added more tests to ensure it stays working
22+
- Check returns in untyped host functions ([#27](https://github.com/explodingcamera/tinywasm/pull/27)) (thanks [@WhaleKit](https://github.com/WhaleKit))
2223

2324
## [0.8.0] - 2024-08-29
2425

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/tinywasm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ libm={version="0.2", default-features=false}
2323
wasm-testsuite={version="0.4.0"}
2424
indexmap="2.7"
2525
wast={workspace=true}
26+
wat={workspace=true}
2627
eyre={workspace=true}
2728
pretty_env_logger={workspace=true}
2829
criterion={workspace=true}

crates/tinywasm/src/error.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use alloc::string::{String, ToString};
2+
use alloc::vec::Vec;
23
use core::{fmt::Display, ops::ControlFlow};
34
use tinywasm_types::FuncType;
45

@@ -20,8 +21,13 @@ pub enum Error {
2021
/// An unknown error occurred
2122
Other(String),
2223

23-
/// A function did not return a value
24-
FuncDidNotReturn,
24+
/// A host function returned an invalid value
25+
InvalidHostFnReturn {
26+
/// The expected type
27+
expected: FuncType,
28+
/// The actual value
29+
actual: Vec<tinywasm_types::WasmValue>,
30+
},
2531

2632
/// An invalid label type was encountered
2733
InvalidLabelType,
@@ -183,7 +189,9 @@ impl Display for Error {
183189
Self::InvalidLabelType => write!(f, "invalid label type"),
184190
Self::Other(message) => write!(f, "unknown error: {message}"),
185191
Self::UnsupportedFeature(feature) => write!(f, "unsupported feature: {feature}"),
186-
Self::FuncDidNotReturn => write!(f, "function did not return"),
192+
Self::InvalidHostFnReturn { expected, actual } => {
193+
write!(f, "invalid host function return: expected={expected:?}, actual={actual:?}")
194+
}
187195
Self::InvalidStore => write!(f, "invalid store"),
188196
}
189197
}

crates/tinywasm/src/func.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ impl FuncHandle {
5151
let func_inst = store.get_func(self.addr);
5252
let wasm_func = match &func_inst.func {
5353
Function::Host(host_func) => {
54-
let func = &host_func.clone().func;
54+
let host_func = host_func.clone();
5555
let ctx = FuncContext { store, module_addr: self.module_addr };
56-
return (func)(ctx, params);
56+
return host_func.call(ctx, params);
5757
}
5858
Function::Wasm(wasm_func) => wasm_func,
5959
};

crates/tinywasm/src/imports.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,26 @@ impl Extern {
139139
ty: &tinywasm_types::FuncType,
140140
func: impl Fn(FuncContext<'_>, &[WasmValue]) -> Result<Vec<WasmValue>> + 'static,
141141
) -> Self {
142-
Self::Function(Function::Host(Rc::new(HostFunction { func: Box::new(func), ty: ty.clone() })))
142+
let _ty = ty.clone();
143+
let inner_func = move |ctx: FuncContext<'_>, args: &[WasmValue]| -> Result<Vec<WasmValue>> {
144+
let _ty = _ty.clone();
145+
let result = func(ctx, args)?;
146+
147+
if result.len() != _ty.results.len() {
148+
return Err(crate::Error::InvalidHostFnReturn { expected: _ty.clone(), actual: result });
149+
};
150+
151+
result.iter().zip(_ty.results.iter()).try_for_each(|(val, ty)| {
152+
if val.val_type() != *ty {
153+
return Err(crate::Error::InvalidHostFnReturn { expected: _ty.clone(), actual: result.clone() });
154+
}
155+
Ok(())
156+
})?;
157+
158+
Ok(result)
159+
};
160+
161+
Self::Function(Function::Host(Rc::new(HostFunction { func: Box::new(inner_func), ty: ty.clone() })))
143162
}
144163

145164
/// Create a new typed function import

crates/tinywasm/src/interpreter/executor.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ impl<'store, 'stack> Executor<'store, 'stack> {
331331
let func = &host_func.clone();
332332
let params = self.stack.values.pop_params(&host_func.ty.params);
333333
let res =
334-
(func.func)(FuncContext { store: self.store, module_addr: self.module.id() }, &params).to_cf()?;
334+
func.call(FuncContext { store: self.store, module_addr: self.module.id() }, &params).to_cf()?;
335335
self.stack.values.extend_from_wasmvalues(&res);
336336
self.cf.incr_instr_ptr();
337337
return ControlFlow::Continue(());
@@ -370,7 +370,7 @@ impl<'store, 'stack> Executor<'store, 'stack> {
370370
let host_func = host_func.clone();
371371
let params = self.stack.values.pop_params(&host_func.ty.params);
372372
let res =
373-
match (host_func.func)(FuncContext { store: self.store, module_addr: self.module.id() }, &params) {
373+
match host_func.call(FuncContext { store: self.store, module_addr: self.module.id() }, &params) {
374374
Ok(res) => res,
375375
Err(e) => return ControlFlow::Break(Some(e)),
376376
};
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
use eyre::Result;
2+
use std::fmt::Write;
3+
use tinywasm::{
4+
types::{FuncType, ValType, WasmValue},
5+
Extern, FuncContext, Imports, Module, Store,
6+
};
7+
8+
const VAL_LISTS: &[&[WasmValue]] = &[
9+
&[],
10+
&[WasmValue::I32(0)],
11+
&[WasmValue::I32(0), WasmValue::I32(0)], // 2 of the same
12+
&[WasmValue::I32(0), WasmValue::I32(0), WasmValue::F64(0.0)], // add another type
13+
&[WasmValue::I32(0), WasmValue::F64(0.0), WasmValue::I32(0)], // reorder
14+
&[WasmValue::RefExtern(0), WasmValue::F64(0.0), WasmValue::I32(0)], // all different types
15+
];
16+
// (f64, i32, i32) and (f64) can be used to "match_none"
17+
18+
fn get_type_lists() -> impl Iterator<Item = impl Iterator<Item = ValType> + Clone> + Clone {
19+
VAL_LISTS.iter().map(|l| l.iter().map(WasmValue::val_type))
20+
}
21+
fn get_modules() -> Vec<(Module, FuncType, Vec<WasmValue>)> {
22+
let mut result = Vec::<(Module, FuncType, Vec<WasmValue>)>::new();
23+
let val_and_tys = get_type_lists().zip(VAL_LISTS);
24+
for res_types in get_type_lists() {
25+
for (arg_types, arg_vals) in val_and_tys.clone() {
26+
let ty = FuncType { results: res_types.clone().collect(), params: arg_types.collect() };
27+
result.push((proxy_module(&ty), ty, arg_vals.to_vec()));
28+
}
29+
}
30+
result
31+
}
32+
33+
#[test]
34+
fn test_return_invalid_type() -> Result<()> {
35+
// try to return from host functions types that don't match their signatures
36+
let mod_list = get_modules();
37+
38+
for (module, func_ty, test_args) in mod_list {
39+
for result_to_try in VAL_LISTS {
40+
println!("trying");
41+
let mut store = Store::default();
42+
let mut imports = Imports::new();
43+
imports
44+
.define("host", "hfn", Extern::func(&func_ty, |_: FuncContext<'_>, _| Ok(result_to_try.to_vec())))
45+
.unwrap();
46+
47+
let instance = module.clone().instantiate(&mut store, Some(imports)).unwrap();
48+
let caller = instance.exported_func_untyped(&store, "call_hfn").unwrap();
49+
let res_types_returned = result_to_try.iter().map(WasmValue::val_type);
50+
dbg!(&res_types_returned, &func_ty);
51+
let res_types_expected = &func_ty.results;
52+
let should_succeed = res_types_returned.eq(res_types_expected.iter().cloned());
53+
// Extern::func that returns wrong type(s) can only be detected when it runs
54+
let call_res = caller.call(&mut store, &test_args);
55+
dbg!(&call_res);
56+
assert_eq!(call_res.is_ok(), should_succeed);
57+
println!("this time ok");
58+
}
59+
}
60+
Ok(())
61+
}
62+
63+
#[test]
64+
fn test_linking_invalid_untyped_func() -> Result<()> {
65+
// try to import host functions with function types no matching those expected by modules
66+
let mod_list = get_modules();
67+
for (module, actual_func_ty, _) in &mod_list {
68+
for (_, func_ty_to_try, _) in &mod_list {
69+
let tried_fn = Extern::func(func_ty_to_try, |_: FuncContext<'_>, _| panic!("not intended to be called"));
70+
let mut store = Store::default();
71+
let mut imports = Imports::new();
72+
imports.define("host", "hfn", tried_fn).unwrap();
73+
74+
let should_succeed = func_ty_to_try == actual_func_ty;
75+
let link_res = module.clone().instantiate(&mut store, Some(imports));
76+
77+
assert_eq!(link_res.is_ok(), should_succeed);
78+
}
79+
}
80+
Ok(())
81+
}
82+
83+
#[test]
84+
fn test_linking_invalid_typed_func() -> Result<()> {
85+
type Existing = (i32, i32, f64);
86+
type NonMatchingOne = f64;
87+
type NonMatchingMul = (f64, i32, i32);
88+
const DONT_CALL: &str = "not meant to be called";
89+
90+
// they don't match any signature from get_modules()
91+
#[rustfmt::skip] // to make it table-like
92+
let matching_none= &[
93+
Extern::typed_func(|_, _: NonMatchingMul| -> tinywasm::Result<Existing> { panic!("{DONT_CALL}") } ),
94+
Extern::typed_func(|_, _: NonMatchingMul| -> tinywasm::Result<()> { panic!("{DONT_CALL}") } ),
95+
Extern::typed_func(|_, _: NonMatchingOne| -> tinywasm::Result<Existing> { panic!("{DONT_CALL}") } ),
96+
Extern::typed_func(|_, _: NonMatchingOne| -> tinywasm::Result<()> { panic!("{DONT_CALL}") } ),
97+
Extern::typed_func(|_, _: Existing | -> tinywasm::Result<NonMatchingMul> { panic!("{DONT_CALL}") } ),
98+
Extern::typed_func(|_, _: Existing | -> tinywasm::Result<NonMatchingOne> { panic!("{DONT_CALL}") } ),
99+
Extern::typed_func(|_, _: () | -> tinywasm::Result<NonMatchingOne> { panic!("{DONT_CALL}") } ),
100+
Extern::typed_func(|_, _: () | -> tinywasm::Result<NonMatchingMul> { panic!("{DONT_CALL}") } ),
101+
Extern::typed_func(|_, _: NonMatchingOne| -> tinywasm::Result<NonMatchingMul> { panic!("{DONT_CALL}") } ),
102+
Extern::typed_func(|_, _: NonMatchingOne| -> tinywasm::Result<NonMatchingOne> { panic!("{DONT_CALL}") } ),
103+
];
104+
105+
let mod_list = get_modules();
106+
for (module, _, _) in mod_list {
107+
for typed_fn in matching_none.clone() {
108+
let mut store = Store::default();
109+
let mut imports = Imports::new();
110+
imports.define("host", "hfn", typed_fn).unwrap();
111+
let link_failure = module.clone().instantiate(&mut store, Some(imports));
112+
link_failure.expect_err("no func in matching_none list should link to any mod");
113+
}
114+
}
115+
116+
// the valid cases are well-checked in other tests
117+
Ok(())
118+
}
119+
120+
fn to_name(ty: &ValType) -> &str {
121+
match ty {
122+
ValType::I32 => "i32",
123+
ValType::I64 => "i64",
124+
ValType::F32 => "f32",
125+
ValType::F64 => "f64",
126+
ValType::V128 => "v128",
127+
ValType::RefFunc => "funcref",
128+
ValType::RefExtern => "externref",
129+
}
130+
}
131+
132+
// make a module with imported function {module:"host", name:"hfn"} that takes specified results and returns specified params
133+
// and 2 wasm functions: call_hfn takes params, passes them to hfn and returns it's results
134+
// and 2 wasm functions: call_hfn_discard takes params, passes them to hfn and drops it's results
135+
fn proxy_module(func_ty: &FuncType) -> Module {
136+
let results = func_ty.results.as_ref();
137+
let params = func_ty.params.as_ref();
138+
let join_surround = |list: &[ValType], keyword| {
139+
if list.is_empty() {
140+
return "".to_string();
141+
}
142+
let step = list.iter().map(|ty| format!("{} ", to_name(ty)).to_string()).collect::<String>();
143+
format!("({keyword} {step})")
144+
};
145+
146+
let results_text = join_surround(results, "result");
147+
let params_text = join_surround(params, "param");
148+
149+
// let params_gets: String = params.iter().enumerate().map(|(num, _)| format!("(local.get {num})\n")).collect();
150+
let params_gets: String = params.iter().enumerate().fold(String::new(), |mut acc, (num, _)| {
151+
let _ = writeln!(acc, "(local.get {num})", num = num);
152+
acc
153+
});
154+
155+
let result_drops = "(drop)\n".repeat(results.len()).to_string();
156+
let wasm_text = format!(
157+
r#"(module
158+
(import "host" "hfn" (func $host_fn {params_text} {results_text}))
159+
(func (export "call_hfn") {params_text} {results_text}
160+
{params_gets}
161+
(call $host_fn)
162+
)
163+
(func (export "call_hfn_discard") {params_text}
164+
{params_gets}
165+
(call $host_fn)
166+
{result_drops}
167+
)
168+
)
169+
"#
170+
);
171+
let wasm = wat::parse_str(wasm_text).expect("failed to parse wat");
172+
Module::parse_bytes(&wasm).expect("failed to make module")
173+
}

0 commit comments

Comments
 (0)