1 use crate::generators::{Config, DiffValue, DiffValueType}; 2 use crate::oracles::engine::{DiffEngine, DiffInstance}; 3 use std::cell::RefCell; 4 use std::rc::Rc; 5 use std::sync::Once; 6 use wasmtime::{Error, Result, Trap, bail}; 7 8 pub struct V8Engine { 9 isolate: Rc<RefCell<v8::OwnedIsolate>>, 10 } 11 12 impl V8Engine { 13 pub fn new(config: &mut Config) -> V8Engine { 14 static INIT: Once = Once::new(); 15 16 INIT.call_once(|| { 17 let platform = v8::new_default_platform(0, false).make_shared(); 18 v8::V8::initialize_platform(platform); 19 v8::V8::initialize(); 20 }); 21 22 let config = &mut config.module_config.config; 23 // FIXME: reference types are disabled for now as we seemingly keep finding 24 // a segfault in v8. This is found relatively quickly locally and keeps 25 // getting found by oss-fuzz and currently we don't think that there's 26 // really much we can do about it. For the time being disable reference 27 // types entirely. An example bug is 28 // https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=45662 29 config.reference_types_enabled = false; 30 31 config.min_memories = config.min_memories.min(1); 32 config.max_memories = config.max_memories.min(1); 33 config.memory64_enabled = false; 34 config.custom_page_sizes_enabled = false; 35 config.wide_arithmetic_enabled = false; 36 37 Self { 38 isolate: Rc::new(RefCell::new(v8::Isolate::new(Default::default()))), 39 } 40 } 41 } 42 43 impl DiffEngine for V8Engine { 44 fn name(&self) -> &'static str { 45 "v8" 46 } 47 48 fn instantiate(&mut self, wasm: &[u8]) -> Result<Box<dyn DiffInstance>> { 49 // Setup a new `Context` in which we'll be creating this instance and 50 // executing code. 51 let mut isolate = self.isolate.borrow_mut(); 52 let isolate = &mut **isolate; 53 let mut scope = v8::HandleScope::new(isolate); 54 let context = v8::Context::new(&mut scope, Default::default()); 55 let global = context.global(&mut scope); 56 let mut scope = v8::ContextScope::new(&mut scope, context); 57 58 // Move the `wasm` into JS and then invoke `new WebAssembly.Module`. 59 let buf = v8::ArrayBuffer::new_backing_store_from_boxed_slice(wasm.into()); 60 let buf = v8::SharedRef::from(buf); 61 let name = v8::String::new(&mut scope, "WASM_BINARY").unwrap(); 62 let buf = v8::ArrayBuffer::with_backing_store(&mut scope, &buf); 63 global.set(&mut scope, name.into(), buf.into()); 64 let module = eval(&mut scope, "new WebAssembly.Module(WASM_BINARY)").unwrap(); 65 let name = v8::String::new(&mut scope, "WASM_MODULE").unwrap(); 66 global.set(&mut scope, name.into(), module); 67 68 // Using our `WASM_MODULE` run instantiation. Note that it's guaranteed 69 // that nothing is imported into differentially-executed modules so 70 // this is expected to only take the module argument. 71 let instance = eval(&mut scope, "new WebAssembly.Instance(WASM_MODULE)")?; 72 73 Ok(Box::new(V8Instance { 74 isolate: self.isolate.clone(), 75 context: v8::Global::new(&mut scope, context), 76 instance: v8::Global::new(&mut scope, instance), 77 })) 78 } 79 80 fn assert_error_match(&self, err: &Error, wasmtime: &Trap) { 81 let v8 = err.to_string(); 82 let wasmtime_msg = wasmtime.to_string(); 83 let verify_wasmtime = |msg: &str| { 84 assert!(wasmtime_msg.contains(msg), "{wasmtime_msg}\n!=\n{v8}"); 85 }; 86 let verify_v8 = |msg: &[&str]| { 87 assert!( 88 msg.iter().any(|msg| v8.contains(msg)), 89 "{wasmtime_msg:?}\n\t!=\n{v8}" 90 ); 91 }; 92 match wasmtime { 93 Trap::MemoryOutOfBounds => { 94 return verify_v8(&["memory access out of bounds", "is out of bounds"]); 95 } 96 Trap::UnreachableCodeReached => { 97 return verify_v8(&[ 98 "unreachable", 99 // All the wasms we test use wasm-smith's 100 // `ensure_termination` option which will `unreachable` when 101 // "fuel" runs out within the wasm module itself. This 102 // sometimes manifests as a call stack size exceeded in v8, 103 // however, since v8 sometimes has different limits on the 104 // call-stack especially when it's run multiple times. To 105 // get these error messages to line up allow v8 to say the 106 // call stack size exceeded when wasmtime says we hit 107 // unreachable. 108 "Maximum call stack size exceeded", 109 ]); 110 } 111 Trap::IntegerDivisionByZero => { 112 return verify_v8(&["divide by zero", "remainder by zero"]); 113 } 114 Trap::StackOverflow => { 115 return verify_v8(&[ 116 "call stack size exceeded", 117 // Similar to the above comment in `UnreachableCodeReached` 118 // if wasmtime hits a stack overflow but v8 ran all the way 119 // to when the `unreachable` instruction was hit then that's 120 // ok. This just means that wasmtime either has less optimal 121 // codegen or different limits on the stack than v8 does, 122 // which isn't an issue per-se. 123 "unreachable", 124 ]); 125 } 126 Trap::IndirectCallToNull => return verify_v8(&["null function"]), 127 Trap::TableOutOfBounds => { 128 return verify_v8(&[ 129 "table initializer is out of bounds", 130 "table index is out of bounds", 131 "element segment out of bounds", 132 ]); 133 } 134 Trap::BadSignature => return verify_v8(&["function signature mismatch"]), 135 Trap::IntegerOverflow | Trap::BadConversionToInteger => { 136 return verify_v8(&[ 137 "float unrepresentable in integer range", 138 "divide result unrepresentable", 139 ]); 140 } 141 other => log::debug!("unknown code {other:?}"), 142 } 143 144 verify_wasmtime("not possibly present in an error, just panic please"); 145 } 146 147 fn is_non_deterministic_error(&self, err: &Error) -> bool { 148 err.to_string().contains("Maximum call stack size exceeded") 149 } 150 } 151 152 struct V8Instance { 153 isolate: Rc<RefCell<v8::OwnedIsolate>>, 154 context: v8::Global<v8::Context>, 155 instance: v8::Global<v8::Value>, 156 } 157 158 impl DiffInstance for V8Instance { 159 fn name(&self) -> &'static str { 160 "v8" 161 } 162 163 fn evaluate( 164 &mut self, 165 function_name: &str, 166 arguments: &[DiffValue], 167 result_tys: &[DiffValueType], 168 ) -> Result<Option<Vec<DiffValue>>> { 169 let mut isolate = self.isolate.borrow_mut(); 170 let isolate = &mut **isolate; 171 let mut scope = v8::HandleScope::new(isolate); 172 let context = v8::Local::new(&mut scope, &self.context); 173 let global = context.global(&mut scope); 174 let mut scope = v8::ContextScope::new(&mut scope, context); 175 176 // See https://webassembly.github.io/spec/js-api/index.html#tojsvalue 177 // for how the Wasm-to-JS conversions are done. 178 let mut params = Vec::new(); 179 for arg in arguments { 180 params.push(match *arg { 181 DiffValue::I32(n) => v8::Number::new(&mut scope, n.into()).into(), 182 DiffValue::F32(n) => v8::Number::new(&mut scope, f32::from_bits(n).into()).into(), 183 DiffValue::F64(n) => v8::Number::new(&mut scope, f64::from_bits(n)).into(), 184 DiffValue::I64(n) => v8::BigInt::new_from_i64(&mut scope, n).into(), 185 DiffValue::FuncRef { null } | DiffValue::ExternRef { null } => { 186 assert!(null); 187 v8::null(&mut scope).into() 188 } 189 // JS doesn't support v128 parameters 190 DiffValue::V128(_) => return Ok(None), 191 DiffValue::AnyRef { .. } => unimplemented!(), 192 DiffValue::ExnRef { .. } => unimplemented!(), 193 DiffValue::ContRef { .. } => unimplemented!(), 194 }); 195 } 196 // JS doesn't support v128 return values 197 for ty in result_tys { 198 if let DiffValueType::V128 = ty { 199 return Ok(None); 200 } 201 } 202 203 let name = v8::String::new(&mut scope, "WASM_INSTANCE").unwrap(); 204 let instance = v8::Local::new(&mut scope, &self.instance); 205 global.set(&mut scope, name.into(), instance); 206 let name = v8::String::new(&mut scope, "EXPORT_NAME").unwrap(); 207 let func_name = v8::String::new(&mut scope, function_name).unwrap(); 208 global.set(&mut scope, name.into(), func_name.into()); 209 let name = v8::String::new(&mut scope, "ARGS").unwrap(); 210 let params = v8::Array::new_with_elements(&mut scope, ¶ms); 211 global.set(&mut scope, name.into(), params.into()); 212 let v8_vals = eval(&mut scope, "WASM_INSTANCE.exports[EXPORT_NAME](...ARGS)")?; 213 214 let mut results = Vec::new(); 215 match result_tys.len() { 216 0 => assert!(v8_vals.is_undefined()), 217 1 => results.push(get_diff_value(&v8_vals, result_tys[0], &mut scope)), 218 _ => { 219 let array = v8::Local::<'_, v8::Array>::try_from(v8_vals).unwrap(); 220 for (i, ty) in result_tys.iter().enumerate() { 221 let v8 = array.get_index(&mut scope, i as u32).unwrap(); 222 results.push(get_diff_value(&v8, *ty, &mut scope)); 223 } 224 } 225 } 226 Ok(Some(results)) 227 } 228 229 fn get_global(&mut self, global_name: &str, ty: DiffValueType) -> Option<DiffValue> { 230 if let DiffValueType::V128 = ty { 231 return None; 232 } 233 let mut isolate = self.isolate.borrow_mut(); 234 let mut scope = v8::HandleScope::new(&mut *isolate); 235 let context = v8::Local::new(&mut scope, &self.context); 236 let global = context.global(&mut scope); 237 let mut scope = v8::ContextScope::new(&mut scope, context); 238 239 let name = v8::String::new(&mut scope, "GLOBAL_NAME").unwrap(); 240 let memory_name = v8::String::new(&mut scope, global_name).unwrap(); 241 global.set(&mut scope, name.into(), memory_name.into()); 242 let val = eval(&mut scope, "WASM_INSTANCE.exports[GLOBAL_NAME].value").unwrap(); 243 Some(get_diff_value(&val, ty, &mut scope)) 244 } 245 246 fn get_memory(&mut self, memory_name: &str, shared: bool) -> Option<Vec<u8>> { 247 let mut isolate = self.isolate.borrow_mut(); 248 let mut scope = v8::HandleScope::new(&mut *isolate); 249 let context = v8::Local::new(&mut scope, &self.context); 250 let global = context.global(&mut scope); 251 let mut scope = v8::ContextScope::new(&mut scope, context); 252 253 let name = v8::String::new(&mut scope, "MEMORY_NAME").unwrap(); 254 let memory_name = v8::String::new(&mut scope, memory_name).unwrap(); 255 global.set(&mut scope, name.into(), memory_name.into()); 256 let v8 = eval(&mut scope, "WASM_INSTANCE.exports[MEMORY_NAME].buffer").unwrap(); 257 let v8_data = if shared { 258 v8::Local::<'_, v8::SharedArrayBuffer>::try_from(v8) 259 .unwrap() 260 .get_backing_store() 261 } else { 262 v8::Local::<'_, v8::ArrayBuffer>::try_from(v8) 263 .unwrap() 264 .get_backing_store() 265 }; 266 267 Some(v8_data.iter().map(|i| i.get()).collect()) 268 } 269 } 270 271 /// Evaluates the JS `code` within `scope`, returning either the result of the 272 /// computation or the stringified exception if one happened. 273 fn eval<'s>(scope: &mut v8::HandleScope<'s>, code: &str) -> Result<v8::Local<'s, v8::Value>> { 274 let mut tc = v8::TryCatch::new(scope); 275 let mut scope = v8::EscapableHandleScope::new(&mut tc); 276 let source = v8::String::new(&mut scope, code).unwrap(); 277 let script = v8::Script::compile(&mut scope, source, None).unwrap(); 278 match script.run(&mut scope) { 279 Some(val) => Ok(scope.escape(val)), 280 None => { 281 drop(scope); 282 assert!(tc.has_caught()); 283 bail!( 284 "{}", 285 tc.message() 286 .unwrap() 287 .get(&mut tc) 288 .to_rust_string_lossy(&mut tc) 289 ) 290 } 291 } 292 } 293 294 fn get_diff_value( 295 val: &v8::Local<'_, v8::Value>, 296 ty: DiffValueType, 297 scope: &mut v8::HandleScope<'_>, 298 ) -> DiffValue { 299 match ty { 300 DiffValueType::I32 => DiffValue::I32(val.to_int32(scope).unwrap().value()), 301 DiffValueType::I64 => { 302 let (val, todo) = val.to_big_int(scope).unwrap().i64_value(); 303 assert!(todo); 304 DiffValue::I64(val) 305 } 306 DiffValueType::F32 => { 307 DiffValue::F32((val.to_number(scope).unwrap().value() as f32).to_bits()) 308 } 309 DiffValueType::F64 => DiffValue::F64(val.to_number(scope).unwrap().value().to_bits()), 310 DiffValueType::FuncRef => DiffValue::FuncRef { 311 null: val.is_null(), 312 }, 313 DiffValueType::ExternRef => DiffValue::ExternRef { 314 null: val.is_null(), 315 }, 316 DiffValueType::AnyRef => unimplemented!(), 317 DiffValueType::ExnRef => unimplemented!(), 318 DiffValueType::V128 => unreachable!(), 319 DiffValueType::ContRef => unimplemented!(), 320 } 321 } 322 323 #[test] 324 fn smoke() { 325 crate::oracles::engine::smoke_test_engine(|_, config| Ok(V8Engine::new(config))) 326 } 327