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