| // This script can be customized by setting the following variables in code that |
| // runs before this script. |
| // |
| // The binary to be run. (If not set, we get the filename from argv and read |
| // from it.) |
| var binary; |
| // A second binary to be linked in and run as well. (Can also be read from |
| // argv.) |
| var secondBinary; |
| // Whether we are fuzzing JSPI. In addition to this being set, the "async" and |
| // "await" keywords must be taken out of the /* KEYWORD */ comments (which they |
| // are normally in, so as not to affect normal fuzzing). |
| var JSPI; |
| |
| // Shell integration: find argv and set up readBinary(). |
| var argv; |
| var readBinary; |
| if (typeof process === 'object' && typeof require === 'function') { |
| // Node.js. |
| argv = process.argv.slice(2); |
| readBinary = function(name) { |
| var data = require('fs').readFileSync(name); |
| if (!data.buffer) data = new Uint8Array(data); |
| return data; |
| }; |
| } else { |
| // A shell like D8. |
| if (typeof scriptArgs != 'undefined') { |
| argv = scriptArgs; |
| } else if (typeof arguments != 'undefined') { |
| argv = arguments; |
| } |
| readBinary = function(name) { |
| if (typeof readbuffer === 'function') { |
| return new Uint8Array(readbuffer(name)); |
| } else { |
| return read(name, 'binary'); |
| } |
| }; |
| } |
| |
| if (!binary) { |
| binary = readBinary(argv[0]); |
| } |
| |
| // Normally we call all the exports of the given wasm file. But, if we are |
| // passed a final parameter in the form of "exports:X,Y,Z" then we call |
| // specifically the exports X, Y, and Z. |
| var exportsToCall; |
| |
| // Passing --fuzz-split makes us treat the two input files as split off |
| // from a single one, and we will run them as that single file (ignoring extra |
| // exports from the second one, and from wasm-split itself). This allows us to |
| // get the same behavior from split modules as before the split. |
| var fuzzSplit = false; |
| |
| for (var i = 0; i < argv.length; i++) { |
| var curr = argv[i]; |
| if (curr.startsWith('exports:')) { |
| exportsToCall = curr.substr('exports:'.length).split(','); |
| argv.splice(i, 1); |
| i--; |
| } else if (curr == '--fuzz-split') { |
| fuzzSplit = true; |
| argv.splice(i, 1); |
| i--; |
| } |
| } |
| |
| // If a second parameter is given, it is a second binary that we will link in |
| // with it. |
| if (argv[1]) { |
| secondBinary = readBinary(argv[1]); |
| } |
| |
| // Utilities. |
| function assert(x, y) { |
| if (!x) throw (y || 'assertion failed');// + new Error().stack; |
| } |
| |
| // Print out a value in a way that works well for fuzzing. |
| function printed(x, y) { |
| if (typeof y !== 'undefined') { |
| // A pair of i32s which are a legalized i64. |
| return x + ' ' + y; |
| } else if (x === null) { |
| // JS has just one null. Print that out rather than typeof null which is |
| // 'object', below. |
| return 'null'; |
| } else if (typeof x === 'string') { |
| // Emit a string in the same format as the binaryen interpreter. This |
| // escaping routine must be kept in sync with String::printEscapedJSON. |
| var escaped = ''; |
| for (u of x) { |
| switch (u) { |
| case '"': |
| escaped += '\\"'; |
| continue; |
| case '\\': |
| escaped += '\\\\'; |
| continue; |
| case '\b': |
| escaped += '\\b'; |
| continue; |
| case '\f': |
| escaped += '\\f'; |
| continue; |
| case '\n': |
| escaped += '\\n'; |
| continue; |
| case '\r': |
| escaped += '\\r'; |
| continue; |
| case '\t': |
| escaped += '\\t'; |
| continue; |
| default: |
| break; |
| } |
| |
| var codePoint = u.codePointAt(0); |
| if (32 <= codePoint && codePoint < 127) { |
| escaped += u; |
| continue |
| } |
| |
| var printEscape = (codePoint) => { |
| escaped += '\\u' |
| escaped += ((codePoint & 0xF000) >> 12).toString(16); |
| escaped += ((codePoint & 0x0F00) >> 8).toString(16); |
| escaped += ((codePoint & 0x00F0) >> 4).toString(16); |
| escaped += (codePoint & 0x000F).toString(16); |
| }; |
| |
| if (codePoint < 0x10000) { |
| printEscape(codePoint); |
| } else { |
| printEscape(0xD800 + ((codePoint - 0x10000) >> 10)); |
| printEscape(0xDC00 + ((codePoint - 0x10000) & 0x3FF)); |
| } |
| } |
| return 'string("' + escaped + '")'; |
| } else if (typeof x === 'bigint') { |
| // Print bigints in legalized form, which is two 32-bit numbers of the low |
| // and high bits. |
| return (Number(x) | 0) + ' ' + (Number(x >> 32n) | 0) |
| } else if (typeof x !== 'number') { |
| // Something that is not a number or string, like a reference. We can't |
| // print a reference because it could look different after opts - imagine |
| // that a function gets renamed internally (that is, the problem is that |
| // JS printing will emit some info about the reference and not a stable |
| // external representation of it). In those cases just print the type, |
| // which will be 'object' or 'function'. |
| return typeof x; |
| } else { |
| // A number. Print the whole thing. |
| return '' + x; |
| } |
| } |
| |
| // Fuzzer integration. |
| function logValue(x, y) { |
| console.log('[LoggingExternalInterface logging ' + printed(x, y) + ']'); |
| } |
| |
| // Track the exports in a map (similar to the Exports object from wasm, i.e., |
| // whose keys are strings and whose values are the corresponding exports). |
| var exports = {}; |
| |
| // Also track exports in a list, to allow access by index. Each entry here will |
| // be in the form of { name: .., value: .. }. That allows us to log the name of |
| // the function and also to call it. This is important because different |
| // functions may have the same name, if they were exported by different |
| // Instances under the same export names. |
| var exportList = []; |
| |
| // Given a wasm function, call it as best we can from JS, and return the result. |
| function callFunc(func) { |
| // Send the function a null for each parameter. Null can be converted without |
| // error to both a number and a reference. |
| var args = []; |
| for (var i = 0; i < func.length; i++) { |
| args.push(null); |
| } |
| return func.apply(null, args); |
| } |
| |
| // Calls a given function in a try-catch. Return 1 if an exception was thrown. |
| // If |rethrow| is set, and an exception is thrown, it is caught and rethrown. |
| // Wasm traps are not swallowed (see details below). |
| /* async */ function tryCall(func, rethrow) { |
| try { |
| /* await */ func(); |
| return 0; |
| } catch (e) { |
| // The exception might be a JS null, but otherwise it must be valid to check |
| // if a property exists on it (VM bugs could cause errors here, specifically |
| // if a wasm exception is caught here, and it is not represented properly). |
| if (e !== null) e.a; |
| |
| // We only want to catch exceptions, not wasm traps: traps should still |
| // halt execution. Handling this requires different code in wasm2js, so |
| // check for that first (wasm2js does not define RuntimeError, so use |
| // that for the check - when wasm2js is run, we override the entire |
| // WebAssembly object with a polyfill, so we know exactly what it |
| // contains). |
| var wasm2js = !WebAssembly.RuntimeError; |
| if (!wasm2js) { |
| // When running native wasm, we can detect wasm traps. |
| if (e instanceof WebAssembly.RuntimeError) { |
| throw e; |
| } |
| } |
| var text = e + ''; |
| // We must not swallow host limitations here: a host limitation is a |
| // problem that means we must not compare the outcome here to any other |
| // VM. |
| var hostIssues = ['requested new array is too large', |
| 'out of memory', |
| 'Maximum call stack size exceeded']; |
| if (wasm2js) { |
| // When wasm2js does trap, it just throws an "abort" error. |
| hostIssues.push('abort'); |
| } |
| for (var hostIssue of hostIssues) { |
| if (text.includes(hostIssue)) { |
| throw e; |
| } |
| } |
| // Otherwise, this is a normal exception we want to catch (a wasm |
| // exception, or a conversion error on the wasm/JS boundary, etc.). Rethrow |
| // if we were asked to. |
| if (rethrow) { |
| throw e; |
| } |
| return 1; |
| } |
| } |
| |
| // Table get/set operations need a BigInt if the table has 64-bit indexes. This |
| // adds a proper cast as needed. |
| function toAddressType(table, index) { |
| // First, cast to unsigned. We do not support larger indexes anyhow. |
| index = index >>> 0; |
| if (typeof table.length == 'bigint') { |
| return BigInt(index); |
| } |
| return index; |
| } |
| |
| // Simple deterministic hashing, on an unsigned 32-bit seed. See e.g. |
| // https://www.boost.org/doc/libs/1_55_0/doc/html/hash/reference.html#boost.hash_combine |
| var hashSeed; |
| |
| function hasHashSeed() { |
| return hashSeed !== undefined; |
| } |
| |
| function hashCombine(value) { |
| // hashSeed must be set before we do anything. |
| assert(hasHashSeed()); |
| |
| hashSeed ^= value + 0x9e3779b9 + (hashSeed << 6) + (hashSeed >>> 2); |
| return hashSeed >>> 0; |
| } |
| |
| // Get a random 32-bit number. This is like hashCombine but does not take a |
| // parameter. |
| function randomBits() { |
| return hashCombine(-1); |
| } |
| |
| // Return true with probability 1 in n. E.g. oneIn(3) returns false 2/3 of the |
| // time, and true 1/3 of the time. |
| function oneIn(n) { |
| return (randomBits() % n) == 0; |
| } |
| |
| // Set up the imports. |
| var tempRet0; |
| var imports = { |
| 'fuzzing-support': { |
| // Logging. |
| 'log-i32': logValue, |
| 'log-i64': logValue, |
| 'log-f32': logValue, |
| 'log-f64': logValue, |
| // JS cannot log v128 values (we trap on the boundary), but we must still |
| // provide an import so that we do not trap during linking. (Alternatively, |
| // we could avoid running JS on code with SIMD in it, but it is useful to |
| // fuzz such code as much as we can.) |
| 'log-v128': logValue, |
| |
| // Throw an exception from JS. |
| 'throw': (which) => { |
| if (!which) { |
| // Throw a JS exception. |
| throw new Error('js exception'); |
| } else { |
| // Throw a wasm exception. |
| throw new WebAssembly.Exception(wasmTag, [which]); |
| } |
| }, |
| |
| // Table operations. |
| 'table-get': (index) => { |
| return exports.table.get(toAddressType(exports.table, index)); |
| }, |
| 'table-set': (index, value) => { |
| exports.table.set(toAddressType(exports.table, index), value); |
| }, |
| |
| // Export operations. |
| 'call-export': /* async */ (index, flags) => { |
| var rethrow = flags & 1; |
| if (JSPI) { |
| // TODO: Figure out why JSPI fails here. |
| rethrow = 0; |
| } |
| if (!rethrow) { |
| /* await */ callFunc(exportList[index].value); |
| } else { |
| tryCall(/* async */ () => /* await */ callFunc(exportList[index].value), |
| rethrow); |
| } |
| }, |
| 'call-export-catch': /* async */ (index) => { |
| return tryCall(/* async */ () => /* await */ callFunc(exportList[index].value)); |
| }, |
| |
| // Funcref operations. |
| 'call-ref': /* async */ (ref, flags) => { |
| // This is a direct function reference, and just like an export, it must |
| // be wrapped for JSPI. |
| ref = wrapExportForJSPI(ref); |
| var rethrow = flags & 1; |
| if (JSPI) { |
| // TODO: Figure out why JSPI fails here. |
| rethrow = 0; |
| } |
| if (!rethrow) { |
| /* await */ callFunc(ref); |
| } else { |
| tryCall(/* async */ () => /* await */ callFunc(ref), |
| rethrow); |
| } |
| }, |
| 'call-ref-catch': /* async */ (ref) => { |
| ref = wrapExportForJSPI(ref); |
| return tryCall(/* async */ () => /* await */ callFunc(ref)); |
| }, |
| |
| // Sleep a given amount of ms (when JSPI) and return a given id after that. |
| 'sleep': (ms, id) => { |
| // Also avoid sleeping even in JSPI mode, rarely, just to add variety |
| // here. Only do this when we have a hash seed, that is, when we are |
| // allowing randomness. |
| if (!JSPI || (hasHashSeed() && oneIn(10))) { |
| return id; |
| } |
| return new Promise((resolve, reject) => { |
| setTimeout(() => { |
| resolve(id); |
| }, 0); // TODO: Use the ms in some reasonable, deterministic manner. |
| // Rather than actually setTimeout on them we could manage |
| // a queue of pending sleeps manually, and order them based |
| // on the "ms" (which would not be literal ms, but just |
| // how many time units to wait). |
| }); |
| }, |
| |
| 'log-branch': (id, expected, actual) => { |
| console.log(`[LoggingExternalInterface log-branch ${id} ${expected} ${actual}]`); |
| }, |
| }, |
| // Emscripten support. |
| 'env': { |
| 'setTempRet0': function(x) { tempRet0 = x }, |
| 'getTempRet0': function() { return tempRet0 }, |
| }, |
| }; |
| |
| // If Tags are available, add some. |
| if (typeof WebAssembly.Tag !== 'undefined') { |
| // A tag for general use in the fuzzer. |
| var wasmTag = imports['fuzzing-support']['wasmtag'] = new WebAssembly.Tag({ |
| 'parameters': ['i32'] |
| }); |
| |
| // The JSTag that represents a JS tag. |
| imports['fuzzing-support']['jstag'] = WebAssembly.JSTag; |
| |
| // This allows j2wasm content to run in the fuzzer. |
| imports['imports'] = { |
| 'j2wasm.ExceptionUtils.tag': new WebAssembly.Tag({ |
| 'parameters': ['externref'] |
| }), |
| }; |
| } |
| |
| // If JSPI is available, wrap the imports and exports. |
| if (JSPI) { |
| for (var name of ['sleep', 'call-export', 'call-export-catch', 'call-ref', |
| 'call-ref-catch']) { |
| imports['fuzzing-support'][name] = |
| new WebAssembly.Suspending(imports['fuzzing-support'][name]); |
| } |
| } |
| |
| function wrapExportForJSPI(value) { |
| if (JSPI && typeof value === 'function') { |
| value = WebAssembly.promising(value); |
| } |
| return value; |
| } |
| |
| // If we are fuzzing a split module, a secondary binary will have imports for |
| // placeholders, whose module name defaults to 'placeholder.deferred' for the |
| // two-module split. Any import like |
| // (import "placeholder.deferred" "0" (func ..)) |
| // will be provided by the secondary module, and must be called using an |
| // indirection. |
| if (secondBinary) { |
| imports['placeholder.deferred'] = new Proxy({}, { |
| get(target, prop, receiver) { |
| // Return a function that throws. We could do an indirect call using the |
| // exported table, but as we immediately link in the secondary module, |
| // these stubs will not be called (they are written to the table, and the |
| // secondary module overwrites them). We do need to return something so |
| // the primary module links without erroring, though. |
| return () => { |
| throw 'proxy stub should not be called'; |
| } |
| } |
| }); |
| } |
| |
| // Compile and instantiate a wasm file. Receives the binary to build, and |
| // whether it is the second one. |
| function build(binary, isSecond) { |
| if (isSecond) { |
| assert(secondBinary); |
| // Provide the primary module's exports to the secondary. |
| imports['primary'] = exports; |
| } |
| |
| var module = new WebAssembly.Module(binary); |
| |
| var instance; |
| try { |
| instance = new WebAssembly.Instance(module, imports); |
| } catch (e) { |
| console.log('exception thrown: failed to instantiate module: ' + e); |
| quit(); |
| } |
| |
| // Do not add the second instance's exports to the list, as that would be |
| // noticeable by calls to call-export-*. When fuzzing wasm-split, we want the |
| // original module's exports to be provided from the primary module, and it is |
| // the only interface to the outside. |
| if (fuzzSplit && isSecond) { |
| return; |
| } |
| |
| // Update the exports. Note that this adds onto |exports| and |exportList|, |
| // which is intentional: if we build another wasm, or build this one more |
| // than once, we want to be able to call them all, so we unify all their |
| // exports. (We do trample in |exports| when keys are equal - basically this |
| // is a single global namespace - but |exportList| is appended to, so we do |
| // keep the ability to call anything that was ever exported.) |
| // |
| // Note we do not iterate on instance.exports: the order there is not |
| // necessarily the order in the wasm (which is what wasm-opt --fuzz-exec uses, |
| // for example), because of JS object iteration rules (numbers are considered |
| // "array indexes" and appear first, |
| // https://tc39.es/ecma262/#sec-ordinaryownpropertykeys). |
| for (var e of WebAssembly.Module.exports(module)) { |
| var key = e.name; |
| var value = instance.exports[key]; |
| value = wrapExportForJSPI(value); |
| exports[key] = value; |
| |
| if (fuzzSplit && key.startsWith('__fuzz_split_')) { |
| // We are fuzzing wasm-split, and this is a new export generated by |
| // wasm-split. Do not note these exports as callable from call-export*, |
| // as they do not match the original pre-split module. |
| continue; |
| } |
| exportList.push({ name: key, value: value }); |
| } |
| } |
| |
| // Run the code by calling exports. The optional |ordering| parameter indicates |
| // howe we should order the calls to the exports: if it is not provided, we call |
| // them in the natural order, which allows our output to be compared to other |
| // executions of the wasm (e.g. from wasm-opt --fuzz-exec). If |ordering| is |
| // provided, it is a random seed we use to make deterministic choices on |
| // the order of calls. |
| /* async */ function callExports(ordering) { |
| hashSeed = ordering; |
| |
| // Call the exports we were told, or if we were not given an explicit list, |
| // call them all. |
| let relevantExports = exportsToCall || exportList; |
| |
| // Build the list of call tasks to run, one for each relevant export. |
| let tasks = []; |
| for (let e of relevantExports) { |
| let name, value; |
| if (typeof e === 'string') { |
| // We are given a string name to call. Look it up in the global namespace. |
| name = e; |
| value = exports[e]; |
| } else { |
| // We are given an object form exportList, which has both a name and a |
| // value. |
| name = e.name; |
| value = e.value; |
| } |
| |
| if (typeof value !== 'function') { |
| continue; |
| } |
| |
| // A task is a name + a function to call. For an export, the function is |
| // simply a call of the export. |
| tasks.push({ name: name, func: /* async */ () => callFunc(value) }); |
| } |
| |
| // Reverse the array, so the first task is at the end, for efficient |
| // popping in the common case. |
| tasks.reverse(); |
| |
| // Execute tasks while they remain. |
| while (tasks.length) { |
| let task; |
| if (ordering === undefined) { |
| // Use the natural order. |
| task = tasks.pop(); |
| } else { |
| // Pick a random task. |
| let i = hashCombine(tasks.length) % tasks.length; |
| task = tasks.splice(i, 1)[0]; |
| } |
| |
| // Execute the task. |
| console.log(`[fuzz-exec] calling ${task.name}${task.deferred ? ' (after defer)' : ''}`); |
| let result; |
| try { |
| result = task.func(); |
| } catch (e) { |
| console.log('exception thrown: ' + e); |
| continue; |
| } |
| |
| if (JSPI) { |
| // When we are changing up the order, in JSPI we can also leave some |
| // promises unresolved until later, which lets us interleave them. Note we |
| // never defer a task more than once, and we only defer a promise (which |
| // we check for using .then). |
| // TODO: Deferring more than once may make sense, by chaining promises in |
| // JS (that would not add wasm execution in the middle, but might |
| // find JS issues in principle). We could also link promises by |
| // depending on each other, ensuring certain orders of execution. |
| if (ordering !== undefined && !task.deferred && result && |
| typeof result == 'object' && typeof result.then === 'function') { |
| if (randomBits() & 1) { |
| // Defer it for later. Reuse the existing task for simplicity. |
| console.log(`(jspi: defer ${task.name})`); |
| task.func = /* async */ () => { |
| console.log(`(jspi: finish ${task.name})`); |
| return /* await */ result; |
| }; |
| task.deferred = true; |
| tasks.push(task); |
| continue; |
| } |
| // Otherwise, continue down. |
| } |
| |
| // Await it right now. |
| try { |
| result = /* await */ result; |
| } catch (e) { |
| console.log('exception thrown: ' + e); |
| continue; |
| } |
| } |
| |
| // Log the result. |
| if (typeof result !== 'undefined') { |
| console.log('[fuzz-exec] note result: ' + task.name + ' => ' + printed(result)); |
| } |
| } |
| } |
| |
| // Build the main wasm. |
| build(binary); |
| |
| // Build the second wasm, if one was provided. |
| if (secondBinary) { |
| build(secondBinary, true); |
| } |
| |
| // Run. |
| callExports(); |