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