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