Coverage Report

Created: 2026-02-23 07:32

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/wasm-tools/crates/wasm-mutate/src/lib.rs
Line
Count
Source
1
//! A WebAssembly test case mutator.
2
//!
3
//! `wasm-mutate` takes in an existing Wasm module and then applies a
4
//! pseudo-random transformation to it, producing a new, mutated Wasm
5
//! module. This new, mutated Wasm module can be fed as a test input to your
6
//! Wasm parser, validator, compiler, or any other Wasm-consuming
7
//! tool. `wasm-mutate` can serve as a custom mutator for mutation-based
8
//! fuzzing.
9
10
#![cfg_attr(not(feature = "clap"), deny(missing_docs))]
11
12
mod error;
13
mod info;
14
mod module;
15
mod mutators;
16
17
pub use error::*;
18
19
use crate::mutators::{
20
    Item, add_function::AddFunctionMutator, add_type::AddTypeMutator,
21
    codemotion::CodemotionMutator, custom::AddCustomSectionMutator, custom::CustomSectionMutator,
22
    custom::ReorderCustomSectionMutator, function_body_unreachable::FunctionBodyUnreachable,
23
    modify_const_exprs::ConstExpressionMutator, modify_data::ModifyDataMutator,
24
    peephole::PeepholeMutator, remove_export::RemoveExportMutator, remove_item::RemoveItemMutator,
25
    remove_section::RemoveSection, rename_export::RenameExportMutator, snip_function::SnipMutator,
26
    start::RemoveStartSection,
27
};
28
use info::ModuleInfo;
29
use mutators::Mutator;
30
use rand::{Rng, SeedableRng, rngs::SmallRng};
31
use std::sync::Arc;
32
33
#[cfg(feature = "clap")]
34
use clap::Parser;
35
36
// NB: only add this doc comment if we are not building the CLI, since otherwise
37
// it will override the main CLI's about text.
38
#[cfg_attr(
39
    not(feature = "clap"),
40
    doc = r###"
41
A WebAssembly test case mutator.
42
43
This is the main entry point into this crate. It provides various methods for
44
configuring what kinds of mutations might be applied to the input Wasm. Once
45
configured, you can apply a transformation to the input Wasm via the
46
[`run`][crate::WasmMutate::run] method.
47
48
# Example
49
50
```
51
# fn _foo() -> anyhow::Result<()> {
52
use wasm_mutate::WasmMutate;
53
54
let input_wasm = wat::parse_str(r#"
55
           (module
56
            (func (export "hello") (result i32)
57
             (i32.const 1234)
58
            )
59
           )
60
           "#)?;
61
62
// Create a `WasmMutate` builder and configure it.
63
let mut mutate = WasmMutate::default();
64
mutate
65
    // Set the RNG seed.
66
    .seed(42)
67
    // Allow mutations that change the semantics of the Wasm module.
68
    .preserve_semantics(false)
69
    // Use at most this much "fuel" when trying to mutate the Wasm module before
70
    // giving up.
71
    .fuel(1_000);
72
73
// Run the configured `WasmMutate` to get a sequence of mutations of the input
74
// Wasm!
75
for mutated_wasm in mutate.run(&input_wasm)? {
76
    let mutated_wasm = mutated_wasm?;
77
    // Feed `mutated_wasm` into your tests...
78
}
79
# Ok(())
80
# }
81
```
82
"###
83
)]
84
#[cfg_attr(feature = "clap", derive(Parser))]
85
#[derive(Clone)]
86
pub struct WasmMutate<'wasm> {
87
    /// The RNG seed used to choose which transformation to apply. Given the
88
    /// same input Wasm and same seed, `wasm-mutate` will always generate the
89
    /// same output Wasm.
90
    #[cfg_attr(feature = "clap", clap(short, long, default_value = "42"))]
91
    seed: u64,
92
93
    /// Only perform semantics-preserving transformations on the Wasm module.
94
    #[cfg_attr(feature = "clap", clap(long))]
95
    preserve_semantics: bool,
96
97
    /// Fuel to control the time of the mutation.
98
    #[cfg_attr(
99
        feature = "clap",
100
        clap(
101
            short,
102
            long,
103
            default_value = "18446744073709551615", // u64::MAX
104
        )
105
    )]
106
    fuel: u64,
107
108
    /// Only perform size-reducing transformations on the Wasm module. This
109
    /// allows `wasm-mutate` to be used as a test case reducer.
110
    #[cfg_attr(feature = "clap", clap(long))]
111
    reduce: bool,
112
113
    // Note: this is only exposed via the programmatic interface, not via the
114
    // CLI.
115
    #[cfg_attr(feature = "clap", clap(skip = None))]
116
    raw_mutate_func: Option<Arc<dyn Fn(&mut Vec<u8>, usize) -> Result<()>>>,
