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    add_function::AddFunctionMutator, add_type::AddTypeMutator, codemotion::CodemotionMutator,
21    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    Item,
27};
28use info::ModuleInfo;
29use mutators::Mutator;
30use rand::{rngs::SmallRng, Rng, SeedableRng};
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        ];
241
242        // Attempt all mutators, but start at an arbitrary index.
243        let start = self.rng().gen_range(0..MUTATORS.len());
244        for m in MUTATORS.iter().cycle().skip(start).take(MUTATORS.len()) {
245            let can_mutate = m.can_mutate(self);
246            log::trace!("Can `{}` mutate? {}", m.name(), can_mutate);
247            if !can_mutate {
248                continue;
249            }
250            log::debug!("attempting to mutate with `{}`", m.name());
251            match m.mutate(self) {
252                Ok(iter) => {
253                    log::debug!("mutator `{}` succeeded", m.name());
254                    return Ok(Box::new(iter.into_iter().map(|r| r.map(|m| m.finish()))));
255                }
256                Err(e) => {
257                    log::debug!("mutator `{}` failed: {}", m.name(), e);
258                    return Err(e);
259                }
260            }
261        }
262
263        Err(Error::no_mutations_applicable())
264    }
265
266    fn setup(&mut self, input_wasm: &'wasm [u8]) -> Result<()> {
267        self.info = Some(ModuleInfo::new(input_wasm)?);
268        self.rng = Some(SmallRng::seed_from_u64(self.seed));
269        Ok(())
270    }
271
272    pub(crate) fn rng(&mut self) -> &mut SmallRng {
273        self.rng.as_mut().unwrap()
274    }
275
276    pub(crate) fn info(&self) -> &ModuleInfo<'wasm> {
277        self.info.as_ref().unwrap()
278    }
279
280    fn raw_mutate(&mut self, data: &mut Vec<u8>, max_size: usize) -> Result<()> {
281        // If a raw mutation function is configured then that's prioritized.
282        if let Some(mutate) = &self.raw_mutate_func {
283            return mutate(data, max_size);
284        }
285
286        // If no raw mutation function is configured then we apply a naive
287        // default heuristic. For now that heuristic is to simply replace a
288        // subslice of data with a random slice of other data.
289        //
290        // First up start/end indices are picked.
291        let a = self.rng().gen_range(0..=data.len());
292        let b = self.rng().gen_range(0..=data.len());
293        let start = a.min(b);
294        let end = a.max(b);
295
296        // Next a length of the replacement is chosen. Note that the replacement
297        // is always smaller than the input if reduction is requested, otherwise
298        // we choose some arbitrary length of bytes to insert.
299        let max_size = if self.reduce || self.rng().r#gen() {
300            0
301        } else {
302            max_size
303        };
304        let len = self
305            .rng()
306            .gen_range(0..=end - start + max_size.saturating_sub(data.len()));
307
308        // With parameters chosen the `Vec::splice` method is used to replace
309        // the data in the input.
310        data.splice(
311            start..end,
312            self.rng()
313                .sample_iter(rand::distributions::Standard)
314                .take(len),
315        );
316
317        Ok(())
318    }
319}
320
321#[cfg(test)]
322pub(crate) fn validate(bytes: &[u8]) {
323    use wasmparser::WasmFeatures;
324
325    let mut validator = wasmparser::Validator::new_with_features(
326        WasmFeatures::default() | WasmFeatures::MEMORY64 | WasmFeatures::MULTI_MEMORY,
327    );
328    let err = match validator.validate_all(bytes) {
329        Ok(_) => return,
330        Err(e) => e,
331    };
332    drop(std::fs::write("test.wasm", &bytes));
333    if let Ok(text) = wasmprinter::print_bytes(bytes) {
334        drop(std::fs::write("test.wat", &text));
335    }
336
337    panic!("wasm failed to validate: {err} (written to test.wasm)");
338}