wasm_mutate/
lib.rs

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
12mod error;
13mod info;
14mod module;
15mod mutators;
16
17pub use error::*;
18
19use 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};
28use info::ModuleInfo;
29use mutators::Mutator;
30use rand::{Rng, SeedableRng, rngs::SmallRng};
31use std::sync::Arc;
32
33#[cfg(feature = "clap")]
34use 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###"
41A WebAssembly test case mutator.
42
43This is the main entry point into this crate. It provides various methods for
44configuring what kinds of mutations might be applied to the input Wasm. Once
45configured, 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<()> {
52use wasm_mutate::WasmMutate;
53
54let 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.
63let mut mutate = WasmMutate::default();
64mutate
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!
75for 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)]
86pub 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
125impl Default for WasmMutate<'_> {
126    fn default() -> Self {
127        let seed = 3;
128        WasmMutate {
129            seed,
130            preserve_semantics: false,
131            reduce: false,
132            raw_mutate_func: None,
133            fuel: u64::MAX,
134            rng: None,
135            info: None,
136        }
137    }
138}
139
140impl<'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    pub fn seed(&mut self, seed: u64) -> &mut Self {
146        self.seed = seed;
147        self
148    }
149
150    /// Configure whether we will only perform semantics-preserving
151    /// transformations on the Wasm module.
152    pub fn preserve_semantics(&mut self, preserve_semantics: bool) -> &mut Self {
153        self.preserve_semantics = preserve_semantics;
154        self
155    }
156
157    /// Configure the fuel used during the mutation
158    pub fn fuel(&mut self, fuel: u64) -> &mut Self {
159        self.fuel = fuel;
160        self
161    }
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    pub fn reduce(&mut self, reduce: bool) -> &mut Self {
169        self.reduce = reduce;
170        self
171    }
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    pub fn raw_mutate_func(
186        &mut self,
187        raw_mutate_func: Option<Arc<dyn Fn(&mut Vec<u8>, usize) -> Result<()>>>,
188    ) -> &mut Self {
189        self.raw_mutate_func = raw_mutate_func;
190        self
191    }
192
193    pub(crate) fn consume_fuel(&mut self, qt: u64) -> Result<()> {
194        if qt > self.fuel {
195            log::info!("Out of fuel");
196            return Err(Error::out_of_fuel());
197        }
198        self.fuel -= qt;
199        Ok(())
200    }
201
202    /// Run this configured `WasmMutate` on the given input Wasm.
203    pub fn run<'a>(
204        &'a mut self,
205        input_wasm: &'wasm [u8],
206    ) -> Result<Box<dyn Iterator<Item = Result<Vec<u8>>> + 'a>> {
207        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        let start = self.rng().random_range(0..MUTATORS.len());
245        for m in MUTATORS.iter().cycle().skip(start).take(MUTATORS.len()) {
246            let can_mutate = m.can_mutate(self);
247            log::trace!("Can `{}` mutate? {}", m.name(), can_mutate);
248            if !can_mutate {
249                continue;
250            }
251            log::debug!("attempting to mutate with `{}`", m.name());
252            match m.mutate(self) {
253                Ok(iter) => {
254                    log::debug!("mutator `{}` succeeded", m.name());
255                    return Ok(Box::new(iter.into_iter().map(|r| r.map(|m| m.finish()))));
256                }
257                Err(e) => {
258                    log::debug!("mutator `{}` failed: {}", m.name(), e);
259                    return Err(e);
260                }
261            }
262        }
263
264        Err(Error::no_mutations_applicable())
265    }
266
267    fn setup(&mut self, input_wasm: &'wasm [u8]) -> Result<()> {
268        self.info = Some(ModuleInfo::new(input_wasm)?);
269        self.rng = Some(SmallRng::seed_from_u64(self.seed));
270        Ok(())
271    }
272
273    pub(crate) fn rng(&mut self) -> &mut SmallRng {
274        self.rng.as_mut().unwrap()
275    }
276
277    pub(crate) fn info(&self) -> &ModuleInfo<'wasm> {
278        self.info.as_ref().unwrap()
279    }
280
281    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        if let Some(mutate) = &self.raw_mutate_func {
284            return mutate(data, max_size);
285        }
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        let a = self.rng().random_range(0..=data.len());
293        let b = self.rng().random_range(0..=data.len());
294        let start = a.min(b);
295        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        let max_size = if self.reduce || self.rng().random() {
301            0
302        } else {
303            max_size
304        };
305        let len = self
306            .rng()
307            .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        data.splice(start..end, self.rng().random_iter().take(len));
312
313        Ok(())
314    }
315}
316
317#[cfg(test)]
318pub(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}