117
118
    #[cfg_attr(feature = "clap", clap(skip = None))]
119
    rng: Option<SmallRng>,
120
121
    #[cfg_attr(feature = "clap", clap(skip = None))]
122
    info: Option<ModuleInfo<'wasm>>,
123
}
124
125
impl Default for WasmMutate<'_> {
126
5.77k
    fn default() -> Self {
127
5.77k
        let seed = 3;
128
5.77k
        WasmMutate {
129
5.77k
            seed,
130
5.77k
            preserve_semantics: false,
131
5.77k
            reduce: false,
132
5.77k
            raw_mutate_func: None,
133
5.77k
            fuel: u64::MAX,
134
5.77k
            rng: None,
135
5.77k
            info: None,
136
5.77k
        }
137
5.77k
    }
138
}
139
140
impl<'wasm> WasmMutate<'wasm> {
141
    /// Set the RNG seed used to choose which transformation to apply.
142
    ///
143
    /// Given the same input Wasm and same seed, `wasm-mutate` will always
144
    /// generate the same output Wasm.
145
5.77k
    pub fn seed(&mut self, seed: u64) -> &mut Self {
146
5.77k
        self.seed = seed;
147
5.77k
        self
148
5.77k
    }
149
150
    /// Configure whether we will only perform semantics-preserving
151
    /// transformations on the Wasm module.
152
5.77k
    pub fn preserve_semantics(&mut self, preserve_semantics: bool) -> &mut Self {
153
5.77k
        self.preserve_semantics = preserve_semantics;
154
5.77k
        self
155
5.77k
    }
156
157
    /// Configure the fuel used during the mutation
158
5.77k
    pub fn fuel(&mut self, fuel: u64) -> &mut Self {
159
5.77k
        self.fuel = fuel;
160
5.77k
        self
161
5.77k
    }
162
163
    /// Configure whether we will only perform size-reducing transformations on
164
    /// the Wasm module.
165
    ///
166
    /// Setting this to `true` allows `wasm-mutate` to be used as a test case
167
    /// reducer.
168
0
    pub fn reduce(&mut self, reduce: bool) -> &mut Self {
169
0
        self.reduce = reduce;
170
0
        self
171
0
    }
172
173
    /// Set a custom raw mutation function.
174
    ///
175
    /// This is used when we need some underlying raw bytes, for example when
176
    /// mutating the contents of a data segment.
177
    ///
178
    /// You can override this to use `libFuzzer`'s `LLVMFuzzerMutate` function
179
    /// to get raw bytes from `libFuzzer`, for example.
180
    ///
181
    /// The function is given the raw data buffer and the maximum size the
182
    /// mutated data should be. After mutating the data, the function should
183
    /// `resize` the data to its final, mutated size, which should be less than
184
    /// or equal to the maximum size.
185
0
    pub fn raw_mutate_func(
186
0
        &mut self,
187
0
        raw_mutate_func: Option<Arc<dyn Fn(&mut Vec<u8>, usize) -> Result<()>>>,
188
0
    ) -> &mut Self {
189
0
        self.raw_mutate_func = raw_mutate_func;
190
0
        self
191
0
    }
192
193
26.4k
    pub(crate) fn consume_fuel(&mut self, qt: u64) -> Result<()> {
194
26.4k
        if qt > self.fuel {
195
6
            log::info!("Out of fuel");
196
6
            return Err(Error::out_of_fuel());
197
26.4k
        }
198
26.4k
        self.fuel -= qt;
199
26.4k
        Ok(())
200
26.4k
    }
201
202
    /// Run this configured `WasmMutate` on the given input Wasm.
203
5.77k
    pub fn run<'a>(
204
5.77k
        &'a mut self,
205
5.77k
        input_wasm: &'wasm [u8],
206
5.77k
    ) -> Result<Box<dyn Iterator<Item = Result<Vec<u8>>> + 'a>> {
207
5.77k
        self.setup(input_wasm)?;
208
209
        const MUTATORS: &[&dyn Mutator] = &[
210
            &PeepholeMutator::new(2),
211
            &RemoveExportMutator,
212
            &RenameExportMutator { max_name_size: 100 },
213
            &SnipMutator,
214
            &CodemotionMutator,
215
            &FunctionBodyUnreachable,
216
            &AddCustomSectionMutator,
217
            &ReorderCustomSectionMutator,
218
            &CustomSectionMutator,
219
            &AddTypeMutator {
220
                max_params: 20,
221
                max_results: 20,
222
            },
223
            &AddFunctionMutator,
224
            &RemoveSection::Custom,
225
            &RemoveSection::Empty,
226
            &ConstExpressionMutator::Global,
227
            &ConstExpressionMutator::ElementOffset,
228
            &ConstExpressionMutator::ElementFunc,
229
            &RemoveItemMutator(Item::Function),
230
            &RemoveItemMutator(Item::Global),
231
            &RemoveItemMutator(Item::Memory),
232
            &RemoveItemMutator(Item::Table),
233
            &RemoveItemMutator(Item::Type),
234
            &RemoveItemMutator(Item::Data),
235
            &RemoveItemMutator(Item::Element),
236
            &RemoveItemMutator(Item::Tag),
237
            &ModifyDataMutator {
238
                max_data_size: 10 << 20, // 10MB
239
            },
240
            &RemoveStartSection,
241
        ];
