Repository: bytecodealliance/wasm-tools
Pull request #1418 (2024-02-15 15:42:21)

Add `exports` field to `Config`
Add an optional `exports` field to `Config` which describes exactly which exports the generated module should have.

This is similar to the solution described [here](https://github.com/bytecodealliance/wasm-tools/issues/1405#issuecomment-1934328598).

adambratschikaye: This is failing on a seemingly unrelated test for `wit-parser`, but I also get that same failure on `main` commit https://github.com/bytecodealliance/wasm-tools/commit/2573bb28cc897d64ce33739e3b46ac63c9588267.
alexcrichton: Ah yeah no worries on the wit-parser failure, that's due to https://github.com/bytecodealliance/wasm-tools/pull/1419
alexcrichton: Also, as a minor note, after https://github.com/bytecodealliance/wasm-tools/pull/1416 we're changing a bit how CI works on this repository, so CI won't fully finish on this PR just yet until it's rebased. After rebasing on `main` though it should be good to go.

Patch:
diff --git a/crates/wasm-smith/src/config.rs b/crates/wasm-smith/src/config.rs
index 6efe88d9..43dbecce 100644
--- a/crates/wasm-smith/src/config.rs
+++ b/crates/wasm-smith/src/config.rs
@@ -20,13 +20,13 @@ macro_rules! define_config {
             /// Defaults to `None` which means that any arbitrary import can be
             /// generated.
             ///
-            /// To only allow specific imports, override this method to return a
-            /// WebAssembly module which describes the imports allowed.
+            /// To only allow specific imports, set this field to a WebAssembly
+            /// module which describes the imports allowed.
             ///
             /// Note that [`Self::min_imports`] is ignored when
             /// `available_imports` are enabled.
             ///
-            /// The returned value must be a valid binary encoding of a
+            /// The provided value must be a valid binary encoding of a
             /// WebAssembly module. `wasm-smith` will panic if the module cannot
             /// be parsed.
             ///
@@ -49,6 +49,51 @@ macro_rules! define_config {
             /// ```
             pub available_imports: Option<Vec<u8>>,
 
+            /// If provided, the generated module will have exports with exactly
+            /// the same names and types as those in the provided WebAssembly
+            /// module. The implementation (e.g. function bodies, global
+            /// initializers) of each export in the generated module will be
+            /// random and unrelated to the implementation in the provided
+            /// module. Only globals and functions are supported.
+            ///
+            ///
+            /// Defaults to `None` which means arbitrary exports will be
+            /// generated.
+            ///
+            /// To specify which exports the generated modules should have, set
+            /// this field to a WebAssembly module which describes the desired
+            /// exports. To generate modules with varying exports that meet some
+            /// constraints, consider randomly generating the value for this
+            /// field.
+            ///
+            /// The provided value must be a valid binary encoding of a
+            /// WebAssembly module. `wasm-smith` will panic if the module cannot
+            /// be parsed.
+            ///
+            /// # Module Limits
+            ///
+            /// All types, functions, globals, and exports that are needed to
+            /// provide the required exports will be generated, even if it
+            /// causes the resulting module to exceed the limits defined in
+            /// [`Self::max_type_size`], [`Self::max_types`],
+            /// [`Self::max_funcs`], [`Self::max_globals`], or
+            /// [`Self::max_exports`].
+            ///
+            /// # Example
+            ///
+            /// As for [`Self::available_imports`], the `wat` crate can be used
+            /// to provide an human-readable description of the desired exports:
+            ///
+            /// ```rust
+            /// Some(wat::parse_str(r#"
+            ///     (module
+            ///         (func (export "foo") (param i32) (result i64) unreachable)
+            ///         (global (export "bar") f32 f32.const 0)
+            ///     )
+            /// "#));
+            /// ```
+            pub exports: Option<Vec<u8>>,
+
             $(
                 $(#[$field_attr])*
                 pub $field: $field_ty,
@@ -59,6 +104,7 @@ macro_rules! define_config {
             fn default() -> Config {
                 Config {
                     available_imports: None,
+                    exports: None,
 
                     $(
                         $field: $default,
@@ -82,12 +128,44 @@ macro_rules! define_config {
             /// Note that [`Self::min_imports`] is ignored when
             /// `available_imports` are enabled.
             ///
-            /// The returned value must be a valid binary encoding of a
+            /// The provided value must be a valid binary encoding of a
             /// WebAssembly module. `wasm-smith` will panic if the module cannot
             /// be parsed.
             #[cfg_attr(feature = "clap", clap(long))]
             available_imports: Option<std::path::PathBuf>,
 
+            /// If provided, the generated module will have exports with exactly
+            /// the same names and types as those in the provided WebAssembly
+            /// module. The implementation (e.g. function bodies, global
+            /// initializers) of each export in the generated module will be
+            /// random and unrelated to the implementation in the provided
+            /// module. Only globals and functions are supported.
+            ///
+            /// Defaults to `None` which means arbitrary exports will be
+            /// generated.
+            ///
+            /// To specify which exports the generated modules should have, set
+            /// this field to a WebAssembly module which describes the desired
+            /// exports. To generate modules with varying exports that meet some
+            /// constraints, consider randomly generating the value for this
+            /// field.
+            ///
+            /// The provided value must be a valid binary encoding of a
+            /// WebAssembly module. `wasm-smith` will panic if the module cannot
+            /// be parsed.
+            ///
+            /// # Module Limits
+            ///
+            /// All types, functions, globals, and exports that are needed to
+            /// provide the required exports will be generated, even if it
+            /// causes the resulting module to exceed the limits defined in
+            /// [`Self::max_type_size`], [`Self::max_types`],
+            /// [`Self::max_funcs`], [`Self::max_globals`], or
+            /// [`Self::max_exports`].
+            ///
+            #[cfg_attr(feature = "clap", clap(long))]
+            exports: Option<std::path::PathBuf>,
+
             $(
                 $(#[$field_attr])*
                 #[cfg_attr(feature = "clap", clap(long))]
@@ -100,6 +178,7 @@ macro_rules! define_config {
             pub fn or(self, other: Self) -> Self {
                 Self {
                     available_imports: self.available_imports.or(other.available_imports),
+                    exports: self.exports.or(other.exports),
 
                     $(
                         $field: self.$field.or(other.$field),
@@ -121,6 +200,13 @@ macro_rules! define_config {
                         } else {
                             None
                         },
+                    exports: if let Some(file) = config
+                        .exports
+                        .as_ref() {
+                            Some(wat::parse_file(file)?)
+                        } else {
+                            None
+                        },
 
                     $(
                         $field: config.$field.unwrap_or(default.$field),
@@ -623,6 +709,7 @@ impl<'a> Arbitrary<'a> for Config {
             max_type_size: 1000,
             canonicalize_nans: false,
             available_imports: None,
+            exports: None,
             threads_enabled: false,
             export_everything: false,
             disallow_traps: false,
diff --git a/crates/wasm-smith/src/core.rs b/crates/wasm-smith/src/core.rs
index ee1b8d2e..ffd13434 100644
--- a/crates/wasm-smith/src/core.rs
+++ b/crates/wasm-smith/src/core.rs
@@ -389,7 +389,6 @@ impl Module {
             self.arbitrary_imports(u)?;
         }
 
-        self.should_encode_types = !self.types.is_empty() || u.arbitrary()?;
         self.should_encode_imports = !self.imports.is_empty() || u.arbitrary()?;
 
         self.arbitrary_tags(u)?;
@@ -397,7 +396,10 @@ impl Module {
         self.arbitrary_tables(u)?;
         self.arbitrary_memories(u)?;
         self.arbitrary_globals(u)?;
-        self.arbitrary_exports(u)?;
+        if !self.required_exports(u)? {
+            self.arbitrary_exports(u)?;
+        };
+        self.should_encode_types = !self.types.is_empty() || u.arbitrary()?;
         self.arbitrary_start(u)?;
         self.arbitrary_elems(u)?;
         self.arbitrary_data(u)?;
@@ -1470,6 +1472,53 @@ impl Module {
         )
     }
 
+    /// Add a new global of the given type and return its global index.
+    /// `choices` can be reused in a loop to avoid allocating a new `Vec` each
+    /// time.
+    fn add_arbitrary_global_of_type(
+        &mut self,
+        ty: GlobalType,
+        num_imported_globals: usize,
+        u: &mut Unstructured,
+        choices: &mut Vec<Box<dyn Fn(&mut Unstructured, ValType) -> Result<GlobalInitExpr>>>,
+    ) -> Result<u32> {
+        choices.clear();
+        let num_funcs = self.funcs.len() as u32;
+        choices.push(Box::new(move |u, ty| {
+            Ok(GlobalInitExpr::ConstExpr(match ty {
+                ValType::I32 => ConstExpr::i32_const(u.arbitrary()?),
+                ValType::I64 => ConstExpr::i64_const(u.arbitrary()?),
+                ValType::F32 => ConstExpr::f32_const(u.arbitrary()?),
+                ValType::F64 => ConstExpr::f64_const(u.arbitrary()?),
+                ValType::V128 => ConstExpr::v128_const(u.arbitrary()?),
+                ValType::Ref(ty) => {
+                    assert!(ty.nullable);
+                    if ty.heap_type == HeapType::Func && num_funcs > 0 && u.arbitrary()? {
+                        let func = u.int_in_range(0..=num_funcs - 1)?;
+                        return Ok(GlobalInitExpr::FuncRef(func));
+                    }
+                    ConstExpr::ref_null(ty.heap_type)
+                }
+            }))
+        }));
+
+        for (i, g) in self.globals[..num_imported_globals].iter().enumerate() {
+            if !g.mutable && g.val_type == ty.val_type {
+                choices.push(Box::new(move |_, _| {
+                    Ok(GlobalInitExpr::ConstExpr(ConstExpr::global_get(i as u32)))
+                }));
+            }
+        }
+
+        let f = u.choose(&choices)?;
+        let expr = f(u, ty.val_type)?;
+        let global_idx = self.globals.len() as u32;
+        self.globals.push(ty);
+        self.defined_globals.push((global_idx, expr));
+
+        Ok(global_idx)
+    }
+
     fn arbitrary_globals(&mut self, u: &mut Unstructured) -> Result<()> {
         let mut choices: Vec<Box<dyn Fn(&mut Unstructured, ValType) -> Result<GlobalInitExpr>>> =
             vec![];
@@ -1481,42 +1530,183 @@ impl Module {
             }
 
             let ty = self.arbitrary_global_type(u)?;
+            self.add_arbitrary_global_of_type(ty, num_imported_globals, u, &mut choices)?;
 
-            choices.clear();
-            let num_funcs = self.funcs.len() as u32;
-            choices.push(Box::new(move |u, ty| {
-                Ok(GlobalInitExpr::ConstExpr(match ty {
-                    ValType::I32 => ConstExpr::i32_const(u.arbitrary()?),
-                    ValType::I64 => ConstExpr::i64_const(u.arbitrary()?),
-                    ValType::F32 => ConstExpr::f32_const(u.arbitrary()?),
-                    ValType::F64 => ConstExpr::f64_const(u.arbitrary()?),
-                    ValType::V128 => ConstExpr::v128_const(u.arbitrary()?),
-                    ValType::Ref(ty) => {
-                        assert!(ty.nullable);
-                        if ty.heap_type == HeapType::Func && num_funcs > 0 && u.arbitrary()? {
-                            let func = u.int_in_range(0..=num_funcs - 1)?;
-                            return Ok(GlobalInitExpr::FuncRef(func));
+            Ok(true)
+        })
+    }
+
+    fn required_exports(&mut self, u: &mut Unstructured) -> Result<bool> {
+        let example_module = if let Some(wasm) = self.config.exports.clone() {
+            wasm
+        } else {
+            return Ok(false);
+        };
+
+        #[cfg(feature = "wasmparser")]
+        {
+            self._required_exports(u, &example_module)?;
+            Ok(true)
+        }
+        #[cfg(not(feature = "wasmparser"))]
+        {
+            let _ = (example_module, u);
+            panic!("support for `exports` was disabled at compile time");
+        }
+    }
+
+    #[cfg(feature = "wasmparser")]
+    fn _required_exports(&mut self, u: &mut Unstructured, example_module: &[u8]) -> Result<()> {
+        fn convert_heap_type(ty: &wasmparser::HeapType) -> HeapType {
+            match ty {
+                wasmparser::HeapType::Concrete(_) => {
+                    panic!("Unable to handle concrete types in exports")
+                }
+                wasmparser::HeapType::Func => HeapType::Func,
+                wasmparser::HeapType::Extern => HeapType::Extern,
+                wasmparser::HeapType::Any => HeapType::Any,
+                wasmparser::HeapType::None => HeapType::None,
+                wasmparser::HeapType::NoExtern => HeapType::NoExtern,
+                wasmparser::HeapType::NoFunc => HeapType::NoFunc,
+                wasmparser::HeapType::Eq => HeapType::Eq,
+                wasmparser::HeapType::Struct => HeapType::Struct,
+                wasmparser::HeapType::Array => HeapType::Array,
+                wasmparser::HeapType::I31 => HeapType::I31,
+                wasmparser::HeapType::Exn => HeapType::Exn,
+            }
+        }
+
+        fn convert_val_type(ty: &wasmparser::ValType) -> ValType {
+            match ty {
+                wasmparser::ValType::I32 => ValType::I32,
+                wasmparser::ValType::I64 => ValType::I64,
+                wasmparser::ValType::F32 => ValType::F32,
+                wasmparser::ValType::F64 => ValType::F64,
+                wasmparser::ValType::V128 => ValType::V128,
+                wasmparser::ValType::Ref(r) => ValType::Ref(RefType {
+                    nullable: r.is_nullable(),
+                    heap_type: convert_heap_type(&r.heap_type()),
+                }),
+            }
+        }
+
+        fn convert_export_kind(kind: &wasmparser::ExternalKind) -> ExportKind {
+            match kind {
+                wasmparser::ExternalKind::Func => ExportKind::Func,
+                wasmparser::ExternalKind::Table => ExportKind::Table,
+                wasmparser::ExternalKind::Memory => ExportKind::Memory,
+                wasmparser::ExternalKind::Global => ExportKind::Global,
+                wasmparser::ExternalKind::Tag => ExportKind::Tag,
+            }
+        }
+
+        let mut required_exports: Vec<wasmparser::Export> = vec![];
+        let mut validator = wasmparser::Validator::new();
+        let exports_types = validator
+            .validate_all(&example_module)
+            .expect("Failed to validate `exports` Wasm");
+        for payload in wasmparser::Parser::new(0).parse_all(&example_module) {
+            match payload.expect("Failed to read `exports` Wasm") {
+                wasmparser::Payload::ExportSection(export_reader) => {
+                    required_exports = export_reader
+                        .into_iter()
+                        .collect::<Result<_, _>>()
+                        .expect("Failed to read `exports` export section");
+                }
+                _ => {}
+            }
+        }
+
+        // For each export, add necessary prerequisites to the module.
+        let mut choices = vec![];
+        let num_imported_globals = self.globals.len() - self.defined_globals.len();
+        for export in required_exports {
+            let new_index = match exports_types
+                .entity_type_from_export(&export)
+                .unwrap_or_else(|| {
+                    panic!(
+                        "Unable to get type from export {:?} in `exports` Wasm",
+                        export,
+                    )
+                }) {
+                // For functions, add the type and a function with that type.
+                wasmparser::types::EntityType::Func(id) => {
+                    let subtype = exports_types.get(id).unwrap_or_else(|| {
+                        panic!(
+                            "Unable to get subtype for function {:?} in `exports` Wasm",
+                            id
+                        )
+                    });
+                    match &subtype.composite_type {
+                        wasmparser::CompositeType::Func(func_type) => {
+                            assert!(
+                                subtype.is_final,
+                                "Subtype {:?} from `exports` Wasm is not final",
+                                subtype
+                            );
+                            assert!(
+                                subtype.supertype_idx.is_none(),
+                                "Subtype {:?} from `exports` Wasm has non-empty supertype",
+                                subtype
+                            );
+                            let new_type = Rc::new(FuncType {
+                                params: func_type
+                                    .params()
+                                    .into_iter()
+                                    .map(convert_val_type)
+                                    .collect(),
+                                results: func_type
+                                    .results()
+                                    .into_iter()
+                                    .map(convert_val_type)
+                                    .collect(),
+                            });
+                            self.rec_groups.push(self.types.len()..self.types.len() + 1);
+                            let type_index = self.add_type(SubType {
+                                is_final: true,
+                                supertype: None,
+                                composite_type: CompositeType::Func(Rc::clone(&new_type)),
+                            });
+                            let func_index = self.funcs.len() as u32;
+                            self.funcs.push((type_index, new_type));
+                            self.num_defined_funcs += 1;
+                            func_index
                         }
-                        ConstExpr::ref_null(ty.heap_type)
+                        _ => panic!(
+                            "Unable to handle type {:?} from `exports` Wasm",
+                            subtype.composite_type
+                        ),
                     }
-                }))
-            }));
-
-            for (i, g) in self.globals[..num_imported_globals].iter().enumerate() {
-                if !g.mutable && g.val_type == ty.val_type {
-                    choices.push(Box::new(move |_, _| {
-                        Ok(GlobalInitExpr::ConstExpr(ConstExpr::global_get(i as u32)))
-                    }));
                 }
-            }
+                // For globals, add a new global.
+                wasmparser::types::EntityType::Global(global_type) => self
+                    .add_arbitrary_global_of_type(
+                        GlobalType {
+                            val_type: convert_val_type(&global_type.content_type),
+                            mutable: global_type.mutable,
+                        },
+                        num_imported_globals,
+                        u,
+                        &mut choices,
+                    )?,
+                wasmparser::types::EntityType::Table(_)
+                | wasmparser::types::EntityType::Memory(_)
+                | wasmparser::types::EntityType::Tag(_) => {
+                    panic!(
+                        "Config `exports` has an export of type {:?} which cannot yet be handled.",
+                        export.kind
+                    )
+                }
+            };
+            self.exports.push((
+                export.name.to_string(),
+                convert_export_kind(&export.kind),
+                new_index,
+            ));
+            self.export_names.insert(export.name.to_string());
+        }
 
-            let f = u.choose(&choices)?;
-            let expr = f(u, ty.val_type)?;
-            let global_idx = self.globals.len() as u32;
-            self.globals.push(ty);
-            self.defined_globals.push((global_idx, expr));
-            Ok(true)
-        })
+        Ok(())
     }
 
     fn arbitrary_exports(&mut self, u: &mut Unstructured) -> Result<()> {
@@ -1773,7 +1963,7 @@ impl Module {
 
     fn arbitrary_code(&mut self, u: &mut Unstructured) -> Result<()> {
         self.code.reserve(self.num_defined_funcs);
-        let mut allocs = CodeBuilderAllocations::new(self);
+        let mut allocs = CodeBuilderAllocations::new(self, self.config.exports.is_some());
         for (_, ty) in self.funcs[self.funcs.len() - self.num_defined_funcs..].iter() {
             let body = self.arbitrary_func_body(u, ty, &mut allocs)?;
             self.code.push(body);
diff --git a/crates/wasm-smith/src/core/code_builder.rs b/crates/wasm-smith/src/core/code_builder.rs
index 6897c8b6..66b20c4a 100644
--- a/crates/wasm-smith/src/core/code_builder.rs
+++ b/crates/wasm-smith/src/core/code_builder.rs
@@ -648,6 +648,10 @@ pub(crate) struct CodeBuilderAllocations {
     global_dropped_f32: Option<u32>,
     global_dropped_f64: Option<u32>,
     global_dropped_v128: Option<u32>,
+
+    // Indicates that additional exports cannot be generated. This will be true
+    // if the `Config` specifies exactly which exports should be present.
+    disallow_exporting: bool,
 }
 
 pub(crate) struct CodeBuilder<'a> {
@@ -704,7 +708,7 @@ enum Float {
 }
 
 impl CodeBuilderAllocations {
-    pub(crate) fn new(module: &Module) -> Self {
+    pub(crate) fn new(module: &Module, disallow_exporting: bool) -> Self {
         let mut mutable_globals = BTreeMap::new();
         for (i, global) in module.globals.iter().enumerate() {
             if global.mutable {
@@ -769,6 +773,42 @@ impl CodeBuilderAllocations {
             }
         }
 
+        let mut global_dropped_i32 = None;
+        let mut global_dropped_i64 = None;
+        let mut global_dropped_f32 = None;
+        let mut global_dropped_f64 = None;
+        let mut global_dropped_v128 = None;
+
+        // If we can't export additional globals, try to use existing exported
+        // mutable globals for dropped values.
+        if disallow_exporting {
+            for (_, kind, index) in module.exports.iter() {
+                if *kind == ExportKind::Global {
+                    let ty = module.globals[*index as usize];
+                    if ty.mutable {
+                        match ty.val_type {
+                            ValType::I32 => {
+                                if global_dropped_i32.is_none() {
+                                    global_dropped_i32 = Some(*index)
+                                } else {
+                                    global_dropped_f32 = Some(*index)
+                                }
+                            }
+                            ValType::I64 => {
+                                if global_dropped_i64.is_none() {
+                                    global_dropped_i64 = Some(*index)
+                                } else {
+                                    global_dropped_f64 = Some(*index)
+                                }
+                            }
+                            ValType::V128 => global_dropped_v128 = Some(*index),
+                            _ => {}
+                        }
+                    }
+                }
+            }
+        }
+
         CodeBuilderAllocations {
             controls: Vec::with_capacity(4),
             operands: Vec::with_capacity(16),
@@ -782,13 +822,14 @@ impl CodeBuilderAllocations {
             memory32,
             memory64,
 
-            global_dropped_i32: None,
-            global_dropped_i64: None,
-            global_dropped_f32: None,
-            global_dropped_f64: None,
-            global_dropped_v128: None,
+            global_dropped_i32,
+            global_dropped_i64,
+            global_dropped_f32,
+            global_dropped_f64,
+            global_dropped_v128,
             globals_cnt: module.globals.len() as u32,
             new_globals: Vec::new(),
+            disallow_exporting,
         }
     }
 
@@ -822,8 +863,9 @@ impl CodeBuilderAllocations {
     pub fn finish(self, u: &mut Unstructured<'_>, module: &mut Module) -> arbitrary::Result<()> {
         // Any globals injected as part of dropping operands on the stack get
         // injected into the module here. Each global is then exported, most of
-        // the time, to ensure it's part of the "image" of this module available
-        // for differential execution for example.
+        // the time (if additional exports are allowed), to ensure it's part of
+        // the "image" of this module available for differential execution for
+        // example.
         for (ty, init) in self.new_globals {
             let global_idx = module.globals.len() as u32;
             module.globals.push(GlobalType {
@@ -833,7 +875,7 @@ impl CodeBuilderAllocations {
             let init = GlobalInitExpr::ConstExpr(init);
             module.defined_globals.push((global_idx, init));
 
-            if u.ratio(1, 100).unwrap_or(false) {
+            if self.disallow_exporting || u.ratio(1, 100).unwrap_or(false) {
                 continue;
             }
 


Test patch:
diff --git a/crates/wasm-smith/tests/available_imports.rs b/crates/wasm-smith/tests/available_imports.rs
index 2ccc4c1c..27e4852d 100644
--- a/crates/wasm-smith/tests/available_imports.rs
+++ b/crates/wasm-smith/tests/available_imports.rs
@@ -4,8 +4,11 @@ use arbitrary::{Arbitrary, Unstructured};
 use rand::{rngs::SmallRng, RngCore, SeedableRng};
 use std::collections::HashMap;
 use wasm_smith::{Config, Module};
+use wasmparser::Validator;
 use wasmparser::{Parser, TypeRef, ValType};
-use wasmparser::{Validator, WasmFeatures};
+
+mod common;
+use common::{parser_features_from_config, validate};
 
 #[test]
 fn smoke_test_imports_config() {
@@ -159,44 +162,3 @@ fn import_config(
     );
     (config, available)
 }
-
-fn parser_features_from_config(config: &Config) -> WasmFeatures {
-    WasmFeatures {
-        mutable_global: true,
-        saturating_float_to_int: config.saturating_float_to_int_enabled,
-        sign_extension: config.sign_extension_ops_enabled,
-        reference_types: config.reference_types_enabled,
-        multi_value: config.multi_value_enabled,
-        bulk_memory: config.bulk_memory_enabled,
-        simd: config.simd_enabled,
-        relaxed_simd: config.relaxed_simd_enabled,
-        multi_memory: config.max_memories > 1,
-        exceptions: config.exceptions_enabled,
-        memory64: config.memory64_enabled,
-        tail_call: config.tail_call_enabled,
-        function_references: config.gc_enabled,
-        gc: config.gc_enabled,
-
-        threads: false,
-        floats: true,
-        extended_const: false,
-        component_model: false,
-        memory_control: false,
-        component_model_values: false,
-        component_model_nested_names: false,
-    }
-}
-
-fn validate(validator: &mut Validator, bytes: &[u8]) {
-    let err = match validator.validate_all(bytes) {
-        Ok(_) => return,
-        Err(e) => e,
-    };
-    eprintln!("Writing Wasm to `test.wasm`");
-    drop(std::fs::write("test.wasm", &bytes));
-    if let Ok(text) = wasmprinter::print_bytes(bytes) {
-        eprintln!("Writing WAT to `test.wat`");
-        drop(std::fs::write("test.wat", &text));
-    }
-    panic!("wasm failed to validate: {}", err);
-}
diff --git a/crates/wasm-smith/tests/common/mod.rs b/crates/wasm-smith/tests/common/mod.rs
new file mode 100644
index 00000000..cc24eed2
--- /dev/null
+++ b/crates/wasm-smith/tests/common/mod.rs
@@ -0,0 +1,43 @@
+use wasm_smith::Config;
+use wasmparser::{types::Types, Validator, WasmFeatures};
+
+pub fn parser_features_from_config(config: &Config) -> WasmFeatures {
+    WasmFeatures {
+        mutable_global: true,
+        saturating_float_to_int: config.saturating_float_to_int_enabled,
+        sign_extension: config.sign_extension_ops_enabled,
+        reference_types: config.reference_types_enabled,
+        multi_value: config.multi_value_enabled,
+        bulk_memory: config.bulk_memory_enabled,
+        simd: config.simd_enabled,
+        relaxed_simd: config.relaxed_simd_enabled,
+        multi_memory: config.max_memories > 1,
+        exceptions: config.exceptions_enabled,
+        memory64: config.memory64_enabled,
+        tail_call: config.tail_call_enabled,
+        function_references: config.gc_enabled,
+        gc: config.gc_enabled,
+
+        threads: false,
+        floats: true,
+        extended_const: false,
+        component_model: false,
+        memory_control: false,
+        component_model_values: false,
+        component_model_nested_names: false,
+    }
+}
+
+pub fn validate(validator: &mut Validator, bytes: &[u8]) -> Types {
+    let err = match validator.validate_all(bytes) {
+        Ok(types) => return types,
+        Err(e) => e,
+    };
+    eprintln!("Writing Wasm to `test.wasm`");
+    drop(std::fs::write("test.wasm", &bytes));
+    if let Ok(text) = wasmprinter::print_bytes(bytes) {
+        eprintln!("Writing WAT to `test.wat`");
+        drop(std::fs::write("test.wat", &text));
+    }
+    panic!("wasm failed to validate: {}", err);
+}
diff --git a/crates/wasm-smith/tests/exports.rs b/crates/wasm-smith/tests/exports.rs
new file mode 100644
index 00000000..ff1dac0c
--- /dev/null
+++ b/crates/wasm-smith/tests/exports.rs
@@ -0,0 +1,147 @@
+use arbitrary::{Arbitrary, Unstructured};
+use rand::{rngs::SmallRng, RngCore, SeedableRng};
+use wasm_smith::{Config, Module};
+use wasmparser::{
+    types::EntityType, CompositeType, FuncType, GlobalType, Parser, Validator, WasmFeatures,
+};
+
+mod common;
+use common::{parser_features_from_config, validate};
+
+#[derive(Debug, PartialEq)]
+enum ExportType {
+    Func(FuncType),
+    Global(GlobalType),
+}
+
+#[test]
+fn smoke_test_single_export() {
+    let test = r#"
+        (module
+        	(func (export "foo") (param i32) (result i64)
+        		unreachable
+        	)
+        )
+        "#;
+    smoke_test_exports(test, 11)
+}
+
+#[test]
+fn smoke_test_multiple_exports() {
+    let test = r#"
+        (module
+        	(func (export "a") (param i32) (result i64)
+        		unreachable
+        	)
+        	(func (export "b")
+        		unreachable
+        	)
+        	(func (export "c")
+        		unreachable
+        	)
+        )
+        "#;
+    smoke_test_exports(test, 12)
+}
+
+#[test]
+fn smoke_test_exported_global() {
+    let test = r#"
+        (module
+        	(func (export "a") (param i32 i32 f32 f64) (result f32)
+        		unreachable
+        	)
+            (global (export "glob") f64 f64.const 0)
+        )
+        "#;
+    smoke_test_exports(test, 20)
+}
+
+#[test]
+fn smoke_test_export_with_imports() {
+    let test = r#"
+        (module
+            (import "" "foo" (func (param i32)))
+            (import "" "bar" (global (mut f32)))
+            (func (param i64) unreachable)
+            (global i32 (i32.const 0))
+            (export "a" (func 0))
+            (export "b" (global 0))
+            (export "c" (func 1))
+            (export "d" (global 1))
+        )
+            "#;
+    smoke_test_exports(test, 21)
+}
+
+#[test]
+fn smoke_test_with_mutable_global_exports() {
+    let test = r#"
+        (module
+            (global (export "1i32") (mut i32) (i32.const 0))
+            (global (export "2i32") (mut i32) (i32.const 0))
+            (global (export "1i64") (mut i64) (i64.const 0))
+            (global (export "2i64") (mut i64) (i64.const 0))
+            (global (export "3i32") (mut i32) (i32.const 0))
+            (global (export "3i64") (mut i64) (i64.const 0))
+            (global (export "4i32") i32 (i32.const 0))
+            (global (export "4i64") i64 (i64.const 0))
+        )"#;
+    smoke_test_exports(test, 22)
+}
+
+fn get_func_and_global_exports(features: WasmFeatures, module: &[u8]) -> Vec<(String, ExportType)> {
+    let mut validator = Validator::new_with_features(features);
+    let types = validate(&mut validator, module);
+    let mut exports = vec![];
+
+    for payload in Parser::new(0).parse_all(module) {
+        let payload = payload.unwrap();
+        if let wasmparser::Payload::ExportSection(rdr) = payload {
+            for export in rdr {
+                let export = export.unwrap();
+                match types.entity_type_from_export(&export).unwrap() {
+                    EntityType::Func(core_id) => {
+                        let sub_type = types.get(core_id).expect("Failed to lookup core id");
+                        assert!(sub_type.is_final);
+                        assert!(sub_type.supertype_idx.is_none());
+                        let CompositeType::Func(func_type) = &sub_type.composite_type else {
+                            panic!("Expected Func CompositeType, but found {:?}", sub_type);
+                        };
+                        exports
+                            .push((export.name.to_string(), ExportType::Func(func_type.clone())));
+                    }
+                    EntityType::Global(global_type) => {
+                        exports.push((export.name.to_string(), ExportType::Global(global_type)))
+                    }
+                    other => {
+                        panic!("Unexpected entity type {:?}", other)
+                    }
+                }
+            }
+        }
+    }
+    exports
+}
+
+fn smoke_test_exports(exports_test_case: &str, seed: u64) {
+    let mut rng = SmallRng::seed_from_u64(seed);
+    let mut buf = vec![0; 512];
+    let wasm = wat::parse_str(exports_test_case).unwrap();
+    let expected_exports = get_func_and_global_exports(WasmFeatures::default(), &wasm);
+
+    for _ in 0..1024 {
+        rng.fill_bytes(&mut buf);
+        let mut u = Unstructured::new(&buf);
+
+        let mut config = Config::arbitrary(&mut u).expect("arbitrary config");
+        config.exports = Some(wasm.clone());
+
+        let features = parser_features_from_config(&config);
+        let module = Module::new(config, &mut u).unwrap();
+        let wasm_bytes = module.to_bytes();
+
+        let generated_exports = get_func_and_global_exports(features, &wasm_bytes);
+        assert_eq!(expected_exports, generated_exports);
+    }
+}