#![cfg(not(miri))]
use super::REALLOC_AND_FREE;
use wasmtime::Result;
use wasmtime::component::{Component, Linker};
use wasmtime::{Config, Engine, PoolingAllocationConfig, Store, StoreContextMut, Trap};
const UTF16_TAG: u32 = 1 << 31;
const STRINGS: &[&str] = &[
"",
"x",
"hello this is a particularly long string yes it is it keeps going",
"à á â ã ä å æ ç è é ê ë",
"Ξ Ο Π Ρ Σ Τ Υ Φ Χ Ψ Ω Ϊ Ϋ ά έ ή",
"STUVWXYZ",
"ËÌÍÎÏÐÑÒ",
"\u{10000}",
"à ascii VWXYZ",
];
static ENCODINGS: [&str; 3] = ["utf8", "utf16", "latin1+utf16"];
#[test]
fn roundtrip() -> Result<()> {
for debug in [true, false] {
let mut config = wasmtime_test_util::component::config();
config.debug_adapter_modules(debug);
let engine = Engine::new(&config)?;
for src in ENCODINGS {
for dst in ENCODINGS {
test_roundtrip(&engine, src, dst)?;
}
}
}
Ok(())
}
fn test_roundtrip(engine: &Engine, src: &str, dst: &str) -> Result<()> {
println!("src={src} dst={dst}");
let mk_echo = |name: &str, encoding: &str| {
format!(
r#"
(component {name}
(import "echo" (func $echo (param "a" string) (result string)))
(core instance $libc (instantiate $libc))
(core func $echo (canon lower (func $echo)
(memory $libc "memory")
(realloc (func $libc "realloc"))
string-encoding={encoding}
))
(core instance $echo (instantiate $echo
(with "libc" (instance $libc))
(with "" (instance (export "echo" (func $echo))))
))
(func (export "echo2") (param "a" string) (result string)
(canon lift
(core func $echo "echo")
(memory $libc "memory")
(realloc (func $libc "realloc"))
string-encoding={encoding}
)
)
)
"#
)
};
let src = mk_echo("$src", src);
let dst = mk_echo("$dst", dst);
let component = format!(
r#"
(component
(import "host" (func $host (param "a" string) (result string)))
(core module $libc
(memory (export "memory") 1)
{REALLOC_AND_FREE}
)
(core module $echo
(import "" "echo" (func $echo (param i32 i32 i32)))
(import "libc" "memory" (memory 0))
(import "libc" "realloc" (func $realloc (param i32 i32 i32 i32) (result i32)))
(func (export "echo") (param i32 i32) (result i32)
(local $retptr i32)
(local.set $retptr
(call $realloc
(i32.const 0)
(i32.const 0)
(i32.const 4)
(i32.const 8)))
(call $echo
(local.get 0)
(local.get 1)
(local.get $retptr))
local.get $retptr
)
)
{src}
{dst}
(instance $dst (instantiate $dst (with "echo" (func $host))))
(instance $src (instantiate $src (with "echo" (func $dst "echo2"))))
(export "echo" (func $src "echo2"))
)
"#
);
let component = Component::new(engine, &component)?;
let mut store = Store::new(engine, String::new());
let mut linker = Linker::new(engine);
linker.root().func_wrap(
"host",
|store: StoreContextMut<String>, (arg,): (String,)| {
assert_eq!(*store.data(), arg);
Ok((arg,))
},
)?;
let instance = linker.instantiate(&mut store, &component)?;
let func = instance.get_typed_func::<(String,), (String,)>(&mut store, "echo")?;
for string in STRINGS {
println!("testing string {string:?}");
*store.data_mut() = string.to_string();
let (ret,) = func.call(&mut store, (string.to_string(),))?;
assert_eq!(ret, *string);
}
Ok(())
}
#[test]
fn ptr_out_of_bounds() -> Result<()> {
let engine = wasmtime_test_util::component::engine();
for src in ENCODINGS {
for dst in ENCODINGS {
test_ptr_out_of_bounds(&engine, src, dst)?;
}
}
Ok(())
}
fn test_ptr_out_of_bounds(engine: &Engine, src: &str, dst: &str) -> Result<()> {
let test = |len: u32| -> Result<()> {
let component = format!(
r#"
(component
(component $c
(core module $m
(func (export "") (param i32 i32))
(func (export "realloc") (param i32 i32 i32 i32) (result i32) i32.const 0)
(memory (export "memory") 1)
)
(core instance $m (instantiate $m))
(func (export "a") (param "a" string)
(canon lift (core func $m "") (realloc (func $m "realloc")) (memory $m "memory")
string-encoding={dst})
)
)
(component $c2
(import "a" (func $f (param "a" string)))
(core module $libc
(memory (export "memory") 1)
)
(core instance $libc (instantiate $libc))
(core func $f (canon lower (func $f) string-encoding={src} (memory $libc "memory")))
(core module $m
(import "" "" (func $f (param i32 i32)))
(func $start (call $f (i32.const 0x8000_0000) (i32.const {len})))
(start $start)
)
(core instance (instantiate $m (with "" (instance (export "" (func $f))))))
)
(instance $c (instantiate $c))
(instance $c2 (instantiate $c2 (with "a" (func $c "a"))))
)
"#
);
let component = Component::new(engine, &component)?;
let mut store = Store::new(engine, ());
let trap = Linker::new(engine)
.instantiate(&mut store, &component)
.err()
.unwrap()
.downcast::<Trap>()?;
assert_eq!(trap, Trap::StringOutOfBounds);
Ok(())
};
test(0)?;
test(1)?;
Ok(())
}
#[test]
fn ptr_overflow() -> Result<()> {
let engine = wasmtime_test_util::component::engine();
for src in ENCODINGS {
for dst in ENCODINGS {
test_ptr_overflow(&engine, src, dst)?;
}
}
Ok(())
}
fn test_ptr_overflow(engine: &Engine, src: &str, dst: &str) -> Result<()> {
let component = format!(
r#"
(component
(component $c
(core module $m
(func (export "") (param i32 i32))
(func (export "realloc") (param i32 i32 i32 i32) (result i32) i32.const 0)
(memory (export "memory") 1)
)
(core instance $m (instantiate $m))
(func (export "a") (param "a" string)
(canon lift (core func $m "") (realloc (func $m "realloc")) (memory $m "memory")
string-encoding={dst})
)
)
(component $c2
(import "a" (func $f (param "a" string)))
(core module $libc
(memory (export "memory") 1)
)
(core instance $libc (instantiate $libc))
(core func $f (canon lower (func $f) string-encoding={src} (memory $libc "memory")))
(core module $m
(import "" "" (func $f (param i32 i32)))
(func (export "f") (param i32) (call $f (i32.const 1000) (local.get 0)))
)
(core instance $m (instantiate $m (with "" (instance (export "" (func $f))))))
(func (export "f") (param "a" u32) (canon lift (core func $m "f")))
)
(instance $c (instantiate $c))
(instance $c2 (instantiate $c2 (with "a" (func $c "a"))))
(export "f" (func $c2 "f"))
)
"#
);
let component = Component::new(engine, &component)?;
let test_overflow = |size: u32| -> Result<()> {
println!("src={src} dst={dst} size={size:#x}");
let mut store = Store::new(engine, ());
let instance = Linker::new(engine).instantiate(&mut store, &component)?;
let func = instance.get_typed_func::<(u32,), ()>(&mut store, "f")?;
let trap = func
.call(&mut store, (size,))
.unwrap_err()
.downcast::<Trap>()?;
assert_eq!(trap, Trap::StringOutOfBounds);
Ok(())
};
let max = 1 << 31;
match src {
"utf8" => {
test_overflow(max)?;
if dst == "utf16" {
test_overflow(max / 2)?;
test_overflow(max / 2 - 100)?;
} else {
test_overflow(max - 100)?;
}
}
"utf16" => {
test_overflow(max / 2)?;
test_overflow(max / 2 - 100)?;
}
"latin1+utf16" => {
test_overflow((max / 2) | UTF16_TAG)?;
test_overflow((max / 2 - 100) | UTF16_TAG)?;
}
_ => unreachable!(),
}
Ok(())
}
#[test]
fn realloc_oob() -> Result<()> {
let engine = wasmtime_test_util::component::engine();
for src in ENCODINGS {
for dst in ENCODINGS {
test_realloc_oob(&engine, src, dst)?;
}
}
Ok(())
}
fn test_realloc_oob(engine: &Engine, src: &str, dst: &str) -> Result<()> {
let component = format!(
r#"
(component
(component $c
(core module $m
(func (export "") (param i32 i32))
(func (export "realloc") (param i32 i32 i32 i32) (result i32) i32.const 100_000)
(memory (export "memory") 1)
)
(core instance $m (instantiate $m))
(func (export "a") (param "a" string)
(canon lift (core func $m "") (realloc (func $m "realloc")) (memory $m "memory")
string-encoding={dst})
)
)
(component $c2
(import "a" (func $f (param "a" string)))
(core module $libc
(memory (export "memory") 1)
)
(core instance $libc (instantiate $libc))
(core func $f (canon lower (func $f) string-encoding={src} (memory $libc "memory")))
(core module $m
(import "" "" (func $f (param i32 i32)))
(func (export "f") (call $f (i32.const 1000) (i32.const 10)))
)
(core instance $m (instantiate $m (with "" (instance (export "" (func $f))))))
(func (export "f") (canon lift (core func $m "f")))
)
(instance $c (instantiate $c))
(instance $c2 (instantiate $c2 (with "a" (func $c "a"))))
(export "f" (func $c2 "f"))
)
"#
);
let component = Component::new(engine, &component)?;
let mut store = Store::new(engine, ());
let instance = Linker::new(engine).instantiate(&mut store, &component)?;
let func = instance.get_typed_func::<(), ()>(&mut store, "f")?;
let trap = func.call(&mut store, ()).unwrap_err().downcast::<Trap>()?;
assert_eq!(trap, Trap::StringOutOfBounds);
Ok(())
}
#[test]
fn raw_string_encodings() -> Result<()> {
let engine = wasmtime_test_util::component::engine();
test_invalid_string_encoding(&engine, "utf8", "utf8", &[0xff], 1)?;
let array = b"valid string until \xffthen valid again";
test_invalid_string_encoding(&engine, "utf8", "utf8", array, array.len() as u32)?;
test_invalid_string_encoding(&engine, "utf8", "utf16", array, array.len() as u32)?;
let array = b"symbol \xce\xa3 until \xffthen valid";
test_invalid_string_encoding(&engine, "utf8", "utf8", array, array.len() as u32)?;
test_invalid_string_encoding(&engine, "utf8", "utf16", array, array.len() as u32)?;
test_invalid_string_encoding(&engine, "utf8", "latin1+utf16", array, array.len() as u32)?;
test_invalid_string_encoding(&engine, "utf16", "utf8", &[0x01, 0xd8], 1)?;
test_invalid_string_encoding(&engine, "utf16", "utf16", &[0x01, 0xd8], 1)?;
test_invalid_string_encoding(
&engine,
"utf16",
"latin1+utf16",
&[0xff, 0xff, 0x01, 0xd8],
2,
)?;
test_invalid_string_encoding(
&engine,
"latin1+utf16",
"utf8",
&[0x01, 0xd8],
1 | UTF16_TAG,
)?;
test_invalid_string_encoding(
&engine,
"latin1+utf16",
"utf16",
&[0x01, 0xd8],
1 | UTF16_TAG,
)?;
test_invalid_string_encoding(
&engine,
"latin1+utf16",
"utf16",
&[0xff, 0xff, 0x01, 0xd8],
2 | UTF16_TAG,
)?;
test_invalid_string_encoding(
&engine,
"latin1+utf16",
"latin1+utf16",
&[0xab, 0x00, 0xff, 0xff, 0x01, 0xd8],
3 | UTF16_TAG,
)?;
test_valid_string_encoding(
&engine,
"latin1+utf16",
"latin1+utf16",
&[0xab, 0x00, 0xff, 0x00],
2 | UTF16_TAG,
)?;
Ok(())
}
fn test_invalid_string_encoding(
engine: &Engine,
src: &str,
dst: &str,
bytes: &[u8],
len: u32,
) -> Result<()> {
let trap = test_raw_when_encoded(engine, src, dst, bytes, len)?.unwrap();
let src = src.replace("latin1+", "");
assert!(
format!("{trap:?}").contains(&format!("invalid {src} encoding")),
"bad error: {trap:?}",
);
Ok(())
}
fn test_valid_string_encoding(
engine: &Engine,
src: &str,
dst: &str,
bytes: &[u8],
len: u32,
) -> Result<()> {
let err = test_raw_when_encoded(engine, src, dst, bytes, len)?;
assert!(err.is_none());
Ok(())
}
fn test_raw_when_encoded(
engine: &Engine,
src: &str,
dst: &str,
bytes: &[u8],
len: u32,
) -> Result<Option<wasmtime::Error>> {
let component = format!(
r#"
(component
(component $c
(core module $m
(func (export "") (param i32 i32))
(func (export "realloc") (param i32 i32 i32 i32) (result i32) i32.const 0)
(memory (export "memory") 1)
)
(core instance $m (instantiate $m))
(func (export "a") (param "a" string)
(canon lift (core func $m "") (realloc (func $m "realloc")) (memory $m "memory")
string-encoding={dst})
)
)
(component $c2
(import "a" (func $f (param "a" string)))
(core module $libc
(memory (export "memory") 1)
(func (export "realloc") (param i32 i32 i32 i32) (result i32) i32.const 0)
)
(core instance $libc (instantiate $libc))
(core func $f (canon lower (func $f) string-encoding={src} (memory $libc "memory")))
(core module $m
(import "" "" (func $f (param i32 i32)))
(func (export "f") (param i32 i32 i32) (call $f (local.get 0) (local.get 2)))
)
(core instance $m (instantiate $m (with "" (instance (export "" (func $f))))))
(func (export "f") (param "a" (list u8)) (param "b" u32) (canon lift (core func $m "f")
(memory $libc "memory")
(realloc (func $libc "realloc"))))
)
(instance $c (instantiate $c))
(instance $c2 (instantiate $c2 (with "a" (func $c "a"))))
(export "f" (func $c2 "f"))
)
"#
);
let component = Component::new(engine, &component)?;
let mut store = Store::new(engine, ());
let instance = Linker::new(engine).instantiate(&mut store, &component)?;
let func = instance.get_typed_func::<(&[u8], u32), ()>(&mut store, "f")?;
match func.call(&mut store, (bytes, len)) {
Ok(_) => Ok(None),
Err(e) => Ok(Some(e)),
}
}
#[test]
fn pass_string_on_component_boundary() -> Result<()> {
let mut pooling_config = PoolingAllocationConfig::new();
pooling_config.total_component_instances(3);
pooling_config.total_memories(2);
pooling_config.total_tables(0);
pooling_config.total_stacks(0);
pooling_config.max_memory_size(65536);
let mut config = Config::new();
config.memory_guard_size(0);
config.memory_reservation(65536);
config.allocation_strategy(pooling_config);
let engine = Engine::new(&config)?;
let component = r#"
(component
;; This component is instantiated first so its allocation function returns a
;; pointer at the end of memory which will be right up against the next
;; linear memory.
(component $c
(core module $m
(func (export "") (param i32 i32))
(func (export "realloc") (param i32 i32 i32 i32) (result i32) i32.const 65520)
(memory (export "memory") 1)
)
(core instance $m (instantiate $m))
(func (export "a") (param "a" string)
(canon lift (core func $m "") (realloc (func $m "realloc")) (memory $m "memory"))
)
)
;; This component is instantiated second meaning its memory is after the
;; one above, so the string is placed first thing in linear memory.
(component $c2
(import "a" (func $f (param "a" string)))
(core module $libc
(memory (export "memory") 1)
(data (memory 0) (i32.const 0) "0123456789abcdef")
)
(core instance $libc (instantiate $libc))
(core func $f (canon lower (func $f) (memory $libc "memory")))
(core module $m
(import "" "" (func $f (param i32 i32)))
(func (export "f")
(call $f
(i32.const 0) ;; ptr
(i32.const 16)) ;; len
)
)
(core instance $m (instantiate $m (with "" (instance (export "" (func $f))))))
(func (export "f") (canon lift (core func $m "f")))
)
(instance $c (instantiate $c))
(instance $c2 (instantiate $c2 (with "a" (func $c "a"))))
(export "f" (func $c2 "f"))
)
"#;
let component = Component::new(&engine, &component)?;
let mut store = Store::new(&engine, ());
let instance = Linker::new(&engine).instantiate(&mut store, &component)?;
let func = instance.get_typed_func::<(), ()>(&mut store, "f")?;
func.call(&mut store, ())?;
Ok(())
}