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, &params);
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