242
243
        // Attempt all mutators, but start at an arbitrary index.
244
4.79k
        let start = self.rng().random_range(0..MUTATORS.len());
245
14.9k
        for m in MUTATORS.iter().cycle().skip(start).take(MUTATORS.len()) {
246
14.9k
            let can_mutate = m.can_mutate(self);
247
14.9k
            log::trace!("Can `{}` mutate? {}", m.name(), can_mutate);
248
14.9k
            if !can_mutate {
249
10.1k
                continue;
250
4.79k
            }
251
4.79k
            log::debug!("attempting to mutate with `{}`", m.name());
252
4.79k
            match m.mutate(self) {
253
3.86k
                Ok(iter) => {
254
3.86k
                    log::debug!("mutator `{}` succeeded", m.name());
255
12.0k
                    return Ok(Box::new(iter.into_iter().map(|r| r.map(|m| m.finish()))));
256
                }
257
931
                Err(e) => {
258
931
                    log::debug!("mutator `{}` failed: {}", m.name(), e);
259
931
                    return Err(e);
260
                }
261
            }
262
        }
263
264
0
        Err(Error::no_mutations_applicable())
265
5.77k
    }
266
267
5.77k
    fn setup(&mut self, input_wasm: &'wasm [u8]) -> Result<()> {
268
5.77k
        self.info = Some(ModuleInfo::new(input_wasm)?);
269
4.79k
        self.rng = Some(SmallRng::seed_from_u64(self.seed));
270
4.79k
        Ok(())
271
5.77k
    }
272
273
42.3k
    pub(crate) fn rng(&mut self) -> &mut SmallRng {
274
42.3k
        self.rng.as_mut().unwrap()
275
42.3k
    }
276
277
194k
    pub(crate) fn info(&self) -> &ModuleInfo<'wasm> {
278
194k
        self.info.as_ref().unwrap()
279
194k
    }
280
281
993
    fn raw_mutate(&mut self, data: &mut Vec<u8>, max_size: usize) -> Result<()> {
282
        // If a raw mutation function is configured then that's prioritized.
283
993
        if let Some(mutate) = &self.raw_mutate_func {
284
0
            return mutate(data, max_size);
285
993
        }
286
287
        // If no raw mutation function is configured then we apply a naive
288
        // default heuristic. For now that heuristic is to simply replace a
289
        // subslice of data with a random slice of other data.
290
        //
291
        // First up start/end indices are picked.
292
993
        let a = self.rng().random_range(0..=data.len());
293
993
        let b = self.rng().random_range(0..=data.len());
294
993
        let start = a.min(b);
295
993
        let end = a.max(b);
296
297
        // Next a length of the replacement is chosen. Note that the replacement
298
        // is always smaller than the input if reduction is requested, otherwise
299
        // we choose some arbitrary length of bytes to insert.
300
993
        let max_size = if self.reduce || self.rng().random() {
301
480
            0
302
        } else {
303
513
            max_size
304
        };
305
993
        let len = self
306
993
            .rng()
307
993
            .random_range(0..=end - start + max_size.saturating_sub(data.len()));
308
309
        // With parameters chosen the `Vec::splice` method is used to replace
310
        // the data in the input.
311
993
        data.splice(start..end, self.rng().random_iter().take(len));
312
313
993
        Ok(())
314
993
    }
315
}
316
317
#[cfg(test)]
318
pub(crate) fn validate(bytes: &[u8]) {
319
    use wasmparser::WasmFeatures;
320
321
    let mut validator = wasmparser::Validator::new_with_features(
322
        WasmFeatures::default() | WasmFeatures::MEMORY64 | WasmFeatures::MULTI_MEMORY,
323
    );
324
    let err = match validator.validate_all(bytes) {
325
        Ok(_) => return,
326
        Err(e) => e,
327
    };
328
    drop(std::fs::write("test.wasm", &bytes));
329
    if let Ok(text) = wasmprinter::print_bytes(bytes) {
330
        drop(std::fs::write("test.wat", &text));
331
    }
332
333
    panic!("wasm failed to validate: {err} (written to test.wasm)");
334
}