Skip to main content

wasmtime_fuzzing/generators/
exception_ops.rs

1//! Generator for exception-handling fuzz test cases.
2//!
3//! Generates Wasm modules that exercise `throw`/`try_table`/`catch` in
4//! various combinations: multiple tags with different signatures, nested
5//! handler scopes, throws at various call depths, catch vs catch_all, and
6//! multiple catch clauses per try_table.
7
8use mutatis::{Candidates, Context, DefaultMutate, Generate, Mutate, Result as MutResult};
9use serde::{Deserialize, Serialize};
10use std::borrow::Cow;
11use wasm_encoder::{
12    BlockType, CodeSection, EntityType, ExportKind, ExportSection, Function, FunctionSection,
13    ImportSection, Instruction, Module, TagKind, TagSection, TagType, TypeSection, ValType,
14};
15
16/// Max number of tags.
17pub const NUM_TAGS_MAX: u32 = 8;
18/// Max call-chain depth (number of functions between run and thrower).
19pub const CALL_DEPTH_MAX: u32 = 6;
20/// Max number of scenarios (throw+catch combos) per test case.
21pub const MAX_SCENARIOS: usize = 16;
22/// Maximum params per tag signature.
23pub const MAX_TAG_PARAMS: usize = 4;
24/// Maximum number of decoy catches.
25pub const MAX_DECOY_CATCHES: usize = 4;
26
27/// Limits controlling the structure of a generated Wasm module.
28#[derive(Debug, Default, Clone, Serialize, Deserialize, Mutate)]
29pub struct ExceptionOpsLimits {
30    /// Number of distinct tags to define.
31    pub(crate) num_tags: u32,
32    /// Depth of the call chain (number of intermediate functions).
33    pub(crate) call_depth: u32,
34}
35
36impl ExceptionOpsLimits {
37    pub(crate) fn fixup(&mut self) {
38        self.num_tags = self.num_tags.clamp(1, NUM_TAGS_MAX);
39        self.call_depth = self.call_depth.clamp(1, CALL_DEPTH_MAX);
40    }
41}
42
43/// A tag signature.
44#[derive(Debug, Clone, Serialize, Deserialize, Mutate)]
45pub struct TagSig {
46    pub(crate) params: Vec<SimpleValType>,
47}
48
49/// Subset of ValType that we generate for tag params.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51#[allow(missing_docs, reason = "self-describing")]
52pub enum SimpleValType {
53    I32,
54    I64,
55    F32,
56    F64,
57}
58
59impl SimpleValType {
60    fn to_val_type(self) -> ValType {
61        match self {
62            Self::I32 => ValType::I32,
63            Self::I64 => ValType::I64,
64            Self::F32 => ValType::F32,
65            Self::F64 => ValType::F64,
66        }
67    }
68
69    /// A deterministic "interesting" constant for the given type and index,
70    /// used as the thrown payload so the oracle can verify the catch.
71    fn test_value(self, idx: u32) -> Instruction<'static> {
72        match self {
73            Self::I32 => Instruction::I32Const(0x1000_i32.wrapping_add(idx.cast_signed())),
74            Self::I64 => Instruction::I64Const(0x2000_i64.wrapping_add(i64::from(idx))),
75            Self::F32 => Instruction::F32Const(wasm_encoder::Ieee32::new(0x4000_0000 + idx)),
76            Self::F64 => Instruction::F64Const(wasm_encoder::Ieee64::new(
77                0x4000_0000_0000_0000 + u64::from(idx),
78            )),
79        }
80    }
81}
82
83/// Which kind of catch clause to use for the handler.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
85pub enum CatchKind {
86    /// `(catch $tag $label)`: specific tag, payload on branch.
87    Catch,
88    /// `(catch_all $label)`: any tag, no payload on branch.
89    CatchAll,
90}
91
92/// One throw-and-catch scenario.
93#[derive(Debug, Clone, Serialize, Deserialize, Mutate)]
94pub struct Scenario {
95    /// Index (into our tag list) of the tag to throw.
96    pub(crate) throw_tag: u32,
97    /// The function depth at which the throw happens (0 = run, call_depth = deepest).
98    pub(crate) throw_depth: u32,
99    /// The function depth at which the handler lives. Must be <= throw_depth.
100    pub(crate) catch_depth: u32,
101    /// What kind of catch clause to use.
102    pub(crate) catch_kind: CatchKind,
103    /// Extra catch clauses (tag indices) to place *before* the real one in the
104    /// try_table, exercising the "skip non-matching" path. These must be
105    /// indices of tags different from `throw_tag`.
106    pub(crate) decoy_catches: Vec<u32>,
107    /// Extra catch clauses (tag indices) to place *after* the real one in the
108    /// try_table, exercising the "clauses past the match" path. These must be
109    /// indices of tags different from `throw_tag`.
110    pub(crate) decoy_catches_after: Vec<u32>,
111}
112
113/// A description of a Wasm module that exercises exception throw/catch.
114#[derive(Debug, Default, Clone, Serialize, Deserialize, Mutate)]
115pub struct ExceptionOps {
116    pub(crate) limits: ExceptionOpsLimits,
117    pub(crate) tag_sigs: Vec<TagSig>,
118    pub(crate) scenarios: Vec<Scenario>,
119}
120
121impl ExceptionOps {
122    /// Encode as a Wasm module.
123    pub fn to_wasm_binary(&mut self) -> Vec<u8> {
124        self.fixup();
125        self.encode()
126    }
127
128    /// Fix up the test case to ensure all indices and structures are valid.
129    pub fn fixup(&mut self) {
130        self.limits.fixup();
131        let num_tags = usize::try_from(self.limits.num_tags).unwrap();
132
133        // Ensure we have exactly num_tags tag signatures.
134        while self.tag_sigs.len() < num_tags {
135            // Default: single i32 param.
136            self.tag_sigs.push(TagSig {
137                params: vec![SimpleValType::I32],
138            });
139        }
140        self.tag_sigs.truncate(num_tags);
141
142        // Clamp param counts.
143        for sig in &mut self.tag_sigs {
144            sig.params.truncate(MAX_TAG_PARAMS);
145            if sig.params.is_empty() {
146                sig.params.push(SimpleValType::I32);
147            }
148        }
149
150        // Ensure at least one scenario.
151        if self.scenarios.is_empty() {
152            self.scenarios.push(Scenario {
153                throw_tag: 0,
154                throw_depth: self.limits.call_depth,
155                catch_depth: 0,
156                catch_kind: CatchKind::Catch,
157                decoy_catches: vec![],
158                decoy_catches_after: vec![],
159            });
160        }
161        self.scenarios.truncate(MAX_SCENARIOS);
162
163        let num_tags = self.limits.num_tags;
164        let depth = self.limits.call_depth;
165        for s in &mut self.scenarios {
166            s.throw_tag = s.throw_tag % num_tags;
167            s.throw_depth = s.throw_depth.clamp(1, depth);
168            s.catch_depth = s.catch_depth.clamp(0, s.throw_depth - 1);
169            // Decoy catches: keep only tags different from the real one.
170            s.decoy_catches.retain(|t| *t % num_tags != s.throw_tag);
171            for t in &mut s.decoy_catches {
172                *t = *t % num_tags;
173            }
174            s.decoy_catches.truncate(MAX_DECOY_CATCHES);
175            s.decoy_catches_after
176                .retain(|t| *t % num_tags != s.throw_tag);
177            for t in &mut s.decoy_catches_after {
178                *t = *t % num_tags;
179            }
180            s.decoy_catches_after.truncate(MAX_DECOY_CATCHES);
181        }
182    }
183
184    /// Module layout:
185    ///
186    /// Types:
187    ///   0..num_tags                       tag signatures: (params) -> ()
188    ///   num_tags..2*num_tags              catch block types: () -> (params)
189    ///   2*num_tags                        () -> (i32)  (run & relay functions)
190    ///   2*num_tags + 1                    (i32, i32) -> ()  (check_i32)
191    ///   2*num_tags + 2                    () -> ()  (thrower functions)
192    ///
193    /// Tags:  0..num_tags (using type indices 0..num_tags)
194    /// Imports:  0: "check_i32"
195    /// Functions:  per-scenario call chains + "run"
196    /// Exports:  "run"
197    fn encode(&self) -> Vec<u8> {
198        let num_tags = self.limits.num_tags;
199        let call_depth = self.limits.call_depth;
200        let num_scenarios = u32::try_from(self.scenarios.len()).unwrap();
201
202        let mut module = Module::new();
203
204        let mut types = TypeSection::new();
205
206        // Type indices 0..num_tags: tag signatures (params -> [])
207        for sig in &self.tag_sigs {
208            let params: Vec<ValType> = sig.params.iter().map(|t| t.to_val_type()).collect();
209            types.ty().function(params, vec![]);
210        }
211
212        // Type indices num_tags..2*num_tags: catch block types ([] -> params)
213        // These are used as block types for the catch target blocks, since
214        // `catch $tag $label` delivers the tag's param types at the label.
215        let catch_block_type_base = num_tags;
216        for sig in &self.tag_sigs {
217            let results: Vec<ValType> = sig.params.iter().map(|t| t.to_val_type()).collect();
218            types.ty().function(vec![], results);
219        }
220
221        // Utility function types
222        let fn_type_void_to_i32 = types.len();
223        types.ty().function(vec![], vec![ValType::I32]);
224
225        let fn_type_check = types.len();
226        types
227            .ty()
228            .function(vec![ValType::I32, ValType::I32], vec![]);
229
230        let fn_type_void_to_void = types.len();
231        types.ty().function(vec![], vec![]);
232
233        let mut tags = TagSection::new();
234        for i in 0..num_tags {
235            tags.tag(TagType {
236                kind: TagKind::Exception,
237                func_type_idx: i,
238            });
239        }
240
241        let mut imports = ImportSection::new();
242        let check_func_idx: u32 = 0;
243        imports.import("", "check_i32", EntityType::Function(fn_type_check));
244        let import_count: u32 = imports.len();
245
246        // For each scenario: (call_depth + 1) functions at depths 0..=call_depth.
247        //   - depth == catch_depth: handler function () -> (i32)
248        //   - depth == throw_depth: thrower function () -> ()
249        //   - other depths < throw_depth: relay function () -> (i32)
250        //   - other depths > throw_depth: unreachable () -> ()
251        //
252        // Plus one "run" function that calls each scenario's depth-0 function.
253
254        let funcs_per_scenario = call_depth + 1;
255        let run_defined_idx = num_scenarios * funcs_per_scenario;
256        let run_func_idx = import_count + run_defined_idx;
257
258        let mut functions = FunctionSection::new();
259        let mut code = CodeSection::new();
260
261        for (si, scenario) in self.scenarios.iter().enumerate() {
262            let scenario_base = import_count + u32::try_from(si).unwrap() * funcs_per_scenario;
263            let tag_sig = &self.tag_sigs[usize::try_from(scenario.throw_tag).unwrap()];
264
265            for d in 0..=call_depth {
266                if d == scenario.throw_depth {
267                    // -- Thrower: pushes payload, throws tag --
268                    functions.function(fn_type_void_to_void);
269                    let mut f = Function::new(vec![]);
270                    for (pi, param_ty) in tag_sig.params.iter().enumerate() {
271                        f.instruction(&param_ty.test_value(
272                            scenario.throw_tag * u32::try_from(MAX_TAG_PARAMS).unwrap()
273                                + u32::try_from(pi).unwrap(),
274                        ));
275                    }
276                    f.instruction(&Instruction::Throw(scenario.throw_tag));
277                    f.instruction(&Instruction::End);
278                    code.function(&f);
279                } else if d == scenario.catch_depth {
280                    // -- Handler: try_table with catch clauses --
281                    functions.function(fn_type_void_to_i32);
282                    let mut f = Function::new(vec![]);
283
284                    // All decoys (before + after the real catch) share the same
285                    // block structure; only the catch-clause ordering differs.
286                    let all_decoys: Vec<u32> = scenario
287                        .decoy_catches
288                        .iter()
289                        .chain(scenario.decoy_catches_after.iter())
290                        .copied()
291                        .collect();
292                    let num_decoys = all_decoys.len();
293                    let num_before = scenario.decoy_catches.len();
294
295                    // Block nesting (outermost to innermost):
296                    //   block $result (result i32)
297                    //     block $catch (result <tag_params> or empty)
298                    //       block $decoy_0 (result <decoy params>)
299                    //         ...
300                    //         block $decoy_{n-1} (result <decoy params>)
301                    //           try_table ...
302                    //
303                    // Catch clause labels are relative to the enclosing blocks,
304                    // not counting the try_table itself. So from the catch:
305                    //   label 0 = $decoy_{n-1} (innermost block)
306                    //   label num_decoys-1 = $decoy_0
307                    //   label num_decoys = $catch
308                    //   label num_decoys+1 = $result
309                    //
310                    // br instructions inside the try_table body do count the
311                    // try_table, so they need +1 compared to catch labels.
312
313                    let catch_label = u32::try_from(num_decoys).unwrap();
314                    let result_label = u32::try_from(num_decoys + 1).unwrap();
315
316                    // For br inside try_table body, add 1 for the try_table scope
317                    let br_result_label = result_label + 1;
318
319                    // Build catch clauses: before-decoys, real catch, after-decoys
320                    let mut catches: Vec<wasm_encoder::Catch> = Vec::new();
321                    for (di, &decoy_tag) in scenario.decoy_catches.iter().enumerate() {
322                        let decoy_label = u32::try_from(num_decoys - 1 - di).unwrap();
323                        catches.push(wasm_encoder::Catch::One {
324                            tag: decoy_tag,
325                            label: decoy_label,
326                        });
327                    }
328                    match scenario.catch_kind {
329                        CatchKind::Catch => {
330                            catches.push(wasm_encoder::Catch::One {
331                                tag: scenario.throw_tag,
332                                label: catch_label,
333                            });
334                        }
335                        CatchKind::CatchAll => {
336                            catches.push(wasm_encoder::Catch::All { label: catch_label });
337                        }
338                    }
339                    for (i, &decoy_tag) in scenario.decoy_catches_after.iter().enumerate() {
340                        let di = num_before + i;
341                        let decoy_label = u32::try_from(num_decoys - 1 - di).unwrap();
342                        catches.push(wasm_encoder::Catch::One {
343                            tag: decoy_tag,
344                            label: decoy_label,
345                        });
346                    }
347
348                    // Emit blocks (outermost first)
349                    // block $result (result i32)
350                    f.instruction(&Instruction::Block(BlockType::Result(ValType::I32)));
351
352                    // block $catch
353                    match scenario.catch_kind {
354                        CatchKind::Catch => {
355                            let bt =
356                                BlockType::FunctionType(catch_block_type_base + scenario.throw_tag);
357                            f.instruction(&Instruction::Block(bt));
358                        }
359                        CatchKind::CatchAll => {
360                            f.instruction(&Instruction::Block(BlockType::Empty));
361                        }
362                    }
363
364                    // Decoy blocks (decoy_0 outermost, decoy_{n-1} innermost)
365                    for &decoy_tag in &all_decoys {
366                        let bt = BlockType::FunctionType(catch_block_type_base + decoy_tag);
367                        f.instruction(&Instruction::Block(bt));
368                    }
369
370                    // try_table (empty body type -- all exits via branches)
371                    f.instruction(&Instruction::TryTable(
372                        BlockType::Empty,
373                        Cow::Borrowed(&catches),
374                    ));
375
376                    // Body: call next deeper function
377                    let next_func = scenario_base + d + 1;
378                    f.instruction(&Instruction::Call(next_func));
379
380                    // Normal completion (no exception): push 0, branch to $result
381                    f.instruction(&Instruction::I32Const(0));
382                    f.instruction(&Instruction::Br(br_result_label));
383
384                    f.instruction(&Instruction::End); // end try_table
385                    // Normal flow always exits via br above; make fall-through unreachable
386                    f.instruction(&Instruction::Unreachable);
387
388                    // End decoy blocks (innermost first) — drop all payload, push -1
389                    // After ending $decoy_{di}, we're inside $decoy_{di-1}
390                    // (or $catch if di==0). Depth to $result = di + 1.
391                    for di in (0..num_decoys).rev() {
392                        f.instruction(&Instruction::End); // end block $decoy_{di}
393                        // Drop the caught payload values
394                        let decoy_tag = all_decoys[di];
395                        let decoy_sig = &self.tag_sigs[usize::try_from(decoy_tag).unwrap()];
396                        for _ in &decoy_sig.params {
397                            f.instruction(&Instruction::Drop);
398                        }
399                        // Wrong tag caught -- return -1
400                        f.instruction(&Instruction::I32Const(-1));
401                        let depth_to_result = u32::try_from(di).unwrap() + 1;
402                        f.instruction(&Instruction::Br(depth_to_result));
403                    }
404
405                    // End $catch block -- verify payload and return 1
406                    f.instruction(&Instruction::End); // end block $catch
407                    match scenario.catch_kind {
408                        CatchKind::Catch => {
409                            // All tag param values are on the stack.
410                            // Verify the first i32 value if present, drop the rest.
411                            let nparams = tag_sig.params.len();
412                            // Drop all values from top (last param) down to first
413                            for _ in 1..nparams {
414                                f.instruction(&Instruction::Drop);
415                            }
416                            // First param is now on top
417                            if tag_sig.params[0] == SimpleValType::I32 {
418                                let idx =
419                                    scenario.throw_tag * u32::try_from(MAX_TAG_PARAMS).unwrap();
420                                let expected = 0x1000_i32.wrapping_add(i32::try_from(idx).unwrap());
421                                f.instruction(&Instruction::I32Const(expected));
422                                f.instruction(&Instruction::Call(check_func_idx));
423                            } else {
424                                f.instruction(&Instruction::Drop);
425                            }
426                            f.instruction(&Instruction::I32Const(1));
427                        }
428                        CatchKind::CatchAll => {
429                            f.instruction(&Instruction::I32Const(1));
430                        }
431                    }
432
433                    f.instruction(&Instruction::End); // end block $result
434                    f.instruction(&Instruction::End); // end function
435                    code.function(&f);
436                } else if d < scenario.throw_depth && d < scenario.catch_depth {
437                    // -- Relay above handler: calls next deeper, returns i32 --
438                    functions.function(fn_type_void_to_i32);
439                    let mut f = Function::new(vec![]);
440                    let next_func = scenario_base + d + 1;
441                    f.instruction(&Instruction::Call(next_func));
442                    f.instruction(&Instruction::End);
443                    code.function(&f);
444                } else if d < scenario.throw_depth {
445                    // -- Relay between handler and thrower: void --
446                    // These functions will never return normally (thrower always throws),
447                    // but the validator needs the types to be correct.
448                    functions.function(fn_type_void_to_void);
449                    let mut f = Function::new(vec![]);
450                    let next_func = scenario_base + d + 1;
451                    f.instruction(&Instruction::Call(next_func));
452                    f.instruction(&Instruction::End);
453                    code.function(&f);
454                } else {
455                    // -- Dead code beyond throw depth --
456                    functions.function(fn_type_void_to_void);
457                    let mut f = Function::new(vec![]);
458                    f.instruction(&Instruction::Unreachable);
459                    f.instruction(&Instruction::End);
460                    code.function(&f);
461                }
462            }
463        }
464
465        // -- "run" function: calls each scenario entry, sums results --
466        functions.function(fn_type_void_to_i32);
467        {
468            let mut f = Function::new(vec![(1, ValType::I32)]);
469            for si in 0..num_scenarios {
470                let scenario_entry = import_count + si * funcs_per_scenario;
471                f.instruction(&Instruction::Call(scenario_entry));
472                f.instruction(&Instruction::LocalGet(0));
473                f.instruction(&Instruction::I32Add);
474                f.instruction(&Instruction::LocalSet(0));
475            }
476            f.instruction(&Instruction::LocalGet(0));
477            f.instruction(&Instruction::End);
478            code.function(&f);
479        }
480
481        let mut exports = ExportSection::new();
482        exports.export("run", ExportKind::Func, run_func_idx);
483
484        module
485            .section(&types)
486            .section(&imports)
487            .section(&functions)
488            .section(&tags)
489            .section(&exports)
490            .section(&code);
491
492        module.finish()
493    }
494
495    /// Pop the last scenario. Returns true if one was removed.
496    pub fn pop(&mut self) -> bool {
497        self.scenarios.pop().is_some()
498    }
499
500    /// Number of scenarios; expected return value from "run" when all
501    /// catches succeed.
502    pub fn expected_result(&mut self) -> i32 {
503        self.fixup();
504        i32::try_from(self.scenarios.len()).unwrap()
505    }
506}
507
508/// Mutator for unit-variant enums ([`SimpleValType`] and [`CatchKind`]),
509/// which need manual impls because `#[derive(Mutate)]` doesn't switch
510/// between variants.
511#[derive(Debug, Default)]
512pub struct EnumMutator;
513
514impl Mutate<SimpleValType> for EnumMutator {
515    fn mutate(&mut self, c: &mut Candidates<'_>, value: &mut SimpleValType) -> MutResult<()> {
516        c.mutation(|ctx| {
517            let choices = [
518                SimpleValType::I32,
519                SimpleValType::I64,
520                SimpleValType::F32,
521                SimpleValType::F64,
522            ];
523            *value = *ctx.rng().choose(&choices).unwrap();
524            Ok(())
525        })?;
526        Ok(())
527    }
528}
529
530impl Generate<SimpleValType> for EnumMutator {
531    fn generate(&mut self, ctx: &mut Context) -> MutResult<SimpleValType> {
532        let choices = [
533            SimpleValType::I32,
534            SimpleValType::I64,
535            SimpleValType::F32,
536            SimpleValType::F64,
537        ];
538        Ok(*ctx.rng().choose(&choices).unwrap())
539    }
540}
541
542impl DefaultMutate for SimpleValType {
543    type DefaultMutate = EnumMutator;
544}
545
546impl Mutate<CatchKind> for EnumMutator {
547    fn mutate(&mut self, c: &mut Candidates<'_>, value: &mut CatchKind) -> MutResult<()> {
548        c.mutation(|ctx| {
549            let choices = [CatchKind::Catch, CatchKind::CatchAll];
550            *value = *ctx.rng().choose(&choices).unwrap();
551            Ok(())
552        })?;
553        Ok(())
554    }
555}
556
557impl Generate<CatchKind> for EnumMutator {
558    fn generate(&mut self, ctx: &mut Context) -> MutResult<CatchKind> {
559        let choices = [CatchKind::Catch, CatchKind::CatchAll];
560        Ok(*ctx.rng().choose(&choices).unwrap())
561    }
562}
563
564impl DefaultMutate for CatchKind {
565    type DefaultMutate = EnumMutator;
566}
567
568#[cfg(test)]
569mod tests {
570    use super::*;
571    use wasmparser::WasmFeatures;
572
573    #[test]
574    fn always_produces_valid_wasm() {
575        mutatis::check::Check::new()
576            .iters(200)
577            .run(|ops: &ExceptionOps| {
578                let mut ops = ops.clone();
579                let wasm = ops.to_wasm_binary();
580                let features = WasmFeatures::EXCEPTIONS
581                    | WasmFeatures::GC_TYPES
582                    | WasmFeatures::REFERENCE_TYPES
583                    | WasmFeatures::MULTI_VALUE
584                    | WasmFeatures::FLOATS
585                    | WasmFeatures::SIMD;
586                let mut validator = wasmparser::Validator::new_with_features(features);
587                validator
588                    .validate_all(&wasm)
589                    .map(|_| ())
590                    .map_err(|e| format!("{e}\n{ops:#?}"))
591            })
592            .unwrap();
593    }
594}