From cf6435b27ce7acef511c245d9d6461df6877ca41 Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Wed, 20 May 2026 20:24:52 -0700 Subject: [PATCH 1/7] Add tag-related API coverage to the public API fuzzer --- crates/fuzzing/src/generators/api.rs | 106 +++++++++++++++++++++++++++ crates/fuzzing/src/oracles/api.rs | 94 ++++++++++++++++++++++++ 2 files changed, 200 insertions(+) diff --git a/crates/fuzzing/src/generators/api.rs b/crates/fuzzing/src/generators/api.rs index a115ab156299..ca3d1a177aaf 100644 --- a/crates/fuzzing/src/generators/api.rs +++ b/crates/fuzzing/src/generators/api.rs @@ -90,6 +90,13 @@ struct Swarm { func_call: bool, get_func_export: bool, func_drop: bool, + tag_type_new: bool, + tag_type_drop: bool, + tag_new: bool, + tag_ty: bool, + tag_eq: bool, + get_tag_export: bool, + tag_drop: bool, } /// A call to one of Wasmtime's public APIs. @@ -273,6 +280,33 @@ pub enum ApiCall { FuncDrop { id: usize, }, + TagTypeNew { + id: usize, + func_ty: usize, + }, + TagTypeDrop { + id: usize, + }, + TagNew { + id: usize, + tag_ty: usize, + store: usize, + }, + TagTy { + tag: usize, + }, + TagEq { + a: usize, + b: usize, + }, + GetTagExport { + id: usize, + instance: usize, + nth: usize, + }, + TagDrop { + id: usize, + }, } use ApiCall::*; @@ -317,6 +351,13 @@ struct Scope { /// associated `store_id`. funcs: BTreeMap, // func_id -> store_id + /// Tag types that are currently live. + tag_types: BTreeSet, + + /// Tags that are currently live. Maps from `tag_id` to the tag's + /// associated `store_id`. + tags: BTreeMap, // tag_id -> store_id + config: Config, } @@ -356,6 +397,8 @@ impl<'a> Arbitrary<'a> for ApiCalls { memories: BTreeMap::default(), func_types: BTreeSet::default(), funcs: BTreeMap::default(), + tag_types: BTreeSet::default(), + tags: BTreeMap::default(), config: config.clone(), }; @@ -396,6 +439,7 @@ impl<'a> Arbitrary<'a> for ApiCalls { scope.tables.retain(|_, store_id| *store_id != id); scope.memories.retain(|_, store_id| *store_id != id); scope.funcs.retain(|_, store_id| *store_id != id); + scope.tags.retain(|_, store_id| *store_id != id); Ok(StoreDrop { id }) }); } @@ -815,6 +859,68 @@ impl<'a> Arbitrary<'a> for ApiCalls { Ok(FuncDrop { id }) }); } + if swarm.tag_type_new && !scope.func_types.is_empty() { + choices.push(|input, scope| { + let types: Vec<_> = scope.func_types.iter().collect(); + let func_ty = **input.choose(&types)?; + let id = scope.next_id(); + scope.tag_types.insert(id); + Ok(TagTypeNew { id, func_ty }) + }); + } + if swarm.tag_type_drop && !scope.tag_types.is_empty() { + choices.push(|input, scope| { + let types: Vec<_> = scope.tag_types.iter().collect(); + let id = **input.choose(&types)?; + scope.tag_types.remove(&id); + Ok(TagTypeDrop { id }) + }); + } + if swarm.tag_new && !scope.tag_types.is_empty() && !scope.stores.is_empty() { + choices.push(|input, scope| { + let types: Vec<_> = scope.tag_types.iter().collect(); + let tag_ty = **input.choose(&types)?; + let stores: Vec<_> = scope.stores.iter().collect(); + let store = **input.choose(&stores)?; + let id = scope.next_id(); + scope.tags.insert(id, store); + Ok(TagNew { id, tag_ty, store }) + }); + } + if swarm.tag_ty && !scope.tags.is_empty() { + choices.push(|input, scope| { + let tags: Vec<_> = scope.tags.keys().collect(); + let tag = **input.choose(&tags)?; + Ok(TagTy { tag }) + }); + } + if swarm.tag_eq && scope.tags.len() >= 2 { + choices.push(|input, scope| { + let tags: Vec<_> = scope.tags.keys().collect(); + let a = **input.choose(&tags)?; + let b = **input.choose(&tags)?; + Ok(TagEq { a, b }) + }); + } + if swarm.get_tag_export && !scope.instances.is_empty() { + choices.push(|input, scope| { + let instances: Vec<_> = scope.instances.keys().collect(); + let instance = **input.choose(&instances)?; + let nth = usize::arbitrary(input)?; + let id = scope.next_id(); + let store = *scope.instances.get(&instance).unwrap(); + scope.tags.insert(id, store); + Ok(GetTagExport { id, instance, nth }) + }); + } + if swarm.tag_drop && !scope.tags.is_empty() { + choices.push(|input, scope| { + let tags: Vec<_> = scope.tags.keys().collect(); + let id = **input.choose(&tags)?; + scope.tags.remove(&id); + Ok(TagDrop { id }) + }); + } if choices.is_empty() { break; diff --git a/crates/fuzzing/src/oracles/api.rs b/crates/fuzzing/src/oracles/api.rs index d04fda0dadb6..44ffca3b47f4 100644 --- a/crates/fuzzing/src/oracles/api.rs +++ b/crates/fuzzing/src/oracles/api.rs @@ -23,6 +23,8 @@ pub fn make_api_calls(api: ApiCalls) { let mut memories: HashMap = Default::default(); let mut func_types: HashMap = Default::default(); let mut funcs: HashMap = Default::default(); + let mut tag_types: HashMap = Default::default(); + let mut tags: HashMap = Default::default(); for call in api.calls { match call { @@ -41,6 +43,7 @@ pub fn make_api_calls(api: ApiCalls) { tables.retain(|_, (_, store_id)| *store_id != id); memories.retain(|_, (_, store_id)| *store_id != id); funcs.retain(|_, (_, store_id)| *store_id != id); + tags.retain(|_, (_, store_id)| *store_id != id); stores.remove(&id); } @@ -662,6 +665,97 @@ pub fn make_api_calls(api: ApiCalls) { log::trace!("dropping func {id}"); funcs.remove(&id); } + + ApiCall::TagTypeNew { id, func_ty } => { + log::trace!("creating tag type {id} from func type {func_ty}"); + let ft = match func_types.get(&func_ty) { + Some(t) => t.clone(), + None => continue, + }; + let old = tag_types.insert(id, TagType::new(ft)); + assert!(old.is_none()); + } + + ApiCall::TagTypeDrop { id } => { + log::trace!("dropping tag type {id}"); + tag_types.remove(&id); + } + + ApiCall::TagNew { id, tag_ty, store } => { + log::trace!("creating tag {id} with type {tag_ty} in store {store}"); + let tt = match tag_types.get(&tag_ty) { + Some(t) => t.clone(), + None => continue, + }; + let st = match stores.get_mut(&store) { + Some(s) => s, + None => continue, + }; + match Tag::new(&mut *st, &tt) { + Ok(t) => { + tags.insert(id, (t, store)); + } + Err(_) => continue, + } + } + + ApiCall::TagTy { tag } => { + log::trace!("checking type of tag {tag}"); + let (t, store_id) = match tags.get(&tag) { + Some(&x) => x, + None => continue, + }; + let st = match stores.get(&store_id) { + Some(s) => s, + None => continue, + }; + let _ = t.ty(st); + } + + ApiCall::TagEq { a, b } => { + log::trace!("comparing tags {a} and {b}"); + let (ta, store_id) = match tags.get(&a) { + Some(&x) => x, + None => continue, + }; + let (tb, store_id_b) = match tags.get(&b) { + Some(&x) => x, + None => continue, + }; + if store_id != store_id_b { + continue; + } + let st = match stores.get(&store_id) { + Some(s) => s, + None => continue, + }; + let _ = Tag::eq(&ta, &tb, st); + } + + ApiCall::GetTagExport { id, instance, nth } => { + log::trace!("getting {nth}th tag export of instance {instance} as {id}"); + let (inst, store_id) = match instances.get(&instance) { + Some(&x) => x, + None => continue, + }; + let st = match stores.get_mut(&store_id) { + Some(s) => s, + None => continue, + }; + let ts = inst + .exports(&mut *st) + .filter_map(|e| e.into_tag()) + .collect::>(); + if ts.is_empty() { + continue; + } + tags.insert(id, (ts[nth % ts.len()], store_id)); + } + + ApiCall::TagDrop { id } => { + log::trace!("dropping tag {id}"); + tags.remove(&id); + } } } } From 84fd1529d9c76672d57c2e42b599f808c5c5b34e Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Thu, 21 May 2026 07:30:41 -0700 Subject: [PATCH 2/7] Add module introspection and serialization API coverage to the public API fuzzer --- crates/fuzzing/src/generators/api.rs | 75 ++++++++++++++++++++++++++++ crates/fuzzing/src/oracles/api.rs | 69 ++++++++++++++++++++++++- 2 files changed, 142 insertions(+), 2 deletions(-) diff --git a/crates/fuzzing/src/generators/api.rs b/crates/fuzzing/src/generators/api.rs index ca3d1a177aaf..2dc452c998c0 100644 --- a/crates/fuzzing/src/generators/api.rs +++ b/crates/fuzzing/src/generators/api.rs @@ -97,6 +97,12 @@ struct Swarm { tag_eq: bool, get_tag_export: bool, tag_drop: bool, + module_imports: bool, + module_exports: bool, + module_get_export: bool, + module_name: bool, + module_validate: bool, + module_serialize_deserialize: bool, } /// A call to one of Wasmtime's public APIs. @@ -307,6 +313,26 @@ pub enum ApiCall { TagDrop { id: usize, }, + ModuleImports { + module: usize, + }, + ModuleExports { + module: usize, + }, + ModuleGetExport { + module: usize, + name: String, + }, + ModuleName { + module: usize, + }, + ModuleValidate { + wasm: Vec, + }, + ModuleSerializeDeserialize { + src_id: usize, + dst_id: usize, + }, } use ApiCall::*; @@ -921,6 +947,55 @@ impl<'a> Arbitrary<'a> for ApiCalls { Ok(TagDrop { id }) }); } + if swarm.module_imports && !scope.modules.is_empty() { + choices.push(|input, scope| { + let modules: Vec<_> = scope.modules.iter().collect(); + let module = **input.choose(&modules)?; + Ok(ModuleImports { module }) + }); + } + if swarm.module_exports && !scope.modules.is_empty() { + choices.push(|input, scope| { + let modules: Vec<_> = scope.modules.iter().collect(); + let module = **input.choose(&modules)?; + Ok(ModuleExports { module }) + }); + } + if swarm.module_get_export && !scope.modules.is_empty() { + choices.push(|input, scope| { + let modules: Vec<_> = scope.modules.iter().collect(); + let module = **input.choose(&modules)?; + let name = String::arbitrary(input)?; + Ok(ModuleGetExport { module, name }) + }); + } + if swarm.module_name && !scope.modules.is_empty() { + choices.push(|input, scope| { + let modules: Vec<_> = scope.modules.iter().collect(); + let module = **input.choose(&modules)?; + Ok(ModuleName { module }) + }); + } + if swarm.module_validate { + choices.push(|input, scope| { + let use_valid_wasm = bool::arbitrary(input)?; + let wasm = if use_valid_wasm { + scope.config.generate(input, Some(1000))?.to_bytes() + } else { + Vec::::arbitrary(input)? + }; + Ok(ModuleValidate { wasm }) + }); + } + if swarm.module_serialize_deserialize && !scope.modules.is_empty() { + choices.push(|input, scope| { + let modules: Vec<_> = scope.modules.iter().collect(); + let src_id = **input.choose(&modules)?; + let dst_id = scope.next_id(); + scope.modules.insert(dst_id); + Ok(ModuleSerializeDeserialize { src_id, dst_id }) + }); + } if choices.is_empty() { break; diff --git a/crates/fuzzing/src/oracles/api.rs b/crates/fuzzing/src/oracles/api.rs index 44ffca3b47f4..672802ae8b1e 100644 --- a/crates/fuzzing/src/oracles/api.rs +++ b/crates/fuzzing/src/oracles/api.rs @@ -7,8 +7,11 @@ use wasmtime::*; pub fn make_api_calls(api: ApiCalls) { // The generator always starts with StoreNew; use its config to build the // shared engine that all stores in this sequence will use. - let engine = match api.calls.first() { - Some(ApiCall::StoreNew { config, .. }) => Engine::new(&config.to_wasmtime()).unwrap(), + let (engine, gc_enabled) = match api.calls.first() { + Some(ApiCall::StoreNew { config, .. }) => ( + Engine::new(&config.to_wasmtime()).unwrap(), + config.module_config.config.gc_enabled, + ), _ => return, }; @@ -691,6 +694,9 @@ pub fn make_api_calls(api: ApiCalls) { Some(s) => s, None => continue, }; + if !gc_enabled { + continue; + } match Tag::new(&mut *st, &tt) { Ok(t) => { tags.insert(id, (t, store)); @@ -756,6 +762,65 @@ pub fn make_api_calls(api: ApiCalls) { log::trace!("dropping tag {id}"); tags.remove(&id); } + + ApiCall::ModuleImports { module } => { + log::trace!("iterating imports of module {module}"); + let m = match modules.get(&module) { + Some(m) => m, + None => continue, + }; + for _ in m.imports() {} + } + + ApiCall::ModuleExports { module } => { + log::trace!("iterating exports of module {module}"); + let m = match modules.get(&module) { + Some(m) => m, + None => continue, + }; + for _ in m.exports() {} + } + + ApiCall::ModuleGetExport { module, ref name } => { + log::trace!("getting export {name:?} of module {module}"); + let m = match modules.get(&module) { + Some(m) => m, + None => continue, + }; + let _ = m.get_export(name); + } + + ApiCall::ModuleName { module } => { + log::trace!("getting name of module {module}"); + let m = match modules.get(&module) { + Some(m) => m, + None => continue, + }; + let _ = m.name(); + } + + ApiCall::ModuleValidate { ref wasm } => { + log::trace!("validating {} bytes of wasm", wasm.len()); + let _ = Module::validate(&engine, wasm); + } + + ApiCall::ModuleSerializeDeserialize { src_id, dst_id } => { + log::trace!("serializing module {src_id} and deserializing as {dst_id}"); + let src = match modules.get(&src_id) { + Some(m) => m, + None => continue, + }; + let bytes = match src.serialize() { + Ok(b) => b, + Err(_) => continue, + }; + match unsafe { Module::deserialize(&engine, &bytes) } { + Ok(m) => { + modules.insert(dst_id, m); + } + Err(_) => continue, + } + } } } } From 67d5ab29b95b8613f0bd92303827a79bd42ddcf9 Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Thu, 21 May 2026 07:41:46 -0700 Subject: [PATCH 3/7] Add advanced table operation coverage to the public API fuzzer --- crates/fuzzing/src/generators/api.rs | 67 ++++++++++++++++++++++++++++ crates/fuzzing/src/oracles/api.rs | 45 +++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/crates/fuzzing/src/generators/api.rs b/crates/fuzzing/src/generators/api.rs index 2dc452c998c0..dfa1ac0a4dc5 100644 --- a/crates/fuzzing/src/generators/api.rs +++ b/crates/fuzzing/src/generators/api.rs @@ -103,6 +103,8 @@ struct Swarm { module_name: bool, module_validate: bool, module_serialize_deserialize: bool, + table_copy: bool, + table_fill: bool, } /// A call to one of Wasmtime's public APIs. @@ -333,6 +335,18 @@ pub enum ApiCall { src_id: usize, dst_id: usize, }, + TableCopy { + dst_table: usize, + dst_index: u64, + src_table: usize, + src_index: u64, + len: u64, + }, + TableFill { + table: usize, + dst: u64, + len: u64, + }, } use ApiCall::*; @@ -996,6 +1010,59 @@ impl<'a> Arbitrary<'a> for ApiCalls { Ok(ModuleSerializeDeserialize { src_id, dst_id }) }); } + if swarm.table_copy && scope.tables.len() >= 2 { + choices.push(|input, scope| { + // Find two table ids that map to the same store. + let by_store: std::collections::BTreeMap> = scope + .tables + .iter() + .fold(Default::default(), |mut m, (&tid, &sid)| { + m.entry(sid).or_default().push(tid); + m + }); + // Only proceed if at least one store has 2+ tables. + let valid_stores: Vec<_> = + by_store.iter().filter(|(_, ts)| ts.len() >= 2).collect(); + if valid_stores.is_empty() { + // Fall back: pick any two tables (may be different stores; oracle skips). + let tables: Vec<_> = scope.tables.keys().collect(); + let dst_table = **input.choose(&tables)?; + let src_table = **input.choose(&tables)?; + let dst_index = u64::arbitrary(input)?; + let src_index = u64::arbitrary(input)?; + let len = u64::arbitrary(input)? % 8; + return Ok(TableCopy { + dst_table, + dst_index, + src_table, + src_index, + len, + }); + } + let (_, store_tables) = *input.choose(&valid_stores)?; + let dst_table = *input.choose(store_tables)?; + let src_table = *input.choose(store_tables)?; + let dst_index = u64::arbitrary(input)?; + let src_index = u64::arbitrary(input)?; + let len = u64::arbitrary(input)? % 8; + Ok(TableCopy { + dst_table, + dst_index, + src_table, + src_index, + len, + }) + }); + } + if swarm.table_fill && !scope.tables.is_empty() { + choices.push(|input, scope| { + let tables: Vec<_> = scope.tables.keys().collect(); + let table = **input.choose(&tables)?; + let dst = u64::arbitrary(input)?; + let len = u64::arbitrary(input)? % 8; + Ok(TableFill { table, dst, len }) + }); + } if choices.is_empty() { break; diff --git a/crates/fuzzing/src/oracles/api.rs b/crates/fuzzing/src/oracles/api.rs index 672802ae8b1e..e39210fbb419 100644 --- a/crates/fuzzing/src/oracles/api.rs +++ b/crates/fuzzing/src/oracles/api.rs @@ -821,6 +821,51 @@ pub fn make_api_calls(api: ApiCalls) { Err(_) => continue, } } + + ApiCall::TableCopy { + dst_table, + dst_index, + src_table, + src_index, + len, + } => { + log::trace!( + "copying table {src_table}[{src_index}..+{len}] to {dst_table}[{dst_index}]" + ); + let (dt, dst_store_id) = match tables.get(&dst_table) { + Some(&x) => x, + None => continue, + }; + let (st_tbl, src_store_id) = match tables.get(&src_table) { + Some(&x) => x, + None => continue, + }; + if dst_store_id != src_store_id { + continue; + } + let st = match stores.get_mut(&dst_store_id) { + Some(s) => s, + None => continue, + }; + let _ = Table::copy(&mut *st, &dt, dst_index, &st_tbl, src_index, len); + } + + ApiCall::TableFill { table, dst, len } => { + log::trace!("filling table {table}[{dst}..+{len}]"); + let (t, store_id) = match tables.get(&table) { + Some(&x) => x, + None => continue, + }; + let st = match stores.get_mut(&store_id) { + Some(s) => s, + None => continue, + }; + let ty = t.ty(&*st); + let elem_ty: ValType = ty.element().clone().into(); + if let Some(val) = elem_ty.default_value() { + let _ = t.fill(&mut *st, dst, val.ref_().unwrap(), len); + } + } } } } From 9f2e23a5fa07db1f447b1b69476e375690b74757 Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Thu, 21 May 2026 08:00:55 -0700 Subject: [PATCH 4/7] Add StructType/StructRefPre/StructRef coverage to the public API fuzzer --- crates/fuzzing/src/generators/api.rs | 139 ++++++++++++++++++++++++++ crates/fuzzing/src/oracles/api.rs | 141 +++++++++++++++++++++++++++ 2 files changed, 280 insertions(+) diff --git a/crates/fuzzing/src/generators/api.rs b/crates/fuzzing/src/generators/api.rs index dfa1ac0a4dc5..52e113f5cb8a 100644 --- a/crates/fuzzing/src/generators/api.rs +++ b/crates/fuzzing/src/generators/api.rs @@ -105,6 +105,15 @@ struct Swarm { module_serialize_deserialize: bool, table_copy: bool, table_fill: bool, + struct_type_new: bool, + struct_type_drop: bool, + struct_ref_pre_new: bool, + struct_ref_pre_drop: bool, + struct_ref_new: bool, + struct_ref_ty: bool, + struct_ref_field: bool, + struct_ref_set_field: bool, + struct_ref_drop: bool, } /// A call to one of Wasmtime's public APIs. @@ -347,6 +356,39 @@ pub enum ApiCall { dst: u64, len: u64, }, + StructTypeNew { + id: usize, + fields: Vec<(FuzzValType, bool)>, + }, + StructTypeDrop { + id: usize, + }, + StructRefPreNew { + id: usize, + struct_ty: usize, + store: usize, + }, + StructRefPreDrop { + id: usize, + }, + StructRefNew { + id: usize, + pre: usize, + }, + StructRefTy { + struct_ref: usize, + }, + StructRefField { + struct_ref: usize, + index: usize, + }, + StructRefSetField { + struct_ref: usize, + index: usize, + }, + StructRefDrop { + id: usize, + }, } use ApiCall::*; @@ -398,6 +440,15 @@ struct Scope { /// associated `store_id`. tags: BTreeMap, // tag_id -> store_id + /// Struct types that are currently live. + struct_types: BTreeSet, + + /// StructRefPres that are currently live. Maps from `pre_id` to the store's id. + struct_ref_pres: BTreeMap, // pre_id -> store_id + + /// StructRefs that are currently live. Maps from `ref_id` to the store's id. + struct_refs: BTreeMap, // ref_id -> store_id + config: Config, } @@ -439,6 +490,9 @@ impl<'a> Arbitrary<'a> for ApiCalls { funcs: BTreeMap::default(), tag_types: BTreeSet::default(), tags: BTreeMap::default(), + struct_types: BTreeSet::default(), + struct_ref_pres: BTreeMap::default(), + struct_refs: BTreeMap::default(), config: config.clone(), }; @@ -480,6 +534,8 @@ impl<'a> Arbitrary<'a> for ApiCalls { scope.memories.retain(|_, store_id| *store_id != id); scope.funcs.retain(|_, store_id| *store_id != id); scope.tags.retain(|_, store_id| *store_id != id); + scope.struct_ref_pres.retain(|_, store_id| *store_id != id); + scope.struct_refs.retain(|_, store_id| *store_id != id); Ok(StoreDrop { id }) }); } @@ -1063,6 +1119,89 @@ impl<'a> Arbitrary<'a> for ApiCalls { Ok(TableFill { table, dst, len }) }); } + if swarm.struct_type_new { + choices.push(|input, scope| { + let id = scope.next_id(); + let fields = Vec::<(FuzzValType, bool)>::arbitrary(input)?; + let fields: Vec<_> = fields.into_iter().take(4).collect(); + scope.struct_types.insert(id); + Ok(StructTypeNew { id, fields }) + }); + } + if swarm.struct_type_drop && !scope.struct_types.is_empty() { + choices.push(|input, scope| { + let types: Vec<_> = scope.struct_types.iter().collect(); + let id = **input.choose(&types)?; + scope.struct_types.remove(&id); + Ok(StructTypeDrop { id }) + }); + } + if swarm.struct_ref_pre_new + && !scope.struct_types.is_empty() + && !scope.stores.is_empty() + { + choices.push(|input, scope| { + let types: Vec<_> = scope.struct_types.iter().collect(); + let struct_ty = **input.choose(&types)?; + let stores: Vec<_> = scope.stores.iter().collect(); + let store = **input.choose(&stores)?; + let id = scope.next_id(); + scope.struct_ref_pres.insert(id, store); + Ok(StructRefPreNew { + id, + struct_ty, + store, + }) + }); + } + if swarm.struct_ref_pre_drop && !scope.struct_ref_pres.is_empty() { + choices.push(|input, scope| { + let pres: Vec<_> = scope.struct_ref_pres.keys().collect(); + let id = **input.choose(&pres)?; + scope.struct_ref_pres.remove(&id); + Ok(StructRefPreDrop { id }) + }); + } + if swarm.struct_ref_new && !scope.struct_ref_pres.is_empty() { + choices.push(|input, scope| { + let pres: Vec<_> = scope.struct_ref_pres.iter().collect(); + let (&pre, &store_id) = *input.choose(&pres)?; + let id = scope.next_id(); + scope.struct_refs.insert(id, store_id); + Ok(StructRefNew { id, pre }) + }); + } + if swarm.struct_ref_ty && !scope.struct_refs.is_empty() { + choices.push(|input, scope| { + let refs: Vec<_> = scope.struct_refs.keys().collect(); + let struct_ref = **input.choose(&refs)?; + Ok(StructRefTy { struct_ref }) + }); + } + if swarm.struct_ref_field && !scope.struct_refs.is_empty() { + choices.push(|input, scope| { + let refs: Vec<_> = scope.struct_refs.keys().collect(); + let struct_ref = **input.choose(&refs)?; + let index = usize::arbitrary(input)?; + Ok(StructRefField { struct_ref, index }) + }); + } + if swarm.struct_ref_set_field && !scope.struct_refs.is_empty() { + choices.push(|input, scope| { + let refs: Vec<_> = scope.struct_refs.keys().collect(); + let struct_ref = **input.choose(&refs)?; + let index = usize::arbitrary(input)?; + Ok(StructRefSetField { struct_ref, index }) + }); + } + if swarm.struct_ref_drop && !scope.struct_refs.is_empty() { + choices.push(|input, scope| { + let refs: Vec<_> = scope.struct_refs.keys().collect(); + let id = **input.choose(&refs)?; + scope.struct_refs.remove(&id); + Ok(StructRefDrop { id }) + }); + } if choices.is_empty() { break; diff --git a/crates/fuzzing/src/oracles/api.rs b/crates/fuzzing/src/oracles/api.rs index e39210fbb419..13897f29b579 100644 --- a/crates/fuzzing/src/oracles/api.rs +++ b/crates/fuzzing/src/oracles/api.rs @@ -28,6 +28,9 @@ pub fn make_api_calls(api: ApiCalls) { let mut funcs: HashMap = Default::default(); let mut tag_types: HashMap = Default::default(); let mut tags: HashMap = Default::default(); + let mut struct_types: HashMap = Default::default(); + let mut struct_ref_pres: HashMap = Default::default(); + let mut struct_refs: HashMap, usize)> = Default::default(); for call in api.calls { match call { @@ -47,6 +50,8 @@ pub fn make_api_calls(api: ApiCalls) { memories.retain(|_, (_, store_id)| *store_id != id); funcs.retain(|_, (_, store_id)| *store_id != id); tags.retain(|_, (_, store_id)| *store_id != id); + struct_ref_pres.retain(|_, (_, _, store_id)| *store_id != id); + struct_refs.retain(|_, (_, store_id)| *store_id != id); stores.remove(&id); } @@ -866,6 +871,142 @@ pub fn make_api_calls(api: ApiCalls) { let _ = t.fill(&mut *st, dst, val.ref_().unwrap(), len); } } + + ApiCall::StructTypeNew { id, ref fields } => { + log::trace!("creating struct type {id}"); + if !gc_enabled { + continue; + } + let field_types = fields.iter().map(|(fvt, mutable)| { + let mutability = if *mutable { + Mutability::Var + } else { + Mutability::Const + }; + FieldType::new(mutability, StorageType::ValType(fvt.to_wasmtime())) + }); + match StructType::new(&engine, field_types) { + Ok(st) => { + struct_types.insert(id, st); + } + Err(_) => continue, + } + } + + ApiCall::StructTypeDrop { id } => { + log::trace!("dropping struct type {id}"); + struct_types.remove(&id); + } + + ApiCall::StructRefPreNew { + id, + struct_ty, + store, + } => { + log::trace!("creating struct ref pre {id} with type {struct_ty} in store {store}"); + let sty = match struct_types.get(&struct_ty) { + Some(t) => t.clone(), + None => continue, + }; + let st = match stores.get_mut(&store) { + Some(s) => s, + None => continue, + }; + if !gc_enabled { + continue; + } + let pre = StructRefPre::new(&mut *st, sty.clone()); + struct_ref_pres.insert(id, (pre, sty, store)); + } + + ApiCall::StructRefPreDrop { id } => { + log::trace!("dropping struct ref pre {id}"); + struct_ref_pres.remove(&id); + } + + ApiCall::StructRefNew { id, pre } => { + log::trace!("creating struct ref {id} from pre {pre}"); + let (allocator, sty, store_id) = match struct_ref_pres.get(&pre) { + Some(x) => x, + None => continue, + }; + let store_id = *store_id; + let fields: Option> = sty + .fields() + .map(|f| f.element_type().unpack().default_value()) + .collect(); + let fields = match fields { + Some(f) => f, + None => continue, + }; + let st = match stores.get_mut(&store_id) { + Some(s) => s, + None => continue, + }; + let mut st = RootScope::new(st); + match StructRef::new(&mut st, allocator, &fields) { + Ok(r) => { + struct_refs.insert(id, (r.to_owned_rooted(st).unwrap(), store_id)); + } + Err(_) => continue, + } + } + + ApiCall::StructRefTy { struct_ref } => { + log::trace!("getting type of struct ref {struct_ref}"); + let (r, store_id) = match struct_refs.get(&struct_ref) { + Some(x) => x, + None => continue, + }; + let st = match stores.get(&store_id) { + Some(s) => s, + None => continue, + }; + let _ = r.ty(st); + } + + ApiCall::StructRefField { struct_ref, index } => { + log::trace!("getting field {index} of struct ref {struct_ref}"); + let (r, store_id) = match struct_refs.get(&struct_ref) { + Some(x) => x, + None => continue, + }; + let st = match stores.get_mut(&store_id) { + Some(s) => s, + None => continue, + }; + let _ = r.field(&mut *st, index); + } + + ApiCall::StructRefSetField { struct_ref, index } => { + log::trace!("setting field {index} of struct ref {struct_ref}"); + let (r, store_id) = match struct_refs.get(&struct_ref) { + Some(x) => x, + None => continue, + }; + let st = match stores.get_mut(&store_id) { + Some(s) => s, + None => continue, + }; + let sty = match r.ty(&*st) { + Ok(t) => t, + Err(_) => continue, + }; + let field_tys: Vec<_> = sty.fields().collect(); + if index >= field_tys.len() { + continue; + } + let val = match field_tys[index].element_type().unpack().default_value() { + Some(v) => v, + None => continue, + }; + let _ = r.set_field(&mut *st, index, val); + } + + ApiCall::StructRefDrop { id } => { + log::trace!("dropping struct ref {id}"); + struct_refs.remove(&id); + } } } } From 818f0304df3c4a04800e41f38578956a46d1ce35 Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Thu, 21 May 2026 08:20:12 -0700 Subject: [PATCH 5/7] Add array-related API coverage to the public API fuzzer --- crates/fuzzing/src/generators/api.rs | 171 +++++++++++++++++++++++++++ crates/fuzzing/src/oracles/api.rs | 171 +++++++++++++++++++++++++++ 2 files changed, 342 insertions(+) diff --git a/crates/fuzzing/src/generators/api.rs b/crates/fuzzing/src/generators/api.rs index 52e113f5cb8a..3756d4b1780d 100644 --- a/crates/fuzzing/src/generators/api.rs +++ b/crates/fuzzing/src/generators/api.rs @@ -114,6 +114,17 @@ struct Swarm { struct_ref_field: bool, struct_ref_set_field: bool, struct_ref_drop: bool, + array_type_new: bool, + array_type_drop: bool, + array_ref_pre_new: bool, + array_ref_pre_drop: bool, + array_ref_new: bool, + array_ref_new_fixed: bool, + array_ref_ty: bool, + array_ref_len: bool, + array_ref_get: bool, + array_ref_set: bool, + array_ref_drop: bool, } /// A call to one of Wasmtime's public APIs. @@ -389,6 +400,49 @@ pub enum ApiCall { StructRefDrop { id: usize, }, + ArrayTypeNew { + id: usize, + elem_ty: FuzzValType, + mutable: bool, + }, + ArrayTypeDrop { + id: usize, + }, + ArrayRefPreNew { + id: usize, + array_ty: usize, + store: usize, + }, + ArrayRefPreDrop { + id: usize, + }, + ArrayRefNew { + id: usize, + pre: usize, + len: u32, + }, + ArrayRefNewFixed { + id: usize, + pre: usize, + count: u8, + }, + ArrayRefTy { + array_ref: usize, + }, + ArrayRefLen { + array_ref: usize, + }, + ArrayRefGet { + array_ref: usize, + index: u32, + }, + ArrayRefSet { + array_ref: usize, + index: u32, + }, + ArrayRefDrop { + id: usize, + }, } use ApiCall::*; @@ -449,6 +503,15 @@ struct Scope { /// StructRefs that are currently live. Maps from `ref_id` to the store's id. struct_refs: BTreeMap, // ref_id -> store_id + /// Array types that are currently live. + array_types: BTreeSet, + + /// ArrayRefPres that are currently live. Maps from `pre_id` to the store's id. + array_ref_pres: BTreeMap, // pre_id -> store_id + + /// ArrayRefs that are currently live. Maps from `ref_id` to the store's id. + array_refs: BTreeMap, // ref_id -> store_id + config: Config, } @@ -493,6 +556,9 @@ impl<'a> Arbitrary<'a> for ApiCalls { struct_types: BTreeSet::default(), struct_ref_pres: BTreeMap::default(), struct_refs: BTreeMap::default(), + array_types: BTreeSet::default(), + array_ref_pres: BTreeMap::default(), + array_refs: BTreeMap::default(), config: config.clone(), }; @@ -536,6 +602,8 @@ impl<'a> Arbitrary<'a> for ApiCalls { scope.tags.retain(|_, store_id| *store_id != id); scope.struct_ref_pres.retain(|_, store_id| *store_id != id); scope.struct_refs.retain(|_, store_id| *store_id != id); + scope.array_ref_pres.retain(|_, store_id| *store_id != id); + scope.array_refs.retain(|_, store_id| *store_id != id); Ok(StoreDrop { id }) }); } @@ -1202,6 +1270,109 @@ impl<'a> Arbitrary<'a> for ApiCalls { Ok(StructRefDrop { id }) }); } + if swarm.array_type_new { + choices.push(|input, scope| { + let id = scope.next_id(); + let elem_ty = FuzzValType::arbitrary(input)?; + let mutable = bool::arbitrary(input)?; + scope.array_types.insert(id); + Ok(ArrayTypeNew { + id, + elem_ty, + mutable, + }) + }); + } + if swarm.array_type_drop && !scope.array_types.is_empty() { + choices.push(|input, scope| { + let types: Vec<_> = scope.array_types.iter().collect(); + let id = **input.choose(&types)?; + scope.array_types.remove(&id); + Ok(ArrayTypeDrop { id }) + }); + } + if swarm.array_ref_pre_new && !scope.array_types.is_empty() && !scope.stores.is_empty() + { + choices.push(|input, scope| { + let types: Vec<_> = scope.array_types.iter().collect(); + let array_ty = **input.choose(&types)?; + let stores: Vec<_> = scope.stores.iter().collect(); + let store = **input.choose(&stores)?; + let id = scope.next_id(); + scope.array_ref_pres.insert(id, store); + Ok(ArrayRefPreNew { + id, + array_ty, + store, + }) + }); + } + if swarm.array_ref_pre_drop && !scope.array_ref_pres.is_empty() { + choices.push(|input, scope| { + let pres: Vec<_> = scope.array_ref_pres.keys().collect(); + let id = **input.choose(&pres)?; + scope.array_ref_pres.remove(&id); + Ok(ArrayRefPreDrop { id }) + }); + } + if swarm.array_ref_new && !scope.array_ref_pres.is_empty() { + choices.push(|input, scope| { + let pres: Vec<_> = scope.array_ref_pres.iter().collect(); + let (&pre, &store_id) = *input.choose(&pres)?; + let id = scope.next_id(); + let len = u32::arbitrary(input)? % 17; + scope.array_refs.insert(id, store_id); + Ok(ArrayRefNew { id, pre, len }) + }); + } + if swarm.array_ref_new_fixed && !scope.array_ref_pres.is_empty() { + choices.push(|input, scope| { + let pres: Vec<_> = scope.array_ref_pres.iter().collect(); + let (&pre, &store_id) = *input.choose(&pres)?; + let id = scope.next_id(); + let count = u8::arbitrary(input)? % 9; + scope.array_refs.insert(id, store_id); + Ok(ArrayRefNewFixed { id, pre, count }) + }); + } + if swarm.array_ref_ty && !scope.array_refs.is_empty() { + choices.push(|input, scope| { + let refs: Vec<_> = scope.array_refs.keys().collect(); + let array_ref = **input.choose(&refs)?; + Ok(ArrayRefTy { array_ref }) + }); + } + if swarm.array_ref_len && !scope.array_refs.is_empty() { + choices.push(|input, scope| { + let refs: Vec<_> = scope.array_refs.keys().collect(); + let array_ref = **input.choose(&refs)?; + Ok(ArrayRefLen { array_ref }) + }); + } + if swarm.array_ref_get && !scope.array_refs.is_empty() { + choices.push(|input, scope| { + let refs: Vec<_> = scope.array_refs.keys().collect(); + let array_ref = **input.choose(&refs)?; + let index = u32::arbitrary(input)?; + Ok(ArrayRefGet { array_ref, index }) + }); + } + if swarm.array_ref_set && !scope.array_refs.is_empty() { + choices.push(|input, scope| { + let refs: Vec<_> = scope.array_refs.keys().collect(); + let array_ref = **input.choose(&refs)?; + let index = u32::arbitrary(input)?; + Ok(ArrayRefSet { array_ref, index }) + }); + } + if swarm.array_ref_drop && !scope.array_refs.is_empty() { + choices.push(|input, scope| { + let refs: Vec<_> = scope.array_refs.keys().collect(); + let id = **input.choose(&refs)?; + scope.array_refs.remove(&id); + Ok(ArrayRefDrop { id }) + }); + } if choices.is_empty() { break; diff --git a/crates/fuzzing/src/oracles/api.rs b/crates/fuzzing/src/oracles/api.rs index 13897f29b579..57ac95afc817 100644 --- a/crates/fuzzing/src/oracles/api.rs +++ b/crates/fuzzing/src/oracles/api.rs @@ -31,6 +31,9 @@ pub fn make_api_calls(api: ApiCalls) { let mut struct_types: HashMap = Default::default(); let mut struct_ref_pres: HashMap = Default::default(); let mut struct_refs: HashMap, usize)> = Default::default(); + let mut array_types: HashMap = Default::default(); + let mut array_ref_pres: HashMap = Default::default(); + let mut array_refs: HashMap, usize)> = Default::default(); for call in api.calls { match call { @@ -52,6 +55,8 @@ pub fn make_api_calls(api: ApiCalls) { tags.retain(|_, (_, store_id)| *store_id != id); struct_ref_pres.retain(|_, (_, _, store_id)| *store_id != id); struct_refs.retain(|_, (_, store_id)| *store_id != id); + array_ref_pres.retain(|_, (_, _, store_id)| *store_id != id); + array_refs.retain(|_, (_, store_id)| *store_id != id); stores.remove(&id); } @@ -1007,6 +1012,172 @@ pub fn make_api_calls(api: ApiCalls) { log::trace!("dropping struct ref {id}"); struct_refs.remove(&id); } + + ApiCall::ArrayTypeNew { + id, + elem_ty, + mutable, + } => { + log::trace!("creating array type {id}"); + if !gc_enabled { + continue; + } + let mutability = if mutable { + Mutability::Var + } else { + Mutability::Const + }; + let ft = FieldType::new(mutability, StorageType::ValType(elem_ty.to_wasmtime())); + let at = ArrayType::new(&engine, ft); + array_types.insert(id, at); + } + + ApiCall::ArrayTypeDrop { id } => { + log::trace!("dropping array type {id}"); + array_types.remove(&id); + } + + ApiCall::ArrayRefPreNew { + id, + array_ty, + store, + } => { + log::trace!("creating array ref pre {id} with type {array_ty} in store {store}"); + let aty = match array_types.get(&array_ty) { + Some(t) => t.clone(), + None => continue, + }; + let st = match stores.get_mut(&store) { + Some(s) => s, + None => continue, + }; + if !gc_enabled { + continue; + } + let pre = ArrayRefPre::new(&mut *st, aty.clone()); + array_ref_pres.insert(id, (pre, aty, store)); + } + + ApiCall::ArrayRefPreDrop { id } => { + log::trace!("dropping array ref pre {id}"); + array_ref_pres.remove(&id); + } + + ApiCall::ArrayRefNew { id, pre, len } => { + log::trace!("creating array ref {id} from pre {pre} with len {len}"); + let (allocator, aty, store_id) = match array_ref_pres.get(&pre) { + Some(x) => x, + None => continue, + }; + let store_id = *store_id; + let len = len % 17; + let elem_val = match aty.field_type().element_type().unpack().default_value() { + Some(v) => v, + None => continue, + }; + let st = match stores.get_mut(&store_id) { + Some(s) => s, + None => continue, + }; + let mut st = RootScope::new(st); + match ArrayRef::new(&mut st, allocator, &elem_val, len) { + Ok(r) => { + array_refs.insert(id, (r.to_owned_rooted(st).unwrap(), store_id)); + } + Err(_) => continue, + } + } + + ApiCall::ArrayRefNewFixed { id, pre, count } => { + log::trace!("creating array ref {id} from pre {pre} with {count} elements"); + let (allocator, aty, store_id) = match array_ref_pres.get(&pre) { + Some(x) => x, + None => continue, + }; + let store_id = *store_id; + let count = count % 9; + let elem_val = match aty.field_type().element_type().unpack().default_value() { + Some(v) => v, + None => continue, + }; + let elems: Vec = (0..count).map(|_| elem_val.clone()).collect(); + let st = match stores.get_mut(&store_id) { + Some(s) => s, + None => continue, + }; + let mut st = RootScope::new(st); + match ArrayRef::new_fixed(&mut st, allocator, &elems) { + Ok(r) => { + array_refs.insert(id, (r.to_owned_rooted(st).unwrap(), store_id)); + } + Err(_) => continue, + } + } + + ApiCall::ArrayRefTy { array_ref } => { + log::trace!("getting type of array ref {array_ref}"); + let (r, store_id) = match array_refs.get(&array_ref) { + Some(x) => x, + None => continue, + }; + let st = match stores.get(&store_id) { + Some(s) => s, + None => continue, + }; + let _ = r.ty(st); + } + + ApiCall::ArrayRefLen { array_ref } => { + log::trace!("getting length of array ref {array_ref}"); + let (r, store_id) = match array_refs.get(&array_ref) { + Some(x) => x, + None => continue, + }; + let st = match stores.get(&store_id) { + Some(s) => s, + None => continue, + }; + let _ = r.len(st); + } + + ApiCall::ArrayRefGet { array_ref, index } => { + log::trace!("getting index {index} of array ref {array_ref}"); + let (r, store_id) = match array_refs.get(&array_ref) { + Some(x) => x, + None => continue, + }; + let st = match stores.get_mut(&store_id) { + Some(s) => s, + None => continue, + }; + let _ = r.get(&mut *st, index); + } + + ApiCall::ArrayRefSet { array_ref, index } => { + log::trace!("setting index {index} of array ref {array_ref}"); + let (r, store_id) = match array_refs.get(&array_ref) { + Some(x) => x, + None => continue, + }; + let st = match stores.get_mut(&store_id) { + Some(s) => s, + None => continue, + }; + let aty = match r.ty(&*st) { + Ok(t) => t, + Err(_) => continue, + }; + let val = match aty.field_type().element_type().unpack().default_value() { + Some(v) => v, + None => continue, + }; + let _ = r.set(&mut *st, index, val); + } + + ApiCall::ArrayRefDrop { id } => { + log::trace!("dropping array ref {id}"); + array_refs.remove(&id); + } } } } From ad21eb71367872fbe2f94e24417f7c7e27718107 Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Thu, 21 May 2026 08:41:40 -0700 Subject: [PATCH 6/7] Add exception-related API coverage to the public API fuzzer --- crates/fuzzing/src/generators/api.rs | 160 +++++++++++++++++++++++++++ crates/fuzzing/src/oracles/api.rs | 141 +++++++++++++++++++++++ 2 files changed, 301 insertions(+) diff --git a/crates/fuzzing/src/generators/api.rs b/crates/fuzzing/src/generators/api.rs index 3756d4b1780d..839e74988d9f 100644 --- a/crates/fuzzing/src/generators/api.rs +++ b/crates/fuzzing/src/generators/api.rs @@ -125,6 +125,16 @@ struct Swarm { array_ref_get: bool, array_ref_set: bool, array_ref_drop: bool, + exn_type_new: bool, + exn_type_from_tag_type: bool, + exn_type_drop: bool, + exn_ref_pre_new: bool, + exn_ref_pre_drop: bool, + exn_ref_new: bool, + exn_ref_ty: bool, + exn_ref_tag: bool, + exn_ref_field: bool, + exn_ref_drop: bool, } /// A call to one of Wasmtime's public APIs. @@ -443,6 +453,43 @@ pub enum ApiCall { ArrayRefDrop { id: usize, }, + ExnTypeNew { + id: usize, + fields: Vec, + }, + ExnTypeFromTagType { + id: usize, + tag_ty: usize, + }, + ExnTypeDrop { + id: usize, + }, + ExnRefPreNew { + id: usize, + exn_ty: usize, + store: usize, + }, + ExnRefPreDrop { + id: usize, + }, + ExnRefNew { + id: usize, + pre: usize, + tag: usize, + }, + ExnRefTy { + exn_ref: usize, + }, + ExnRefTag { + exn_ref: usize, + }, + ExnRefField { + exn_ref: usize, + index: usize, + }, + ExnRefDrop { + id: usize, + }, } use ApiCall::*; @@ -512,6 +559,15 @@ struct Scope { /// ArrayRefs that are currently live. Maps from `ref_id` to the store's id. array_refs: BTreeMap, // ref_id -> store_id + /// Exception types that are currently live. + exn_types: BTreeSet, + + /// ExnRefPres that are currently live. Maps from `pre_id` to the store's id. + exn_ref_pres: BTreeMap, // pre_id -> store_id + + /// ExnRefs that are currently live. Maps from `ref_id` to the store's id. + exn_refs: BTreeMap, // ref_id -> store_id + config: Config, } @@ -559,6 +615,9 @@ impl<'a> Arbitrary<'a> for ApiCalls { array_types: BTreeSet::default(), array_ref_pres: BTreeMap::default(), array_refs: BTreeMap::default(), + exn_types: BTreeSet::default(), + exn_ref_pres: BTreeMap::default(), + exn_refs: BTreeMap::default(), config: config.clone(), }; @@ -604,6 +663,8 @@ impl<'a> Arbitrary<'a> for ApiCalls { scope.struct_refs.retain(|_, store_id| *store_id != id); scope.array_ref_pres.retain(|_, store_id| *store_id != id); scope.array_refs.retain(|_, store_id| *store_id != id); + scope.exn_ref_pres.retain(|_, store_id| *store_id != id); + scope.exn_refs.retain(|_, store_id| *store_id != id); Ok(StoreDrop { id }) }); } @@ -1373,6 +1434,105 @@ impl<'a> Arbitrary<'a> for ApiCalls { Ok(ArrayRefDrop { id }) }); } + if swarm.exn_type_new { + choices.push(|input, scope| { + let id = scope.next_id(); + let fields = Vec::::arbitrary(input)?; + let fields: Vec<_> = fields.into_iter().take(4).collect(); + scope.exn_types.insert(id); + Ok(ExnTypeNew { id, fields }) + }); + } + if swarm.exn_type_from_tag_type && !scope.tag_types.is_empty() { + choices.push(|input, scope| { + let types: Vec<_> = scope.tag_types.iter().collect(); + let tag_ty = **input.choose(&types)?; + let id = scope.next_id(); + scope.exn_types.insert(id); + Ok(ExnTypeFromTagType { id, tag_ty }) + }); + } + if swarm.exn_type_drop && !scope.exn_types.is_empty() { + choices.push(|input, scope| { + let types: Vec<_> = scope.exn_types.iter().collect(); + let id = **input.choose(&types)?; + scope.exn_types.remove(&id); + Ok(ExnTypeDrop { id }) + }); + } + if swarm.exn_ref_pre_new && !scope.exn_types.is_empty() && !scope.stores.is_empty() { + choices.push(|input, scope| { + let types: Vec<_> = scope.exn_types.iter().collect(); + let exn_ty = **input.choose(&types)?; + let stores: Vec<_> = scope.stores.iter().collect(); + let store = **input.choose(&stores)?; + let id = scope.next_id(); + scope.exn_ref_pres.insert(id, store); + Ok(ExnRefPreNew { id, exn_ty, store }) + }); + } + if swarm.exn_ref_pre_drop && !scope.exn_ref_pres.is_empty() { + choices.push(|input, scope| { + let pres: Vec<_> = scope.exn_ref_pres.keys().collect(); + let id = **input.choose(&pres)?; + scope.exn_ref_pres.remove(&id); + Ok(ExnRefPreDrop { id }) + }); + } + if swarm.exn_ref_new + && scope + .exn_ref_pres + .values() + .any(|sid| scope.tags.values().any(|tsid| tsid == sid)) + { + choices.push(|input, scope| { + let pres_with_tags: Vec<_> = scope + .exn_ref_pres + .iter() + .filter(|&(_, &sid)| scope.tags.values().any(|&tsid| tsid == sid)) + .collect(); + let (&pre, &store_id) = *input.choose(&pres_with_tags)?; + let same_store_tags: Vec<_> = scope + .tags + .iter() + .filter(|&(_, &sid)| sid == store_id) + .collect(); + let (&tag, _) = *input.choose(&same_store_tags)?; + let id = scope.next_id(); + scope.exn_refs.insert(id, store_id); + Ok(ExnRefNew { id, pre, tag }) + }); + } + if swarm.exn_ref_ty && !scope.exn_refs.is_empty() { + choices.push(|input, scope| { + let refs: Vec<_> = scope.exn_refs.keys().collect(); + let exn_ref = **input.choose(&refs)?; + Ok(ExnRefTy { exn_ref }) + }); + } + if swarm.exn_ref_tag && !scope.exn_refs.is_empty() { + choices.push(|input, scope| { + let refs: Vec<_> = scope.exn_refs.keys().collect(); + let exn_ref = **input.choose(&refs)?; + Ok(ExnRefTag { exn_ref }) + }); + } + if swarm.exn_ref_field && !scope.exn_refs.is_empty() { + choices.push(|input, scope| { + let refs: Vec<_> = scope.exn_refs.keys().collect(); + let exn_ref = **input.choose(&refs)?; + let index = usize::arbitrary(input)?; + Ok(ExnRefField { exn_ref, index }) + }); + } + if swarm.exn_ref_drop && !scope.exn_refs.is_empty() { + choices.push(|input, scope| { + let refs: Vec<_> = scope.exn_refs.keys().collect(); + let id = **input.choose(&refs)?; + scope.exn_refs.remove(&id); + Ok(ExnRefDrop { id }) + }); + } if choices.is_empty() { break; diff --git a/crates/fuzzing/src/oracles/api.rs b/crates/fuzzing/src/oracles/api.rs index 57ac95afc817..470ca41a3a87 100644 --- a/crates/fuzzing/src/oracles/api.rs +++ b/crates/fuzzing/src/oracles/api.rs @@ -34,6 +34,9 @@ pub fn make_api_calls(api: ApiCalls) { let mut array_types: HashMap = Default::default(); let mut array_ref_pres: HashMap = Default::default(); let mut array_refs: HashMap, usize)> = Default::default(); + let mut exn_types: HashMap = Default::default(); + let mut exn_ref_pres: HashMap = Default::default(); + let mut exn_refs: HashMap, usize)> = Default::default(); for call in api.calls { match call { @@ -57,6 +60,8 @@ pub fn make_api_calls(api: ApiCalls) { struct_refs.retain(|_, (_, store_id)| *store_id != id); array_ref_pres.retain(|_, (_, _, store_id)| *store_id != id); array_refs.retain(|_, (_, store_id)| *store_id != id); + exn_ref_pres.retain(|_, (_, _, store_id)| *store_id != id); + exn_refs.retain(|_, (_, store_id)| *store_id != id); stores.remove(&id); } @@ -1178,6 +1183,142 @@ pub fn make_api_calls(api: ApiCalls) { log::trace!("dropping array ref {id}"); array_refs.remove(&id); } + + ApiCall::ExnTypeNew { id, ref fields } => { + log::trace!("creating exn type {id}"); + if !gc_enabled { + continue; + } + match ExnType::new(&engine, fields.iter().map(|f| f.to_wasmtime())) { + Ok(et) => { + exn_types.insert(id, et); + } + Err(_) => continue, + } + } + + ApiCall::ExnTypeFromTagType { id, tag_ty } => { + log::trace!("creating exn type {id} from tag type {tag_ty}"); + if !gc_enabled { + continue; + } + let tt = match tag_types.get(&tag_ty) { + Some(t) => t, + None => continue, + }; + match ExnType::from_tag_type(tt) { + Ok(et) => { + exn_types.insert(id, et); + } + Err(_) => continue, + } + } + + ApiCall::ExnTypeDrop { id } => { + log::trace!("dropping exn type {id}"); + exn_types.remove(&id); + } + + ApiCall::ExnRefPreNew { id, exn_ty, store } => { + log::trace!("creating exn ref pre {id} with type {exn_ty} in store {store}"); + let ety = match exn_types.get(&exn_ty) { + Some(t) => t.clone(), + None => continue, + }; + let st = match stores.get_mut(&store) { + Some(s) => s, + None => continue, + }; + if !gc_enabled { + continue; + } + let pre = ExnRefPre::new(&mut *st, ety.clone()); + exn_ref_pres.insert(id, (pre, ety, store)); + } + + ApiCall::ExnRefPreDrop { id } => { + log::trace!("dropping exn ref pre {id}"); + exn_ref_pres.remove(&id); + } + + ApiCall::ExnRefNew { id, pre, tag } => { + log::trace!("creating exn ref {id} from pre {pre} with tag {tag}"); + let (allocator, ety, store_id) = match exn_ref_pres.get(&pre) { + Some(x) => x, + None => continue, + }; + let store_id = *store_id; + let (t, tag_store_id) = match tags.get(&tag) { + Some(&x) => x, + None => continue, + }; + if store_id != tag_store_id { + continue; + } + let fields: Option> = ety + .fields() + .map(|f| f.element_type().unpack().default_value()) + .collect(); + let fields = match fields { + Some(f) => f, + None => continue, + }; + let st = match stores.get_mut(&store_id) { + Some(s) => s, + None => continue, + }; + let mut st = RootScope::new(st); + match ExnRef::new(&mut st, allocator, &t, &fields) { + Ok(r) => { + exn_refs.insert(id, (r.to_owned_rooted(st).unwrap(), store_id)); + } + Err(_) => continue, + } + } + + ApiCall::ExnRefTy { exn_ref } => { + log::trace!("getting type of exn ref {exn_ref}"); + let (r, store_id) = match exn_refs.get(&exn_ref) { + Some(x) => x, + None => continue, + }; + let st = match stores.get(&store_id) { + Some(s) => s, + None => continue, + }; + let _ = r.ty(st); + } + + ApiCall::ExnRefTag { exn_ref } => { + log::trace!("getting tag of exn ref {exn_ref}"); + let (r, store_id) = match exn_refs.get(&exn_ref) { + Some(x) => x, + None => continue, + }; + let st = match stores.get_mut(&store_id) { + Some(s) => s, + None => continue, + }; + let _ = r.tag(&mut *st); + } + + ApiCall::ExnRefField { exn_ref, index } => { + log::trace!("getting field {index} of exn ref {exn_ref}"); + let (r, store_id) = match exn_refs.get(&exn_ref) { + Some(x) => x, + None => continue, + }; + let st = match stores.get_mut(&store_id) { + Some(s) => s, + None => continue, + }; + let _ = r.field(&mut *st, index); + } + + ApiCall::ExnRefDrop { id } => { + log::trace!("dropping exn ref {id}"); + exn_refs.remove(&id); + } } } } From 9a60a792a7154da3ba45c394f1b95e50dc405541 Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Fri, 22 May 2026 11:54:11 -0700 Subject: [PATCH 7/7] Add support for collecting garbage to the public API fuzzer --- crates/fuzzing/src/generators/api.rs | 11 +++++++++++ crates/fuzzing/src/oracles/api.rs | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/crates/fuzzing/src/generators/api.rs b/crates/fuzzing/src/generators/api.rs index 839e74988d9f..fcf69f987b08 100644 --- a/crates/fuzzing/src/generators/api.rs +++ b/crates/fuzzing/src/generators/api.rs @@ -45,6 +45,7 @@ impl FuzzValType { struct Swarm { store_new: bool, store_drop: bool, + store_gc: bool, module_new: bool, module_drop: bool, instance_new: bool, @@ -148,6 +149,9 @@ pub enum ApiCall { StoreDrop { id: usize, }, + StoreGc { + id: usize, + }, ModuleNew { id: usize, wasm: Vec, @@ -668,6 +672,13 @@ impl<'a> Arbitrary<'a> for ApiCalls { Ok(StoreDrop { id }) }); } + if swarm.store_gc && !scope.stores.is_empty() { + choices.push(|input, scope| { + let stores: Vec<_> = scope.stores.iter().collect(); + let id = **input.choose(&stores)?; + Ok(StoreGc { id }) + }); + } if swarm.module_new { choices.push(|input, scope| { let id = scope.next_id(); diff --git a/crates/fuzzing/src/oracles/api.rs b/crates/fuzzing/src/oracles/api.rs index 470ca41a3a87..0c07ee39b8d4 100644 --- a/crates/fuzzing/src/oracles/api.rs +++ b/crates/fuzzing/src/oracles/api.rs @@ -65,6 +65,15 @@ pub fn make_api_calls(api: ApiCalls) { stores.remove(&id); } + ApiCall::StoreGc { id } => { + log::trace!("collecting garbage in store {id}"); + let st = match stores.get_mut(&id) { + Some(s) => s, + None => continue, + }; + let _ = st.gc(None); + } + ApiCall::ModuleNew { id, wasm } => { log::debug!("creating module: {id}"); log_wasm(&wasm);