diff --git a/crates/wasmtime/src/component/func/host.rs b/crates/wasmtime/src/component/func/host.rs index 82d86b1ac8..45bbe3bd63 100644 --- a/crates/wasmtime/src/component/func/host.rs +++ b/crates/wasmtime/src/component/func/host.rs @@ -230,12 +230,15 @@ where fn validate_inbounds(memory: &[u8], ptr: &ValRaw) -> Result { // FIXME: needs memory64 support let ptr = usize::try_from(ptr.get_u32())?; + if ptr % usize::try_from(T::align())? != 0 { + bail!("pointer not aligned"); + } let end = match ptr.checked_add(T::size()) { Some(n) => n, - None => bail!("return pointer size overflow"), + None => bail!("pointer size overflow"), }; if end > memory.len() { - bail!("return pointer out of bounds") + bail!("pointer out of bounds") } Ok(ptr) } diff --git a/crates/wasmtime/src/component/func/options.rs b/crates/wasmtime/src/component/func/options.rs index 9f095f8fab..25fc61cef6 100644 --- a/crates/wasmtime/src/component/func/options.rs +++ b/crates/wasmtime/src/component/func/options.rs @@ -83,7 +83,7 @@ impl Options { // Invoke the wasm malloc function using its raw and statically known // signature. let result = unsafe { - usize::try_from(crate::TypedFunc::<(u32, u32, u32, u32), u32>::call_raw( + crate::TypedFunc::<(u32, u32, u32, u32), u32>::call_raw( store, realloc, ( @@ -92,9 +92,14 @@ impl Options { old_align, u32::try_from(new_size)?, ), - )?)? + )? }; + if result % old_align != 0 { + bail!("realloc return: result not aligned"); + } + let result = usize::try_from(result)?; + let memory = self.memory_mut(store.0); let result_slice = match memory.get_mut(result..).and_then(|s| s.get_mut(..new_size)) { diff --git a/crates/wasmtime/src/component/func/typed.rs b/crates/wasmtime/src/component/func/typed.rs index 9356f6f656..5842bc2627 100644 --- a/crates/wasmtime/src/component/func/typed.rs +++ b/crates/wasmtime/src/component/func/typed.rs @@ -236,7 +236,11 @@ where fn lift_heap_result(store: &StoreOpaque, options: &Options, dst: &ValRaw) -> Result { assert!(Return::flatten_count() > MAX_STACK_RESULTS); // FIXME: needs to read an i64 for memory64 - let ptr = usize::try_from(dst.get_u32()).unwrap(); + let ptr = usize::try_from(dst.get_u32())?; + if ptr % usize::try_from(Return::align())? != 0 { + bail!("return pointer not aligned"); + } + let memory = Memory::new(store, options); let bytes = memory .as_slice() @@ -704,6 +708,7 @@ macro_rules! integers { } fn store(&self, memory: &mut MemoryMut<'_, T>, offset: usize) -> Result<()> { + debug_assert!(offset % Self::size() == 0); *memory.get(offset) = self.to_le_bytes(); Ok(()) } @@ -717,6 +722,7 @@ macro_rules! integers { #[inline] fn load(_mem: &Memory<'_>, bytes: &[u8]) -> Result { + debug_assert!((bytes.as_ptr() as usize) % Self::size() == 0); Ok($primitive::from_le_bytes(bytes.try_into().unwrap())) } } @@ -780,6 +786,7 @@ macro_rules! floats { } fn store(&self, memory: &mut MemoryMut<'_, T>, offset: usize) -> Result<()> { + debug_assert!(offset % Self::size() == 0); let ptr = memory.get(offset); *ptr = canonicalize(*self).to_bits().to_le_bytes(); Ok(()) @@ -794,6 +801,7 @@ macro_rules! floats { #[inline] fn load(_mem: &Memory<'_>, bytes: &[u8]) -> Result { + debug_assert!((bytes.as_ptr() as usize) % Self::size() == 0); Ok(canonicalize($float::from_le_bytes(bytes.try_into().unwrap()))) } } @@ -838,6 +846,7 @@ unsafe impl Lower for bool { } fn store(&self, memory: &mut MemoryMut<'_, T>, offset: usize) -> Result<()> { + debug_assert!(offset % Self::size() == 0); memory.get::<1>(offset)[0] = *self as u8; Ok(()) } @@ -894,6 +903,7 @@ unsafe impl Lower for char { } fn store(&self, memory: &mut MemoryMut<'_, T>, offset: usize) -> Result<()> { + debug_assert!(offset % Self::size() == 0); *memory.get::<4>(offset) = u32::from(*self).to_le_bytes(); Ok(()) } @@ -907,6 +917,7 @@ unsafe impl Lift for char { #[inline] fn load(_memory: &Memory<'_>, bytes: &[u8]) -> Result { + debug_assert!((bytes.as_ptr() as usize) % Self::size() == 0); let bits = u32::from_le_bytes(bytes.try_into().unwrap()); Ok(char::try_from(bits)?) } @@ -948,6 +959,7 @@ unsafe impl Lower for str { } fn store(&self, mem: &mut MemoryMut<'_, T>, offset: usize) -> Result<()> { + debug_assert!(offset % (Self::align() as usize) == 0); let (ptr, len) = lower_string(mem, self)?; // FIXME: needs memory64 handling *mem.get(offset + 0) = (ptr as i32).to_le_bytes(); @@ -1109,6 +1121,7 @@ unsafe impl Lift for WasmStr { } fn load(memory: &Memory<'_>, bytes: &[u8]) -> Result { + debug_assert!((bytes.as_ptr() as usize) % (Self::align() as usize) == 0); // FIXME: needs memory64 treatment let ptr = u32::from_le_bytes(bytes[..4].try_into().unwrap()); let len = u32::from_le_bytes(bytes[4..].try_into().unwrap()); @@ -1160,6 +1173,7 @@ where } fn store(&self, mem: &mut MemoryMut<'_, U>, offset: usize) -> Result<()> { + debug_assert!(offset % (Self::align() as usize) == 0); let (ptr, len) = lower_list(mem, self)?; *mem.get(offset + 0) = (ptr as i32).to_le_bytes(); *mem.get(offset + 4) = (len as i32).to_le_bytes(); @@ -1225,6 +1239,9 @@ impl WasmList { Some(n) if n <= memory.as_slice().len() => {} _ => bail!("list pointer/length out of bounds of memory"), } + if ptr % usize::try_from(T::align())? != 0 { + bail!("list pointer is not aligned") + } Ok(WasmList { ptr, len, @@ -1324,6 +1341,7 @@ unsafe impl Lift for WasmList { } fn load(memory: &Memory<'_>, bytes: &[u8]) -> Result { + debug_assert!((bytes.as_ptr() as usize) % (Self::align() as usize) == 0); // FIXME: needs memory64 treatment let ptr = u32::from_le_bytes(bytes[..4].try_into().unwrap()); let len = u32::from_le_bytes(bytes[4..].try_into().unwrap()); @@ -1432,6 +1450,7 @@ where } fn store(&self, mem: &mut MemoryMut<'_, U>, offset: usize) -> Result<()> { + debug_assert!(offset % (Self::align() as usize) == 0); match self { None => { mem.get::<1>(offset)[0] = 0; @@ -1458,6 +1477,7 @@ where } fn load(memory: &Memory<'_>, bytes: &[u8]) -> Result { + debug_assert!((bytes.as_ptr() as usize) % (Self::align() as usize) == 0); let discrim = bytes[0]; let payload = &bytes[align_to(1, T::align())..]; match discrim { @@ -1555,6 +1575,7 @@ where } fn store(&self, mem: &mut MemoryMut<'_, U>, offset: usize) -> Result<()> { + debug_assert!(offset % (Self::align() as usize) == 0); match self { Ok(e) => { mem.get::<1>(offset)[0] = 0; @@ -1602,7 +1623,8 @@ where } fn load(memory: &Memory<'_>, bytes: &[u8]) -> Result { - let align = as ComponentType>::align(); + debug_assert!((bytes.as_ptr() as usize) % (Self::align() as usize) == 0); + let align = Self::align(); let discrim = bytes[0]; let payload = &bytes[align_to(1, align)..]; match discrim { @@ -1674,9 +1696,8 @@ macro_rules! impl_component_ty_for_tuples { } fn store(&self, memory: &mut MemoryMut<'_, U>, mut offset: usize) -> Result<()> { + debug_assert!(offset % (Self::align() as usize) == 0); let ($($t,)*) = self; - // TODO: this requires that `offset` is aligned which we may not - // want to do $($t.store(memory, next_field::<$t>(&mut offset))?;)* Ok(()) } @@ -1691,6 +1712,7 @@ macro_rules! impl_component_ty_for_tuples { } fn load(memory: &Memory<'_>, bytes: &[u8]) -> Result { + debug_assert!((bytes.as_ptr() as usize) % (Self::align() as usize) == 0); let mut offset = 0; $(let $t = $t::load(memory, &bytes[next_field::<$t>(&mut offset)..][..$t::size()])?;)* Ok(($($t,)*)) diff --git a/tests/all/component_model.rs b/tests/all/component_model.rs index 4d855e7c4a..54a86999e6 100644 --- a/tests/all/component_model.rs +++ b/tests/all/component_model.rs @@ -5,6 +5,67 @@ use wasmtime::{Config, Engine}; mod func; mod import; +// A simple bump allocator which can be used with modules +const REALLOC_AND_FREE: &str = r#" + (global $last (mut i32) (i32.const 8)) + (func $realloc (export "canonical_abi_realloc") + (param $old_ptr i32) + (param $old_size i32) + (param $align i32) + (param $new_size i32) + (result i32) + + ;; Test if the old pointer is non-null + local.get $old_ptr + if + ;; If the old size is bigger than the new size then + ;; this is a shrink and transparently allow it + local.get $old_size + local.get $new_size + i32.gt_u + if + local.get $old_ptr + return + end + + ;; ... otherwise this is unimplemented + unreachable + end + + ;; align up `$last` + (global.set $last + (i32.and + (i32.add + (global.get $last) + (i32.add + (local.get $align) + (i32.const -1))) + (i32.xor + (i32.add + (local.get $align) + (i32.const -1)) + (i32.const -1)))) + + ;; save the current value of `$last` as the return value + global.get $last + + ;; ensure anything necessary is set to valid data by spraying a bit + ;; pattern that is invalid + global.get $last + i32.const 0xde + local.get $new_size + memory.fill + + ;; bump our pointer + (global.set $last + (i32.add + (global.get $last) + (local.get $new_size))) + ) + + (func (export "canonical_abi_free") (param i32 i32 i32)) +"#; + fn engine() -> Engine { let mut config = Config::new(); config.wasm_component_model(true); diff --git a/tests/all/component_model/func.rs b/tests/all/component_model/func.rs index 220679e630..c3a6e1e6ce 100644 --- a/tests/all/component_model/func.rs +++ b/tests/all/component_model/func.rs @@ -1,3 +1,4 @@ +use super::REALLOC_AND_FREE; use anyhow::Result; use std::rc::Rc; use std::sync::Arc; @@ -7,67 +8,6 @@ use wasmtime::{Store, Trap, TrapCode}; const CANON_32BIT_NAN: u32 = 0b01111111110000000000000000000000; const CANON_64BIT_NAN: u64 = 0b0111111111111000000000000000000000000000000000000000000000000000; -// A simple bump allocator which can be used with modules below -const REALLOC_AND_FREE: &str = r#" - (global $last (mut i32) (i32.const 8)) - (func $realloc (export "canonical_abi_realloc") - (param $old_ptr i32) - (param $old_size i32) - (param $align i32) - (param $new_size i32) - (result i32) - - ;; Test if the old pointer is non-null - local.get $old_ptr - if - ;; If the old size is bigger than the new size then - ;; this is a shrink and transparently allow it - local.get $old_size - local.get $new_size - i32.gt_u - if - local.get $old_ptr - return - end - - ;; ... otherwise this is unimplemented - unreachable - end - - ;; align up `$last` - (global.set $last - (i32.and - (i32.add - (global.get $last) - (i32.add - (local.get $align) - (i32.const -1))) - (i32.xor - (i32.add - (local.get $align) - (i32.const -1)) - (i32.const -1)))) - - ;; save the current value of `$last` as the return value - global.get $last - - ;; ensure anything necessary is set to valid data by spraying a bit - ;; pattern that is invalid - global.get $last - i32.const 0xde - local.get $new_size - memory.fill - - ;; bump our pointer - (global.set $last - (i32.add - (global.get $last) - (local.get $new_size))) - ) - - (func (export "canonical_abi_free") (param i32 i32 i32)) -"#; - #[test] fn thunks() -> Result<()> { let component = r#" @@ -676,7 +616,7 @@ fn tuple_result() -> Result<()> { ) (func (export "invalid") (result i32) - i32.const -1 + i32.const -8 ) (func (export "canonical_abi_realloc") (param i32 i32 i32 i32) (result i32) @@ -1001,7 +941,7 @@ fn many_parameters() -> Result<()> { #[test] fn some_traps() -> Result<()> { - let middle_of_memory = i32::MAX / 2; + let middle_of_memory = (i32::MAX / 2) & (!0xff); let component = format!( r#"(component (module $m @@ -1067,7 +1007,7 @@ fn some_traps() -> Result<()> { (func (export "take-list") (param i32 i32)) (func (export "canonical_abi_realloc") (param i32 i32 i32 i32) (result i32) - i32.const 65535) + i32.const 65532) (func (export "canonical_abi_free") (param i32 i32 i32) unreachable) ) @@ -1194,11 +1134,10 @@ fn some_traps() -> Result<()> { .call(&mut store, (&[],))?; instance .get_typed_func::<(&[u8],), (), _>(&mut store, "take-list-end-oob")? - .call(&mut store, (&[1],))?; - assert_oob(&err); + .call(&mut store, (&[1, 2, 3, 4],))?; let err = instance .get_typed_func::<(&[u8],), (), _>(&mut store, "take-list-end-oob")? - .call(&mut store, (&[1, 2],)) + .call(&mut store, (&[1, 2, 3, 4, 5],)) .unwrap_err(); assert_oob(&err); instance @@ -1206,10 +1145,10 @@ fn some_traps() -> Result<()> { .call(&mut store, ("",))?; instance .get_typed_func::<(&str,), (), _>(&mut store, "take-string-end-oob")? - .call(&mut store, ("x",))?; + .call(&mut store, ("abcd",))?; let err = instance .get_typed_func::<(&str,), (), _>(&mut store, "take-string-end-oob")? - .call(&mut store, ("xy",)) + .call(&mut store, ("abcde",)) .unwrap_err(); assert_oob(&err); let err = instance @@ -1858,3 +1797,98 @@ impl<'a> SliceExt<'a> for &'a [u8] { a.try_into().unwrap() } } + +#[test] +fn invalid_alignment() -> Result<()> { + let component = format!( + r#"(component + (module $m + (memory (export "memory") 1) + (func (export "canonical_abi_realloc") (param i32 i32 i32 i32) (result i32) + i32.const 1) + (func (export "canonical_abi_free") (param i32 i32 i32) + unreachable) + + + (func (export "take-i32") (param i32)) + (func (export "ret-1") (result i32) i32.const 1) + (func (export "ret-unaligned-list") (result i32) + (i32.store offset=0 (i32.const 8) (i32.const 1)) + (i32.store offset=4 (i32.const 8) (i32.const 1)) + i32.const 8) + ) + (instance $i (instantiate (module $m))) + + (func (export "many-params") + (canon.lift + (func + (param string) (param string) (param string) (param string) + (param string) (param string) (param string) (param string) + (param string) (param string) (param string) (param string) + ) + (into $i) + (func $i "take-i32") + ) + ) + (func (export "string-ret") + (canon.lift (func (result string)) (into $i) (func $i "ret-1")) + ) + (func (export "list-u32-ret") + (canon.lift (func (result (list u32))) (into $i) (func $i "ret-unaligned-list")) + ) + )"# + ); + + let engine = super::engine(); + let component = Component::new(&engine, component)?; + let mut store = Store::new(&engine, ()); + let instance = Linker::new(&engine).instantiate(&mut store, &component)?; + + let err = instance + .get_typed_func::<( + &str, + &str, + &str, + &str, + &str, + &str, + &str, + &str, + &str, + &str, + &str, + &str, + ), (), _>(&mut store, "many-params")? + .call(&mut store, ("", "", "", "", "", "", "", "", "", "", "", "")) + .unwrap_err(); + assert!( + err.to_string() + .contains("realloc return: result not aligned"), + "{}", + err + ); + + let err = instance + .get_typed_func::<(), WasmStr, _>(&mut store, "string-ret")? + .call(&mut store, ()) + .err() + .unwrap(); + assert!( + err.to_string().contains("return pointer not aligned"), + "{}", + err + ); + + let err = instance + .get_typed_func::<(), WasmList, _>(&mut store, "list-u32-ret")? + .call(&mut store, ()) + .err() + .unwrap(); + assert!( + err.to_string().contains("list pointer is not aligned"), + "{}", + err + ); + + Ok(()) +} diff --git a/tests/all/component_model/import.rs b/tests/all/component_model/import.rs index 5f774dd57e..364792cf5e 100644 --- a/tests/all/component_model/import.rs +++ b/tests/all/component_model/import.rs @@ -1,3 +1,4 @@ +use super::REALLOC_AND_FREE; use anyhow::Result; use wasmtime::component::*; use wasmtime::{Store, StoreContextMut, Trap}; @@ -332,3 +333,293 @@ fn attempt_to_reenter_during_host() -> Result<()> { func.call(&mut store, ())?; Ok(()) } + +#[test] +fn stack_and_heap_args_and_rets() -> Result<()> { + let component = format!( + r#" +(component + (type $many_params (tuple + string string string string + string string string string + string)) + (import "f1" (func $f1 (param u32) (result u32))) + (import "f2" (func $f2 (param $many_params) (result u32))) + (import "f3" (func $f3 (param u32) (result string))) + (import "f4" (func $f4 (param $many_params) (result string))) + + (module $libc + {REALLOC_AND_FREE} + (memory (export "memory") 1) + ) + (instance $libc (instantiate (module $libc))) + + (func $f1_lower (canon.lower (into $libc) (func $f1))) + (func $f2_lower (canon.lower (into $libc) (func $f2))) + (func $f3_lower (canon.lower (into $libc) (func $f3))) + (func $f4_lower (canon.lower (into $libc) (func $f4))) + + (module $m + (import "host" "f1" (func $f1 (param i32) (result i32))) + (import "host" "f2" (func $f2 (param i32) (result i32))) + (import "host" "f3" (func $f3 (param i32 i32))) + (import "host" "f4" (func $f4 (param i32 i32))) + (import "libc" "memory" (memory 1)) + + (func $run (export "run") + block + i32.const 1 + call $f1 + i32.const 2 + i32.eq + br_if 0 + unreachable + end + + block + call $allocate_empty_strings + call $f2 + i32.const 3 + i32.eq + br_if 0 + unreachable + end + + block + i32.const 8 + i32.const 16000 + call $f3 + (call $validate_string_ret (i32.const 16000)) + end + + block + call $allocate_empty_strings + i32.const 20000 + call $f4 + (call $validate_string_ret (i32.const 20000)) + end + ) + + (func $allocate_empty_strings (result i32) + (local $ret i32) + (local $offset i32) + (local $cnt i32) + (local.set $ret (i32.const 8000)) + (local.set $cnt (i32.const 9)) + + loop + (call $setup_str (i32.add (local.get $ret) (local.get $offset))) + (local.set $offset (i32.add (local.get $offset) (i32.const 8))) + + (local.tee $cnt (i32.add (local.get $cnt) (i32.const -1))) + br_if 0 + end + + local.get $ret + ) + (func $setup_str (param $addr i32) + (i32.store offset=0 (local.get $addr) (i32.const 1000)) + (i32.store offset=4 (local.get $addr) (i32.const 3)) + ) + + (func $validate_string_ret (param $addr i32) + (local $base i32) + (local $len i32) + (local.set $base (i32.load (local.get $addr))) + (local.set $len (i32.load offset=4 (local.get $addr))) + + block + local.get $len + i32.const 3 + i32.eq + br_if 0 + unreachable + end + + (i32.load8_u offset=0 (local.get $base)) + i32.const 120 ;; 'x' + i32.ne + if unreachable end + + (i32.load8_u offset=1 (local.get $base)) + i32.const 121 ;; 'y' + i32.ne + if unreachable end + + (i32.load8_u offset=2 (local.get $base)) + i32.const 122 ;; 'z' + i32.ne + if unreachable end + ) + + (data (i32.const 1000) "abc") + ) + (instance $m (instantiate (module $m) + (with "libc" (instance $libc)) + (with "host" (instance + (export "f1" (func $f1_lower)) + (export "f2" (func $f2_lower)) + (export "f3" (func $f3_lower)) + (export "f4" (func $f4_lower)) + )) + )) + + (func (export "run") + (canon.lift (func) (func $m "run")) + ) +) + "# + ); + + let engine = super::engine(); + let mut linker = Linker::new(&engine); + linker.root().func_wrap("f1", |x: u32| -> Result { + assert_eq!(x, 1); + Ok(2) + })?; + linker.root().func_wrap( + "f2", + |cx: StoreContextMut<'_, ()>, + arg: ( + WasmStr, + WasmStr, + WasmStr, + WasmStr, + WasmStr, + WasmStr, + WasmStr, + WasmStr, + WasmStr, + )| + -> Result { + assert_eq!(arg.0.to_str(&cx).unwrap(), "abc"); + Ok(3) + }, + )?; + linker + .root() + .func_wrap("f3", |arg: u32| -> Result { + assert_eq!(arg, 8); + Ok("xyz".to_string()) + })?; + linker.root().func_wrap( + "f4", + |cx: StoreContextMut<'_, ()>, + arg: ( + WasmStr, + WasmStr, + WasmStr, + WasmStr, + WasmStr, + WasmStr, + WasmStr, + WasmStr, + WasmStr, + )| + -> Result { + assert_eq!(arg.0.to_str(&cx).unwrap(), "abc"); + Ok("xyz".to_string()) + }, + )?; + let component = Component::new(&engine, component)?; + let mut store = Store::new(&engine, ()); + let instance = linker.instantiate(&mut store, &component)?; + instance + .get_typed_func::<(), (), _>(&mut store, "run")? + .call(&mut store, ())?; + Ok(()) +} + +#[test] +fn bad_import_alignment() -> Result<()> { + let component = format!( + r#" +(component + (import "unaligned-retptr" (func $unaligned_retptr (result string))) + (type $many_arg (tuple + string string string string + string string string string + string + )) + (import "unaligned-argptr" (func $unaligned_argptr (param $many_arg))) + (module $libc_panic + (memory (export "memory") 1) + (func (export "canonical_abi_realloc") (param i32 i32 i32 i32) (result i32) + unreachable) + (func (export "canonical_abi_free") (param i32 i32 i32) + unreachable) + ) + (instance $libc_panic (instantiate (module $libc_panic))) + + (func $unaligned_retptr_lower + (canon.lower (into $libc_panic) (func $unaligned_retptr)) + ) + (func $unaligned_argptr_lower + (canon.lower (into $libc_panic) (func $unaligned_argptr)) + ) + + (module $m + (import "host" "unaligned-retptr" (func $unaligned_retptr (param i32))) + (import "host" "unaligned-argptr" (func $unaligned_argptr (param i32))) + + (func (export "unaligned-retptr") + (call $unaligned_retptr (i32.const 1))) + (func (export "unaligned-argptr") + (call $unaligned_argptr (i32.const 1))) + ) + (instance $m (instantiate (module $m) + (with "host" (instance + (export "unaligned-retptr" (func $unaligned_retptr_lower)) + (export "unaligned-argptr" (func $unaligned_argptr_lower)) + )) + )) + + (func (export "unaligned-retptr") + (canon.lift (func) (func $m "unaligned-retptr")) + ) + (func (export "unaligned-argptr") + (canon.lift (func) (func $m "unaligned-argptr")) + ) +) + "# + ); + + let engine = super::engine(); + let mut linker = Linker::new(&engine); + linker + .root() + .func_wrap("unaligned-retptr", || -> Result { + Ok(String::new()) + })?; + linker.root().func_wrap( + "unaligned-argptr", + |_: ( + WasmStr, + WasmStr, + WasmStr, + WasmStr, + WasmStr, + WasmStr, + WasmStr, + WasmStr, + WasmStr, + )| + -> Result<()> { unreachable!() }, + )?; + let component = Component::new(&engine, component)?; + let mut store = Store::new(&engine, ()); + let instance = linker.instantiate(&mut store, &component)?; + let trap = instance + .get_typed_func::<(), (), _>(&mut store, "unaligned-retptr")? + .call(&mut store, ()) + .unwrap_err() + .downcast::()?; + assert!(trap.to_string().contains("pointer not aligned"), "{}", trap); + let trap = instance + .get_typed_func::<(), (), _>(&mut store, "unaligned-argptr")? + .call(&mut store, ()) + .unwrap_err() + .downcast::()?; + assert!(trap.to_string().contains("pointer not aligned"), "{}", trap); + Ok(()) +}