1 use crate::generators::gc_ops::{
2     limits::GcOpsLimits,
3     ops::{GcOp, GcOps, OP_NAMES},
4     types::{RecGroupId, TypeId, Types},
5 };
6 use mutatis;
7 use rand::rngs::StdRng;
8 use rand::{Rng, SeedableRng};
9 use wasmparser;
10 use wasmprinter;
11 
12 /// Creates empty GcOps
13 fn empty_test_ops() -> GcOps {
14     let mut t = GcOps {
15         limits: GcOpsLimits {
16             num_params: 5,
17             num_globals: 5,
18             table_size: 5,
19             max_rec_groups: 5,
20             max_types: 5,
21         },
22         ops: vec![],
23         types: Types::new(),
24     };
25     for i in 0..t.limits.max_rec_groups {
26         t.types.insert_rec_group(RecGroupId(i));
27     }
28     t
29 }
30 
31 /// Creates GcOps with all default opcodes
32 fn test_ops(num_params: u32, num_globals: u32, table_size: u32) -> GcOps {
33     let mut t = GcOps {
34         limits: GcOpsLimits {
35             num_params,
36             num_globals,
37             table_size,
38             max_rec_groups: 7,
39             max_types: 10,
40         },
41         ops: vec![
42             GcOp::NullExtern,
43             GcOp::Drop,
44             GcOp::Gc,
45             GcOp::LocalSet { local_index: 0 },
46             GcOp::LocalGet { local_index: 0 },
47             GcOp::GlobalSet { global_index: 0 },
48             GcOp::GlobalGet { global_index: 0 },
49             GcOp::StructNew { type_index: 0 },
50         ],
51         types: Types::new(),
52     };
53 
54     for i in 0..t.limits.max_rec_groups {
55         t.types.insert_rec_group(RecGroupId(i));
56     }
57 
58     let mut rng = StdRng::seed_from_u64(0xC0FFEE);
59     if t.limits.max_rec_groups > 0 {
60         for i in 0..t.limits.max_types {
61             let gid = RecGroupId(rng.gen_range(0..t.limits.max_rec_groups));
62             t.types.insert_empty_struct(TypeId(i), gid);
63         }
64     }
65 
66     t
67 }
68 
69 #[test]
70 fn mutate_gc_ops_with_default_mutator() -> mutatis::Result<()> {
71     let _ = env_logger::try_init();
72 
73     let mut features = wasmparser::WasmFeatures::default();
74     features.insert(wasmparser::WasmFeatures::REFERENCE_TYPES);
75     features.insert(wasmparser::WasmFeatures::FUNCTION_REFERENCES);
76     features.insert(wasmparser::WasmFeatures::GC_TYPES);
77     features.insert(wasmparser::WasmFeatures::GC);
78 
79     let mut ops = test_ops(5, 5, 5);
80 
81     let mut session = mutatis::Session::new();
82     for _ in 0..2048 {
83         session.mutate(&mut ops)?;
84 
85         let wasm = ops.to_wasm_binary();
86         crate::oracles::log_wasm(&wasm);
87 
88         let mut validator = wasmparser::Validator::new_with_features(features);
89         if let Err(e) = validator.validate_all(&wasm) {
90             let mut config = wasmprinter::Config::new();
91             config.print_offsets(true);
92             config.print_operand_stack(true);
93             let mut wat = String::new();
94             let wat = match config.print(&wasm, &mut wasmprinter::PrintFmtWrite(&mut wat)) {
95                 Ok(()) => wat,
96                 Err(e) => format!("<failed to disassemble Wasm binary to WAT: {e}>"),
97             };
98             panic!(
99                 "Emitted Wasm binary is not valid!\n\n\
100                  === Validation Error ===\n\n\
101                  {e}\n\n\
102                  === GcOps ===\n\n\
103                  {ops:#?}\n\n\
104                  === Wat ===\n\n\
105                  {wat}"
106             );
107         }
108     }
109     Ok(())
110 }
111 
112 #[test]
113 fn struct_new_removed_when_no_types() -> mutatis::Result<()> {
114     let _ = env_logger::try_init();
115 
116     let mut ops = test_ops(0, 0, 0);
117     ops.limits.max_types = 0;
118     ops.ops = vec![GcOp::StructNew { type_index: 42 }];
119 
120     ops.fixup();
121     assert!(
122         ops.ops
123             .iter()
124             .all(|op| !matches!(op, GcOp::StructNew { .. })),
125         "StructNew should be removed when there are no types"
126     );
127     Ok(())
128 }
129 
130 #[test]
131 fn local_ops_removed_when_no_params() -> mutatis::Result<()> {
132     let _ = env_logger::try_init();
133 
134     let mut ops = test_ops(0, 0, 0);
135     ops.limits.num_params = 0;
136     ops.ops = vec![
137         GcOp::LocalGet { local_index: 42 },
138         GcOp::LocalSet { local_index: 99 },
139     ];
140 
141     ops.fixup();
142     assert!(
143         ops.ops
144             .iter()
145             .all(|op| !matches!(op, GcOp::LocalGet { .. } | GcOp::LocalSet { .. })),
146         "LocalGet/LocalSet should be removed when there are no params"
147     );
148     Ok(())
149 }
150 
151 #[test]
152 fn global_ops_removed_when_no_globals() -> mutatis::Result<()> {
153     let _ = env_logger::try_init();
154 
155     let mut ops = test_ops(0, 0, 0);
156     ops.limits.num_globals = 0;
157     ops.ops = vec![
158         GcOp::GlobalGet { global_index: 42 },
159         GcOp::GlobalSet { global_index: 99 },
160     ];
161 
162     ops.fixup();
163     assert!(
164         ops.ops
165             .iter()
166             .all(|op| !matches!(op, GcOp::GlobalGet { .. } | GcOp::GlobalSet { .. })),
167         "GlobalGet/GlobalSet should be removed when there are no globals"
168     );
169     Ok(())
170 }
171 
172 #[test]
173 fn every_op_generated() -> mutatis::Result<()> {
174     let _ = env_logger::try_init();
175     let mut unseen_ops: std::collections::HashSet<_> = OP_NAMES.iter().copied().collect();
176 
177     let mut res = empty_test_ops();
178     let mut session = mutatis::Session::new().seed(0xC0FFEE);
179 
180     'outer: for _ in 0..=1024 {
181         session.mutate(&mut res)?;
182         for op in &res.ops {
183             unseen_ops.remove(op.name());
184             if unseen_ops.is_empty() {
185                 break 'outer;
186             }
187         }
188     }
189 
190     assert!(unseen_ops.is_empty(), "Failed to generate {unseen_ops:?}");
191     Ok(())
192 }
193 
194 #[test]
195 fn emits_empty_rec_groups_and_validates() -> mutatis::Result<()> {
196     let _ = env_logger::try_init();
197 
198     let mut ops = test_ops(5, 5, 5);
199 
200     let wasm = ops.to_wasm_binary();
201 
202     let feats = wasmparser::WasmFeatures::default();
203     feats.reference_types();
204     feats.gc();
205     let mut validator = wasmparser::Validator::new_with_features(feats);
206     assert!(
207         validator.validate_all(&wasm).is_ok(),
208         "GC validation failed"
209     );
210 
211     let wat = wasmprinter::print_bytes(&wasm).expect("to WAT");
212     let recs = wat.matches("(rec").count();
213     let structs = wat.matches("(struct)").count();
214 
215     assert_eq!(recs, 7, "expected 2 (rec) blocks, got {recs}");
216     assert_eq!(structs, 10, "expected no struct types, got {structs}");
217 
218     Ok(())
219 }
220 
221 #[test]
222 fn fixup_check_types_and_indexes() -> mutatis::Result<()> {
223     let _ = env_logger::try_init();
224 
225     let mut ops = test_ops(5, 5, 5);
226 
227     // These `GcOp`s do not have their operands satisfied, and their results are
228     // not the operands of the next op, so `fixup` will need to deal with
229     // that. Additionally, their immediates are out-of-bounds of their
230     // respective index spaces, which `fixup` will also need to address.
231     ops.ops = vec![
232         GcOp::TakeTypedStructCall {
233             type_index: ops.limits.max_types + 7,
234         },
235         GcOp::GlobalSet {
236             global_index: ops.limits.num_globals * 2,
237         },
238         GcOp::StructNew {
239             type_index: ops.limits.max_types + 9,
240         },
241         GcOp::LocalSet {
242             local_index: ops.limits.num_params * 5,
243         },
244     ];
245 
246     // Call `fixup` to insert missing types, rewrite the immediates such that
247     // they are within their bounds, insert missing operands, and drop unused
248     // results.
249     ops.fixup();
250 
251     // Check that we got the expected `GcOp` sequence after `fixup`:
252     assert_eq!(
253         ops.ops,
254         [
255             // Inserted by `fixup` to satisfy `TakeTypedStructCall`'s operands.
256             GcOp::StructNew { type_index: 7 },
257             // The `type_index` is now valid.
258             GcOp::TakeTypedStructCall { type_index: 7 },
259             // Inserted by `fixup` to satisfy `GlobalSet`'s operands.
260             GcOp::NullExtern,
261             // The `global_index` is now valid.
262             GcOp::GlobalSet { global_index: 0 },
263             // The `type_index` is now valid.
264             GcOp::StructNew { type_index: 9 },
265             // Inserted by `fixup` to satisfy `LocalSet`'s operands.
266             GcOp::NullExtern,
267             // The `local_index` is now valid.
268             GcOp::LocalSet { local_index: 0 },
269             // Inserted by `fixup` to make sure the operand stack is empty at
270             // the end of the block.
271             GcOp::Drop,
272         ]
273     );
274 
275     // Verify that we generate a valid Wasm binary after calling `fixup`.
276     let wasm = ops.to_wasm_binary();
277     let wat = wasmprinter::print_bytes(&wasm).unwrap();
278     log::debug!("{wat}");
279     let feats = wasmparser::WasmFeatures::default();
280     feats.reference_types();
281     feats.gc();
282     let mut validator = wasmparser::Validator::new_with_features(feats);
283     assert!(
284         validator.validate_all(&wasm).is_ok(),
285         "GC validation should pass after fixup"
286     );
287 
288     Ok(())
289 }
290