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