diff --git a/Cargo.toml b/Cargo.toml index 006f754..86813ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "waffle" -version = "0.0.25" +version = "0.0.27" description = "Wasm Analysis Framework For Lightweight Experiments" authors = ["Chris Fallin "] license = "Apache-2.0 WITH LLVM-exception" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index b5e8748..4da48f2 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -11,11 +11,11 @@ cargo-fuzz = true [dependencies] libfuzzer-sys = "0.4" -wasm-smith = "0.8" +wasm-smith = "0.202.0" env_logger = "0.9" log = "0.4" -wasmparser = "0.95" -wasmtime = "7.0" +wasmparser = "0.202.0" +wasmtime = "19.0" [dependencies.waffle] path = ".." diff --git a/fuzz/fuzz_targets/differential.rs b/fuzz/fuzz_targets/differential.rs index b72af67..4f86ec3 100644 --- a/fuzz/fuzz_targets/differential.rs +++ b/fuzz/fuzz_targets/differential.rs @@ -4,114 +4,110 @@ use std::sync::atomic::{AtomicU64, Ordering}; use waffle::{FrontendOptions, Module}; -fuzz_target!( - |module: wasm_smith::ConfiguredModule| { - let _ = env_logger::try_init(); - log::debug!("original module: {:?}", module.module); +fuzz_target!(|module: waffle::fuzzing::ArbitraryModule| { + let module = module.0; + let _ = env_logger::try_init(); + log::debug!("original module: {:?}", module); - let orig_bytes = module.module.to_bytes(); + let orig_bytes = module.to_bytes(); - if waffle::fuzzing::reject(&orig_bytes[..]) { - log::debug!("Discarding fuzz run. Body:\n{:?}", module); - return; - } else { - log::info!("body: {:?}", module); - } - - let mut config = wasmtime::Config::default(); - config.consume_fuel(true); - let engine = wasmtime::Engine::new(&config).unwrap(); - let orig_module = - wasmtime::Module::new(&engine, &orig_bytes[..]).expect("failed to parse original wasm"); - let mut orig_store = wasmtime::Store::new(&engine, ()); - orig_store.out_of_fuel_trap(); - orig_store.add_fuel(10000).unwrap(); - let orig_instance = wasmtime::Instance::new(&mut orig_store, &orig_module, &[]); - let orig_instance = match orig_instance { - Ok(orig_instance) => orig_instance, - Err(e) => { - log::info!("cannot run start on orig intsance ({:?}); discarding", e); - return; - } - }; - - let mut parsed_module = - Module::from_wasm_bytes(&orig_bytes[..], &FrontendOptions::default()).unwrap(); - parsed_module.expand_all_funcs().unwrap(); - parsed_module.per_func_body(|body| body.optimize()); - let roundtrip_bytes = parsed_module.to_wasm_bytes().unwrap(); - - if let Ok(filename) = std::env::var("FUZZ_DUMP_WASM") { - std::fs::write(format!("{}_orig.wasm", filename), &orig_bytes[..]).unwrap(); - std::fs::write(format!("{}_roundtrip.wasm", filename), &roundtrip_bytes[..]).unwrap(); - } - - let total = TOTAL.fetch_add(1, Ordering::Relaxed); - - let roundtrip_module = wasmtime::Module::new(&engine, &roundtrip_bytes[..]) - .expect("failed to parse roundtripped wasm"); - let mut roundtrip_store = wasmtime::Store::new(&engine, ()); - roundtrip_store.out_of_fuel_trap(); - // After roundtrip, fuel consumption rate may differ. That's fine; - // what matters is that it terminated above without a trap (hence - // halts in a reasonable time). - roundtrip_store.add_fuel(u64::MAX).unwrap(); - let roundtrip_instance = - wasmtime::Instance::new(&mut roundtrip_store, &roundtrip_module, &[]) - .expect("cannot instantiate roundtripped wasm"); - - // Ensure exports are equal. - - let a_globals: Vec<_> = orig_instance - .exports(&mut orig_store) - .filter_map(|e| e.into_global()) - .collect(); - let a_globals: Vec = a_globals - .into_iter() - .map(|g| g.get(&mut orig_store)) - .collect(); - let a_mems: Vec = orig_instance - .exports(&mut orig_store) - .filter_map(|e| e.into_memory()) - .collect(); - - let b_globals: Vec<_> = roundtrip_instance - .exports(&mut roundtrip_store) - .filter_map(|e| e.into_global()) - .collect(); - let b_globals: Vec = b_globals - .into_iter() - .map(|g| g.get(&mut roundtrip_store)) - .collect(); - let b_mems: Vec = roundtrip_instance - .exports(&mut roundtrip_store) - .filter_map(|e| e.into_memory()) - .collect(); - - log::info!("a_globals = {:?}", a_globals); - log::info!("b_globals = {:?}", b_globals); - - assert_eq!(a_globals.len(), b_globals.len()); - for (a, b) in a_globals.into_iter().zip(b_globals.into_iter()) { - match (a, b) { - (wasmtime::Val::I32(a), wasmtime::Val::I32(b)) => assert_eq!(a, b), - (wasmtime::Val::I64(a), wasmtime::Val::I64(b)) => assert_eq!(a, b), - (wasmtime::Val::F32(a), wasmtime::Val::F32(b)) => assert_eq!(a, b), - (wasmtime::Val::F64(a), wasmtime::Val::F64(b)) => assert_eq!(a, b), - _ => panic!("mismatching types"), - } - } - - assert_eq!(a_mems.len(), b_mems.len()); - for (a, b) in a_mems.into_iter().zip(b_mems.into_iter()) { - let a_data = a.data(&orig_store); - let b_data = b.data(&roundtrip_store); - assert_eq!(a_data, b_data); - } - - success(total); + if waffle::fuzzing::reject(&orig_bytes[..]) { + log::debug!("Discarding fuzz run. Body:\n{:?}", module); + return; + } else { + log::info!("body: {:?}", module); } -); + + let mut config = wasmtime::Config::default(); + config.consume_fuel(true); + let engine = wasmtime::Engine::new(&config).unwrap(); + let orig_module = + wasmtime::Module::new(&engine, &orig_bytes[..]).expect("failed to parse original wasm"); + let mut orig_store = wasmtime::Store::new(&engine, ()); + orig_store.set_fuel(10000).unwrap(); + let orig_instance = wasmtime::Instance::new(&mut orig_store, &orig_module, &[]); + let orig_instance = match orig_instance { + Ok(orig_instance) => orig_instance, + Err(e) => { + log::info!("cannot run start on orig intsance ({:?}); discarding", e); + return; + } + }; + + let mut parsed_module = + Module::from_wasm_bytes(&orig_bytes[..], &FrontendOptions::default()).unwrap(); + parsed_module.expand_all_funcs().unwrap(); + parsed_module.per_func_body(|body| body.optimize()); + let roundtrip_bytes = parsed_module.to_wasm_bytes().unwrap(); + + if let Ok(filename) = std::env::var("FUZZ_DUMP_WASM") { + std::fs::write(format!("{}_orig.wasm", filename), &orig_bytes[..]).unwrap(); + std::fs::write(format!("{}_roundtrip.wasm", filename), &roundtrip_bytes[..]).unwrap(); + } + + let total = TOTAL.fetch_add(1, Ordering::Relaxed); + + let roundtrip_module = wasmtime::Module::new(&engine, &roundtrip_bytes[..]) + .expect("failed to parse roundtripped wasm"); + let mut roundtrip_store = wasmtime::Store::new(&engine, ()); + // After roundtrip, fuel consumption rate may differ. That's fine; + // what matters is that it terminated above without a trap (hence + // halts in a reasonable time). + roundtrip_store.set_fuel(u64::MAX).unwrap(); + let roundtrip_instance = wasmtime::Instance::new(&mut roundtrip_store, &roundtrip_module, &[]) + .expect("cannot instantiate roundtripped wasm"); + + // Ensure exports are equal. + + let a_globals: Vec<_> = orig_instance + .exports(&mut orig_store) + .filter_map(|e| e.into_global()) + .collect(); + let a_globals: Vec = a_globals + .into_iter() + .map(|g| g.get(&mut orig_store)) + .collect(); + let a_mems: Vec = orig_instance + .exports(&mut orig_store) + .filter_map(|e| e.into_memory()) + .collect(); + + let b_globals: Vec<_> = roundtrip_instance + .exports(&mut roundtrip_store) + .filter_map(|e| e.into_global()) + .collect(); + let b_globals: Vec = b_globals + .into_iter() + .map(|g| g.get(&mut roundtrip_store)) + .collect(); + let b_mems: Vec = roundtrip_instance + .exports(&mut roundtrip_store) + .filter_map(|e| e.into_memory()) + .collect(); + + log::info!("a_globals = {:?}", a_globals); + log::info!("b_globals = {:?}", b_globals); + + assert_eq!(a_globals.len(), b_globals.len()); + for (a, b) in a_globals.into_iter().zip(b_globals.into_iter()) { + match (a, b) { + (wasmtime::Val::I32(a), wasmtime::Val::I32(b)) => assert_eq!(a, b), + (wasmtime::Val::I64(a), wasmtime::Val::I64(b)) => assert_eq!(a, b), + (wasmtime::Val::F32(a), wasmtime::Val::F32(b)) => assert_eq!(a, b), + (wasmtime::Val::F64(a), wasmtime::Val::F64(b)) => assert_eq!(a, b), + _ => panic!("mismatching types"), + } + } + + assert_eq!(a_mems.len(), b_mems.len()); + for (a, b) in a_mems.into_iter().zip(b_mems.into_iter()) { + let a_data = a.data(&orig_store); + let b_data = b.data(&roundtrip_store); + assert_eq!(a_data, b_data); + } + + success(total); +}); static TOTAL: AtomicU64 = AtomicU64::new(0); static SUCCESS: AtomicU64 = AtomicU64::new(0); diff --git a/fuzz/fuzz_targets/opt_diff.rs b/fuzz/fuzz_targets/opt_diff.rs index cee3e11..524bbc8 100644 --- a/fuzz/fuzz_targets/opt_diff.rs +++ b/fuzz/fuzz_targets/opt_diff.rs @@ -3,66 +3,65 @@ use libfuzzer_sys::fuzz_target; use waffle::{FrontendOptions, InterpContext, InterpResult, Module}; -fuzz_target!( - |module: wasm_smith::ConfiguredModule| { - let _ = env_logger::try_init(); - log::debug!("original module: {:?}", module.module); +fuzz_target!(|module: waffle::fuzzing::ArbitraryModule| { + let module = module.0; + let _ = env_logger::try_init(); + log::debug!("original module: {:?}", module); - let orig_bytes = module.module.to_bytes(); + let orig_bytes = module.to_bytes(); - if waffle::fuzzing::reject(&orig_bytes[..]) { - log::debug!("Discarding fuzz run. Body:\n{:?}", module); - return; - } else { - log::info!("body: {:?}", module); - } - - let mut parsed_module = - Module::from_wasm_bytes(&orig_bytes[..], &FrontendOptions::default()).unwrap(); - parsed_module.expand_all_funcs().unwrap(); - - let start = parsed_module.start_func.unwrap(); - - let mut orig_ctx = match InterpContext::new(&parsed_module) { - Ok(ctx) => ctx, - Err(e) => { - log::trace!("Rejecting due to instantiation error: {:?}", e); - return; - } - }; - orig_ctx.fuel = 10000; - - match orig_ctx.call(&parsed_module, start, &[]) { - InterpResult::OutOfFuel => { - // Silently reject. - log::trace!("Rejecting due to timeout in orig"); - return; - } - InterpResult::Trap(..) => { - // Silently reject. - log::trace!("Rejecting due to trap in orig"); - return; - } - InterpResult::Ok(_) => {} - ret => panic!("Bad result: {:?}", ret), - } - - let mut opt_module = parsed_module.clone(); - opt_module.per_func_body(|body| body.optimize()); - opt_module.per_func_body(|body| body.convert_to_max_ssa(None)); - - let mut opt_ctx = InterpContext::new(&opt_module).unwrap(); - // Allow a little leeway for opts to not actually optimize. - opt_ctx.fuel = 20000; - opt_ctx.call(&opt_module, start, &[]).ok().unwrap(); - - log::trace!( - "Orig ran in {} fuel; opt ran in {} fuel", - 10000 - orig_ctx.fuel, - 20000 - opt_ctx.fuel - ); - - assert_eq!(orig_ctx.memories, opt_ctx.memories); - assert_eq!(orig_ctx.globals, opt_ctx.globals); + if waffle::fuzzing::reject(&orig_bytes[..]) { + log::debug!("Discarding fuzz run. Body:\n{:?}", module); + return; + } else { + log::info!("body: {:?}", module); } -); + + let mut parsed_module = + Module::from_wasm_bytes(&orig_bytes[..], &FrontendOptions::default()).unwrap(); + parsed_module.expand_all_funcs().unwrap(); + + let start = parsed_module.start_func.unwrap(); + + let mut orig_ctx = match InterpContext::new(&parsed_module) { + Ok(ctx) => ctx, + Err(e) => { + log::trace!("Rejecting due to instantiation error: {:?}", e); + return; + } + }; + orig_ctx.fuel = 10000; + + match orig_ctx.call(&parsed_module, start, &[]) { + InterpResult::OutOfFuel => { + // Silently reject. + log::trace!("Rejecting due to timeout in orig"); + return; + } + InterpResult::Trap(..) => { + // Silently reject. + log::trace!("Rejecting due to trap in orig"); + return; + } + InterpResult::Ok(_) => {} + ret => panic!("Bad result: {:?}", ret), + } + + let mut opt_module = parsed_module.clone(); + opt_module.per_func_body(|body| body.optimize()); + opt_module.per_func_body(|body| body.convert_to_max_ssa(None)); + + let mut opt_ctx = InterpContext::new(&opt_module).unwrap(); + // Allow a little leeway for opts to not actually optimize. + opt_ctx.fuel = 20000; + opt_ctx.call(&opt_module, start, &[]).ok().unwrap(); + + log::trace!( + "Orig ran in {} fuel; opt ran in {} fuel", + 10000 - orig_ctx.fuel, + 20000 - opt_ctx.fuel + ); + + assert_eq!(orig_ctx.memories, opt_ctx.memories); + assert_eq!(orig_ctx.globals, opt_ctx.globals); +}); diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 2f153d5..ae07a45 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -941,6 +941,7 @@ impl<'a> WasmFuncBackend<'a> { Operator::CallRef { sig_index } => { Some(wasm_encoder::Instruction::CallRef(sig_index.index() as u32)) } + Operator::RefIsNull => Some(wasm_encoder::Instruction::RefIsNull), Operator::RefFunc { func_index } => { Some(wasm_encoder::Instruction::RefFunc(func_index.index() as u32)) } diff --git a/src/frontend.rs b/src/frontend.rs index 120e82a..2bde9d7 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -1426,6 +1426,7 @@ impl<'a, 'b> FunctionBodyBuilder<'a, 'b> { | wasmparser::Operator::F32x4DemoteF64x2Zero | wasmparser::Operator::F64x2PromoteLowF32x4 | wasmparser::Operator::CallRef { .. } + | wasmparser::Operator::RefIsNull | wasmparser::Operator::RefFunc { .. } => { self.emit(Operator::try_from(&op).unwrap(), loc)? } diff --git a/src/fuzzing.rs b/src/fuzzing.rs index 3b77a2a..4b8a8b9 100644 --- a/src/fuzzing.rs +++ b/src/fuzzing.rs @@ -1,111 +1,84 @@ //! Fuzzing-specific utilities. -// use libfuzzer_sys::arbitrary; +use libfuzzer_sys::arbitrary; -// pub fn reject(bytes: &[u8]) -> bool { -// let parser = wasmparser::Parser::new(0); -// let mut has_start = false; -// let mut has_global_set = false; -// let mut num_globals = 0; -// for payload in parser.parse_all(bytes) { -// match payload.unwrap() { -// wasmparser::Payload::CodeSectionEntry(body) => { -// for op in body.get_operators_reader().unwrap() { -// let op = op.unwrap(); -// match op { -// wasmparser::Operator::GlobalSet { .. } => { -// has_global_set = true; -// } -// _ => {} -// } -// } -// } -// wasmparser::Payload::StartSection { .. } => { -// has_start = true; -// } -// wasmparser::Payload::ExportSection(mut reader) => { -// for _ in 0..reader.get_count() { -// let e = reader.read().unwrap(); -// match &e.kind { -// &wasmparser::ExternalKind::Global => { -// num_globals += 1; -// } -// _ => {} -// } -// } -// } -// wasmparser::Payload::MemorySection(mut reader) => { -// for _ in 0..reader.get_count() { -// let m = reader.read().unwrap(); -// if m.maximum.is_none() || m.maximum.unwrap() > 100 { -// return true; -// } -// } -// } -// _ => {} -// } -// } +pub fn reject(bytes: &[u8]) -> bool { + let parser = wasmparser::Parser::new(0); + let mut has_start = false; + let mut has_global_set = false; + let mut num_globals = 0; + for payload in parser.parse_all(bytes) { + match payload.unwrap() { + wasmparser::Payload::CodeSectionEntry(body) => { + for op in body.get_operators_reader().unwrap() { + let op = op.unwrap(); + match op { + wasmparser::Operator::GlobalSet { .. } => { + has_global_set = true; + } + _ => {} + } + } + } + wasmparser::Payload::StartSection { .. } => { + has_start = true; + } + wasmparser::Payload::ExportSection(reader) => { + for export in reader { + let export = export.unwrap(); + match &export.kind { + &wasmparser::ExternalKind::Global => { + num_globals += 1; + } + _ => {} + } + } + } + wasmparser::Payload::MemorySection(reader) => { + for mem in reader { + let mem = mem.unwrap(); + if mem.maximum.is_none() || mem.maximum.unwrap() > 100 { + return true; + } + } + } + _ => {} + } + } -// if !has_start || !has_global_set || num_globals < 1 { -// return true; -// } + if !has_start || !has_global_set || num_globals < 1 { + return true; + } -// false -// } + false +} -// #[derive(Debug)] -// pub struct Config; +pub fn fuzzing_config() -> wasm_smith::Config { + wasm_smith::Config { + min_funcs: 1, + max_funcs: 1, + min_memories: 1, + max_memories: 1, + min_globals: 10, + max_globals: 10, + min_tables: 0, + max_tables: 0, + min_imports: 0, + max_imports: 0, + min_exports: 12, + max_exports: 12, + allow_start_export: true, + canonicalize_nans: true, + max_memory32_pages: 1, + ..Default::default() + } +} -// impl<'a> arbitrary::Arbitrary<'a> for Config { -// fn arbitrary(_u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { -// Ok(Config) -// } -// } +#[derive(Debug)] +pub struct ArbitraryModule(pub wasm_smith::Module); -// impl wasm_smith::Config for Config { -// fn min_funcs(&self) -> usize { -// 1 -// } -// fn max_funcs(&self) -> usize { -// 1 -// } -// fn min_memories(&self) -> u32 { -// 1 -// } -// fn max_memories(&self) -> usize { -// 1 -// } -// fn min_globals(&self) -> usize { -// 10 -// } -// fn max_globals(&self) -> usize { -// 10 -// } -// fn min_tables(&self) -> u32 { -// 0 -// } -// fn max_tables(&self) -> usize { -// 0 -// } -// fn min_imports(&self) -> usize { -// 0 -// } -// fn max_imports(&self) -> usize { -// 0 -// } -// fn min_exports(&self) -> usize { -// 12 -// } -// fn max_exports(&self) -> usize { -// 12 -// } -// fn allow_start_export(&self) -> bool { -// true -// } -// fn canonicalize_nans(&self) -> bool { -// true -// } -// fn max_memory_pages(&self, _is_64: bool) -> u64 { -// 1 -// } -// } +impl<'a> arbitrary::Arbitrary<'a> for ArbitraryModule { + fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { + Ok(Self(wasm_smith::Module::new(fuzzing_config(), u)?)) + } +} diff --git a/src/op_traits.rs b/src/op_traits.rs index 3be35c9..a5ebd6f 100644 --- a/src/op_traits.rs +++ b/src/op_traits.rs @@ -3,7 +3,7 @@ use crate::entity::EntityRef; use crate::ir::{Module, Type, Value}; use crate::Operator; -use anyhow::Result; +use anyhow::{Context, Result}; use std::borrow::Cow; pub fn op_inputs( @@ -485,6 +485,7 @@ pub fn op_inputs( params.push(Type::TypedFuncRef(true, sig_index.index() as u32)); Ok(params.into()) } + Operator::RefIsNull => Ok(vec![op_stack.context("in getting stack")?.last().unwrap().0].into()), Operator::RefFunc { .. } => Ok(Cow::Borrowed(&[])), Operator::MemoryCopy { .. } => Ok(Cow::Borrowed(&[Type::I32, Type::I32, Type::I32])), Operator::MemoryFill { .. } => Ok(Cow::Borrowed(&[Type::I32, Type::I32, Type::I32])), @@ -716,10 +717,10 @@ pub fn op_outputs( Operator::V128Load16Lane { .. } => Ok(Cow::Borrowed(&[Type::V128])), Operator::V128Load32Lane { .. } => Ok(Cow::Borrowed(&[Type::V128])), Operator::V128Load64Lane { .. } => Ok(Cow::Borrowed(&[Type::V128])), - Operator::V128Store8Lane { .. } => Ok(Cow::Borrowed(&[Type::V128])), - Operator::V128Store16Lane { .. } => Ok(Cow::Borrowed(&[Type::V128])), - Operator::V128Store32Lane { .. } => Ok(Cow::Borrowed(&[Type::V128])), - Operator::V128Store64Lane { .. } => Ok(Cow::Borrowed(&[Type::V128])), + Operator::V128Store8Lane { .. } => Ok(Cow::Borrowed(&[])), + Operator::V128Store16Lane { .. } => Ok(Cow::Borrowed(&[])), + Operator::V128Store32Lane { .. } => Ok(Cow::Borrowed(&[])), + Operator::V128Store64Lane { .. } => Ok(Cow::Borrowed(&[])), Operator::V128Const { .. } => Ok(Cow::Borrowed(&[Type::V128])), Operator::I8x16Shuffle { .. } => Ok(Cow::Borrowed(&[Type::V128])), @@ -955,6 +956,7 @@ pub fn op_outputs( Operator::CallRef { sig_index } => { Ok(Vec::from(module.signatures[*sig_index].returns.clone()).into()) } + Operator::RefIsNull => Ok(Cow::Borrowed(&[Type::I32])), Operator::RefFunc { func_index } => { let ty = module.funcs[*func_index].sig(); Ok(vec![Type::TypedFuncRef(true, ty.index() as u32)].into()) @@ -1427,6 +1429,7 @@ impl Operator { Operator::F64x2PromoteLowF32x4 => &[], Operator::CallRef { .. } => &[All], + Operator::RefIsNull => &[], Operator::RefFunc { .. } => &[], } } @@ -1922,6 +1925,7 @@ impl std::fmt::Display for Operator { Operator::F64x2PromoteLowF32x4 => write!(f, "f64x2promotelowf32x4")?, Operator::CallRef { sig_index } => write!(f, "call_ref<{}>", sig_index)?, + Operator::RefIsNull => write!(f, "ref_is_null")?, Operator::RefFunc { func_index } => write!(f, "ref_func<{}>", func_index)?, } diff --git a/src/ops.rs b/src/ops.rs index 6b919db..fa677ab 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -634,6 +634,7 @@ pub enum Operator { CallRef { sig_index: Signature, }, + RefIsNull, RefFunc { func_index: Func, }, @@ -1276,6 +1277,7 @@ impl<'a, 'b> std::convert::TryFrom<&'b wasmparser::Operator<'a>> for Operator { &wasmparser::Operator::CallRef { type_index } => Ok(Operator::CallRef { sig_index: Signature::from(type_index), }), + &wasmparser::Operator::RefIsNull => Ok(Operator::RefIsNull), &wasmparser::Operator::RefFunc { function_index } => Ok(Operator::RefFunc { func_index: Func::from(function_index), }), diff --git a/wasm_tests/non-nullable-funcrefs.wat b/wasm_tests/non-nullable-funcrefs.wat new file mode 100644 index 0000000..f5978f8 --- /dev/null +++ b/wasm_tests/non-nullable-funcrefs.wat @@ -0,0 +1,8 @@ +(module + (type $t (func (param i32) (result i32))) + (func $f1 (param i32) (result i32) local.get 0) + (func $f2 (param i32) (result i32) local.get 0) + (table $t 1 1 (ref null $t)) + (elem $t (i32.const 0) (ref null $t) + (item (ref.func $f1)) + (item (ref.func $f2))))