Coverage Report

Created: 2026-07-19 07:55

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::{RngExt, 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
3.99k
    fn default() -> Self {
127
3.99k
        let seed = 3;
128
3.99k
        WasmMutate {
129
3.99k
            seed,
130
3.99k
            preserve_semantics: false,
131
3.99k
            reduce: false,
132
3.99k
            raw_mutate_func: None,
133
3.99k
            fuel: u64::MAX,
134
3.99k
            rng: None,
135
3.99k
            info: None,
136
3.99k
        }
137
3.99k
    }
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
3.99k
    pub fn seed(&mut self, seed: u64) -> &mut Self {
146
3.99k
        self.seed = seed;
147
3.99k
        self
148
3.99k
    }
149
150
    /// Configure whether we will only perform semantics-preserving
151
    /// transformations on the Wasm module.
152
3.99k
    pub fn preserve_semantics(&mut self, preserve_semantics: bool) -> &mut Self {
153
3.99k
        self.preserve_semantics = preserve_semantics;
154
3.99k
        self
155
3.99k
    }
156
157
    /// Configure the fuel used during the mutation
158
3.99k
    pub fn fuel(&mut self, fuel: u64) -> &mut Self {
159
3.99k
        self.fuel = fuel;
160
3.99k
        self
161
3.99k
    }
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
18.2k
    pub(crate) fn consume_fuel(&mut self, qt: u64) -> Result<()> {
194
18.2k
        if qt > self.fuel {
195
5
            log::info!("Out of fuel");
196
5
            return Err(Error::out_of_fuel());
197
18.2k
        }
198
18.2k
        self.fuel -= qt;
199
18.2k
        Ok(())
200
18.2k
    }
201
202
    /// Run this configured `WasmMutate` on the given input Wasm.
203
3.99k
    pub fn run<'a>(
204
3.99k
        &'a mut self,
205
3.99k
        input_wasm: &'wasm [u8],
206
3.99k
    ) -> Result<Box<dyn Iterator<Item = Result<Vec<u8>>> + 'a>> {
207
3.99k
        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
3.30k
        let start = self.rng().random_range(0..MUTATORS.len());
245
9.82k
        for m in MUTATORS.iter().cycle().skip(start).take(MUTATORS.len()) {
246
9.82k
            let can_mutate = m.can_mutate(self);
247
9.82k
            log::trace!("Can `{}` mutate? {}", m.name(), can_mutate);
248
9.82k
            if !can_mutate {
249
6.51k
                continue;
250
3.30k
            }
251
3.30k
            log::debug!("attempting to mutate with `{}`", m.name());
252
3.30k
            match m.mutate(self) {
253
2.58k
                Ok(iter) => {
254
2.58k
                    log::debug!("mutator `{}` succeeded", m.name());
255
7.82k
                    return Ok(Box::new(iter.into_iter().map(|r| r.map(|m| m.finish()))));
256
                }
257
720
                Err(e) => {
258
720
                    log::debug!("mutator `{}` failed: {}", m.name(), e);
259
720
                    return Err(e);
260
                }
261
            }
262
        }
263
264
0
        Err(Error::no_mutations_applicable())
265
3.99k
    }
266
267
3.99k
    fn setup(&mut self, input_wasm: &'wasm [u8]) -> Result<()> {
268
3.99k
        self.info = Some(ModuleInfo::new(input_wasm)?);
269
3.30k
        self.rng = Some(SmallRng::seed_from_u64(self.seed));
270
3.30k
        Ok(())
271
3.99k
    }
272
273
26.2k
    pub(crate) fn rng(&mut self) -> &mut SmallRng {
274
26.2k
        self.rng.as_mut().unwrap()
275
26.2k
    }
276
277
135k
    pub(crate) fn info(&self) -> &ModuleInfo<'wasm> {
278
135k
        self.info.as_ref().unwrap()
279
135k
    }
280
281
858
    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
858
        if let Some(mutate) = &self.raw_mutate_func {
284
0
            return mutate(data, max_size);
285
858
        }
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
858
        let a = self.rng().random_range(0..=data.len());
293
858
        let b = self.rng().random_range(0..=data.len());
294
858
        let start = a.min(b);
295
858
        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
858
        let max_size = if self.reduce || self.rng().random() {
301
363
            0
302
        } else {
303
495
            max_size
304
        };
305
858
        let len = self
306
858
            .rng()
307
858
            .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
858
        data.splice(start..end, self.rng().random_iter().take(len));
312
313
858
        Ok(())
314
858
    }
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
}