wasmtime_fuzzing/generators/
table_ops.rs

1//! Generating series of `table.get` and `table.set` operations.
2use mutatis::mutators as m;
3use mutatis::{Candidates, Context, DefaultMutate, Generate, Mutate, Result as MutResult};
4use smallvec::SmallVec;
5use std::ops::RangeInclusive;
6use wasm_encoder::{
7    CodeSection, ConstExpr, EntityType, ExportKind, ExportSection, Function, FunctionSection,
8    GlobalSection, ImportSection, Instruction, Module, RefType, TableSection, TableType,
9    TypeSection, ValType,
10};
11
12/// A description of a Wasm module that makes a series of `externref` table
13/// operations.
14#[derive(Debug, Default)]
15pub struct TableOps {
16    pub(crate) num_params: u32,
17    pub(crate) num_globals: u32,
18    pub(crate) table_size: i32,
19    ops: Vec<TableOp>,
20}
21
22const NUM_PARAMS_RANGE: RangeInclusive<u32> = 0..=10;
23const NUM_GLOBALS_RANGE: RangeInclusive<u32> = 0..=10;
24const TABLE_SIZE_RANGE: RangeInclusive<i32> = 0..=100;
25const MAX_OPS: usize = 100;
26
27impl TableOps {
28    /// Serialize this module into a Wasm binary.
29    ///
30    /// The module requires several function imports. See this function's
31    /// implementation for their exact types.
32    ///
33    /// The single export of the module is a function "run" that takes
34    /// `self.num_params` parameters of type `externref`.
35    ///
36    /// The "run" function does not terminate; you should run it with limited
37    /// fuel. It also is not guaranteed to avoid traps: it may access
38    /// out-of-bounds of the table.
39    pub fn to_wasm_binary(&self) -> Vec<u8> {
40        let mut module = Module::new();
41
42        // Encode the types for all functions that we are using.
43        let mut types = TypeSection::new();
44
45        // 0: "gc"
46        types.ty().function(
47            vec![],
48            // Return a bunch of stuff from `gc` so that we exercise GCing when
49            // there is return pointer space allocated on the stack. This is
50            // especially important because the x64 backend currently
51            // dynamically adjusts the stack pointer for each call that uses
52            // return pointers rather than statically allocating space in the
53            // stack frame.
54            vec![ValType::EXTERNREF, ValType::EXTERNREF, ValType::EXTERNREF],
55        );
56
57        // 1: "run"
58        let mut params: Vec<ValType> = Vec::with_capacity(self.num_params as usize);
59        for _i in 0..self.num_params {
60            params.push(ValType::EXTERNREF);
61        }
62        let results = vec![];
63        types.ty().function(params, results);
64
65        // 2: `take_refs`
66        types.ty().function(
67            vec![ValType::EXTERNREF, ValType::EXTERNREF, ValType::EXTERNREF],
68            vec![],
69        );
70
71        // 3: `make_refs`
72        types.ty().function(
73            vec![],
74            vec![ValType::EXTERNREF, ValType::EXTERNREF, ValType::EXTERNREF],
75        );
76
77        // Import the GC function.
78        let mut imports = ImportSection::new();
79        imports.import("", "gc", EntityType::Function(0));
80        imports.import("", "take_refs", EntityType::Function(2));
81        imports.import("", "make_refs", EntityType::Function(3));
82
83        // Define our table.
84        let mut tables = TableSection::new();
85        tables.table(TableType {
86            element_type: RefType::EXTERNREF,
87            minimum: self.table_size as u64,
88            maximum: None,
89            table64: false,
90            shared: false,
91        });
92
93        // Define our globals.
94        let mut globals = GlobalSection::new();
95        for _ in 0..self.num_globals {
96            globals.global(
97                wasm_encoder::GlobalType {
98                    val_type: wasm_encoder::ValType::EXTERNREF,
99                    mutable: true,
100                    shared: false,
101                },
102                &ConstExpr::ref_null(wasm_encoder::HeapType::EXTERN),
103            );
104        }
105
106        // Define the "run" function export.
107        let mut functions = FunctionSection::new();
108        functions.function(1);
109
110        let mut exports = ExportSection::new();
111        exports.export("run", ExportKind::Func, 3);
112
113        // Give ourselves one scratch local that we can use in various `TableOp`
114        // implementations.
115        let mut func = Function::new(vec![(1, ValType::EXTERNREF)]);
116
117        func.instruction(&Instruction::Loop(wasm_encoder::BlockType::Empty));
118        for op in &self.ops {
119            op.insert(&mut func, self.num_params);
120        }
121        func.instruction(&Instruction::Br(0));
122        func.instruction(&Instruction::End);
123        func.instruction(&Instruction::End);
124
125        let mut code = CodeSection::new();
126        code.function(&func);
127
128        module
129            .section(&types)
130            .section(&imports)
131            .section(&functions)
132            .section(&tables)
133            .section(&globals)
134            .section(&exports)
135            .section(&code);
136
137        module.finish()
138    }
139
140    /// Computes the abstract stack depth after executing all operations
141    pub fn abstract_stack_depth(&self, index: usize) -> usize {
142        debug_assert!(index <= self.ops.len());
143        let mut stack: usize = 0;
144        for op in self.ops.iter().take(index) {
145            let pop = op.operands_len();
146            let push = op.results_len();
147            stack = stack.saturating_sub(pop);
148            stack += push;
149        }
150        stack
151    }
152
153    /// Fixes the stack after mutating the `idx`th op.
154    ///
155    /// The abstract stack depth starting at the `idx`th opcode must be `stack`.
156    fn fixup(&mut self, idx: usize, mut stack: usize) {
157        let mut new_ops = Vec::with_capacity(self.ops.len());
158        new_ops.extend_from_slice(&self.ops[..idx]);
159
160        // Iterate through all ops including and after `idx`, inserting a null
161        // ref for any missing operands when they want to pop more operands than
162        // exist on the stack.
163        new_ops.extend(self.ops[idx..].iter().copied().flat_map(|op| {
164            let mut temp = SmallVec::<[_; 4]>::new();
165
166            while stack < op.operands_len() {
167                temp.push(TableOp::Null());
168                stack += 1;
169            }
170
171            temp.push(op);
172            stack = stack - op.operands_len() + op.results_len();
173
174            temp
175        }));
176
177        // Now make sure that the stack is empty at the end of the ops by
178        // inserting drops as necessary.
179        for _ in 0..stack {
180            new_ops.push(TableOp::Drop());
181        }
182
183        self.ops = new_ops;
184    }
185}
186
187/// A mutator for the table ops
188#[derive(Debug)]
189pub struct TableOpsMutator;
190
191impl Mutate<TableOps> for TableOpsMutator {
192    fn mutate(&mut self, c: &mut Candidates<'_>, ops: &mut TableOps) -> mutatis::Result<()> {
193        // Insert
194        if !c.shrink() {
195            c.mutation(|ctx| {
196                if let Some(idx) = ctx.rng().gen_index(ops.ops.len() + 1) {
197                    let stack = ops.abstract_stack_depth(idx);
198                    let (op, _new_stack_size) = TableOp::generate(ctx, &ops, stack)?;
199                    ops.ops.insert(idx, op);
200                    ops.fixup(idx, stack);
201                }
202                Ok(())
203            })?;
204        }
205
206        // Remove
207        if !ops.ops.is_empty() {
208            c.mutation(|ctx| {
209                let idx = ctx
210                    .rng()
211                    .gen_index(ops.ops.len())
212                    .expect("ops is not empty");
213                let stack = ops.abstract_stack_depth(idx);
214                ops.ops.remove(idx);
215                ops.fixup(idx, stack);
216                Ok(())
217            })?;
218        }
219
220        Ok(())
221    }
222}
223
224impl DefaultMutate for TableOps {
225    type DefaultMutate = TableOpsMutator;
226}
227
228impl Default for TableOpsMutator {
229    fn default() -> Self {
230        TableOpsMutator
231    }
232}
233
234impl<'a> arbitrary::Arbitrary<'a> for TableOps {
235    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
236        let mut session = mutatis::Session::new().seed(u.arbitrary()?);
237        session
238            .generate()
239            .map_err(|_| arbitrary::Error::IncorrectFormat)
240    }
241}
242
243impl Generate<TableOps> for TableOpsMutator {
244    fn generate(&mut self, ctx: &mut Context) -> MutResult<TableOps> {
245        let num_params = m::range(NUM_PARAMS_RANGE).generate(ctx)?;
246        let num_globals = m::range(NUM_GLOBALS_RANGE).generate(ctx)?;
247        let table_size = m::range(TABLE_SIZE_RANGE).generate(ctx)?;
248
249        let mut ops = TableOps {
250            num_params,
251            num_globals,
252            table_size,
253            ops: vec![
254                TableOp::Null(),
255                TableOp::Drop(),
256                TableOp::Gc(),
257                TableOp::LocalSet(0),
258                TableOp::LocalGet(0),
259                TableOp::GlobalSet(0),
260                TableOp::GlobalGet(0),
261            ],
262        };
263
264        let mut stack: usize = 0;
265        while ops.ops.len() < MAX_OPS {
266            let (op, new_stack_len) = TableOp::generate(ctx, &ops, stack)?;
267            ops.ops.push(op);
268            stack = new_stack_len;
269        }
270
271        // Drop any leftover refs on the stack.
272        for _ in 0..stack {
273            ops.ops.push(TableOp::Drop());
274        }
275
276        Ok(ops)
277    }
278}
279
280macro_rules! define_table_ops {
281    (
282        $(
283            $op:ident $( ( $($limit:expr => $ty:ty),* ) )? : $params:expr => $results:expr ,
284        )*
285    ) => {
286        #[derive(Copy, Clone, Debug)]
287        pub(crate) enum TableOp {
288            $(
289                $op ( $( $($ty),* )? ),
290            )*
291        }
292        #[cfg(test)]
293        const OP_NAMES: &'static[&'static str] = &[
294            $(
295                stringify!($op),
296            )*
297        ];
298
299        impl TableOp {
300            #[cfg(test)]
301            fn name(&self) -> &'static str  {
302                match self {
303                    $(
304                        Self::$op (..) => stringify!($op),
305                    )*
306                }
307            }
308
309            pub fn operands_len(&self) -> usize {
310                match self {
311                    $(
312                        Self::$op (..) => $params,
313                    )*
314                }
315            }
316
317            pub fn results_len(&self) -> usize {
318                match self {
319                    $(
320                        Self::$op (..) => $results,
321                    )*
322                }
323            }
324        }
325
326        $(
327            #[allow(non_snake_case, reason = "macro-generated code")]
328            fn $op(
329                _ctx: &mut mutatis::Context,
330                _ops: &TableOps,
331                stack: usize,
332            ) -> mutatis::Result<(TableOp, usize)> {
333                #[allow(unused_comparisons, reason = "macro-generated code")]
334                {
335                    debug_assert!(stack >= $params);
336                }
337
338                let op = TableOp::$op(
339                    $($({
340                        let limit_fn = $limit as fn(&TableOps) -> $ty;
341                        let limit = (limit_fn)(_ops);
342                        debug_assert!(limit > 0);
343                        m::range(0..=limit - 1).generate(_ctx)?
344                    })*)?
345                );
346                let new_stack = stack - $params + $results;
347                Ok((op, new_stack))
348            }
349        )*
350
351        impl TableOp {
352            fn generate(
353                ctx: &mut mutatis::Context,
354                ops: &TableOps,
355                stack: usize,
356            ) -> mutatis::Result<(TableOp, usize)> {
357                let mut valid_choices: Vec<
358                    fn (&mut mutatis::Context, &TableOps, usize) -> mutatis::Result<(TableOp, usize)>
359                > = vec![];
360
361                $(
362                    #[allow(unused_comparisons, reason = "macro-generated code")]
363                    if stack >= $params $($(
364                        && {
365                            let limit_fn: fn(&TableOps) -> $ty = $limit;
366                            let limit = (limit_fn)(ops);
367                            limit > 0
368                        }
369                    )*)? {
370                        valid_choices.push($op);
371                    }
372                )*
373
374                let f = *ctx.rng()
375                    .choose(&valid_choices)
376                    .expect("should always have a valid op choice");
377
378                (f)(ctx, ops, stack)
379            }
380        }
381    };
382}
383
384define_table_ops! {
385    Gc : 0 => 3,
386
387    MakeRefs : 0 => 3,
388    TakeRefs : 3 => 0,
389
390    // Add one to make sure that out of bounds table accesses are possible, but still rare.
391    TableGet(|ops| ops.table_size + 1 => i32) : 0 => 1,
392    TableSet(|ops| ops.table_size + 1 => i32) : 1 => 0,
393
394    GlobalGet(|ops| ops.num_globals => u32) : 0 => 1,
395    GlobalSet(|ops| ops.num_globals => u32) : 1 => 0,
396
397    LocalGet(|ops| ops.num_params => u32) : 0 => 1,
398    LocalSet(|ops| ops.num_params => u32) : 1 => 0,
399
400    Drop : 1 => 0,
401
402    Null : 0 => 1,
403}
404
405impl TableOp {
406    fn insert(self, func: &mut Function, scratch_local: u32) {
407        let gc_func_idx = 0;
408        let take_refs_func_idx = 1;
409        let make_refs_func_idx = 2;
410
411        match self {
412            Self::Gc() => {
413                func.instruction(&Instruction::Call(gc_func_idx));
414            }
415            Self::MakeRefs() => {
416                func.instruction(&Instruction::Call(make_refs_func_idx));
417            }
418            Self::TakeRefs() => {
419                func.instruction(&Instruction::Call(take_refs_func_idx));
420            }
421            Self::TableGet(x) => {
422                func.instruction(&Instruction::I32Const(x));
423                func.instruction(&Instruction::TableGet(0));
424            }
425            Self::TableSet(x) => {
426                func.instruction(&Instruction::LocalSet(scratch_local));
427                func.instruction(&Instruction::I32Const(x));
428                func.instruction(&Instruction::LocalGet(scratch_local));
429                func.instruction(&Instruction::TableSet(0));
430            }
431            Self::GlobalGet(x) => {
432                func.instruction(&Instruction::GlobalGet(x));
433            }
434            Self::GlobalSet(x) => {
435                func.instruction(&Instruction::GlobalSet(x));
436            }
437            Self::LocalGet(x) => {
438                func.instruction(&Instruction::LocalGet(x));
439            }
440            Self::LocalSet(x) => {
441                func.instruction(&Instruction::LocalSet(x));
442            }
443            Self::Drop() => {
444                func.instruction(&Instruction::Drop);
445            }
446            Self::Null() => {
447                func.instruction(&Instruction::RefNull(wasm_encoder::HeapType::EXTERN));
448            }
449        }
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456
457    /// Creates empty TableOps
458    fn empty_test_ops(num_params: u32, num_globals: u32, table_size: i32) -> TableOps {
459        TableOps {
460            num_params,
461            num_globals,
462            table_size,
463            ops: vec![],
464        }
465    }
466
467    /// Creates TableOps with all default opcodes
468    fn test_ops(num_params: u32, num_globals: u32, table_size: i32) -> TableOps {
469        TableOps {
470            num_params,
471            num_globals,
472            table_size,
473            ops: vec![
474                TableOp::Null(),
475                TableOp::Drop(),
476                TableOp::Gc(),
477                TableOp::LocalSet(0),
478                TableOp::LocalGet(0),
479                TableOp::GlobalSet(0),
480                TableOp::GlobalGet(0),
481            ],
482        }
483    }
484
485    #[test]
486    fn mutate_table_ops_with_default_mutator() -> mutatis::Result<()> {
487        let _ = env_logger::try_init();
488        use mutatis::Session;
489        use wasmparser::Validator;
490        let mut res = test_ops(5, 5, 5);
491        let mut session = Session::new();
492
493        for _ in 0..1024 {
494            session.mutate(&mut res)?;
495            let wasm = res.to_wasm_binary();
496            let mut validator = Validator::new();
497            let wat = wasmprinter::print_bytes(&wasm).expect("[-] Failed .print_bytes(&wasm).");
498            let result = validator.validate_all(&wasm);
499            log::debug!("{wat}");
500            assert!(
501                result.is_ok(),
502                "\n[-] Invalid wat: {}\n\t\t==== Failed Wat ====\n{}",
503                result.err().expect("[-] Failed .err() in assert macro."),
504                wat
505            );
506        }
507        Ok(())
508    }
509
510    #[test]
511    fn every_op_generated() -> mutatis::Result<()> {
512        let _ = env_logger::try_init();
513        let mut unseen_ops: std::collections::HashSet<_> = OP_NAMES.iter().copied().collect();
514
515        let mut res = empty_test_ops(5, 5, 5);
516        let mut generator = TableOpsMutator;
517        let mut session = mutatis::Session::new();
518
519        'outer: for _ in 0..=1024 {
520            session.mutate_with(&mut generator, &mut res)?;
521            for op in &res.ops {
522                unseen_ops.remove(op.name());
523                if unseen_ops.is_empty() {
524                    break 'outer;
525                }
526            }
527        }
528        assert!(unseen_ops.is_empty(), "Failed to generate {unseen_ops:?}");
529        Ok(())
530    }
531
532    #[test]
533    fn test_wat_string() -> mutatis::Result<()> {
534        let _ = env_logger::try_init();
535
536        let mut table_ops = test_ops(2, 2, 5);
537        table_ops.ops.extend([
538            TableOp::Null(),
539            TableOp::Drop(),
540            TableOp::Gc(),
541            TableOp::LocalSet(0),
542            TableOp::LocalGet(0),
543            TableOp::GlobalSet(0),
544            TableOp::GlobalGet(0),
545            TableOp::Null(),
546            TableOp::Drop(),
547            TableOp::Gc(),
548            TableOp::LocalSet(0),
549            TableOp::LocalGet(0),
550            TableOp::GlobalSet(0),
551            TableOp::GlobalGet(0),
552            TableOp::Null(),
553            TableOp::Drop(),
554        ]);
555        let wasm = table_ops.to_wasm_binary();
556        let wat = wasmprinter::print_bytes(&wasm).expect("Failed to convert to WAT");
557        let expected = r#"
558        (module
559        (type (;0;) (func (result externref externref externref)))
560        (type (;1;) (func (param externref externref)))
561        (type (;2;) (func (param externref externref externref)))
562        (type (;3;) (func (result externref externref externref)))
563        (import "" "gc" (func (;0;) (type 0)))
564        (import "" "take_refs" (func (;1;) (type 2)))
565        (import "" "make_refs" (func (;2;) (type 3)))
566        (table (;0;) 5 externref)
567        (global (;0;) (mut externref) ref.null extern)
568        (global (;1;) (mut externref) ref.null extern)
569        (export "run" (func 3))
570        (func (;3;) (type 1) (param externref externref)
571            (local externref)
572            loop ;; label = @1
573            ref.null extern
574            drop
575            call 0
576            local.set 0
577            local.get 0
578            global.set 0
579            global.get 0
580            ref.null extern
581            drop
582            call 0
583            local.set 0
584            local.get 0
585            global.set 0
586            global.get 0
587            ref.null extern
588            drop
589            call 0
590            local.set 0
591            local.get 0
592            global.set 0
593            global.get 0
594            ref.null extern
595            drop
596            br 0 (;@1;)
597            end
598        )
599        )
600        "#;
601
602        let generated = wat.split_whitespace().collect::<Vec<_>>().join(" ");
603        let expected = expected.split_whitespace().collect::<Vec<_>>().join(" ");
604        assert_eq!(generated, expected, "WAT does not match expected");
605
606        Ok(())
607    }
608}