current commit
260605.3 · Fuzz with start functions (#8736)
// 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:')) {
    var payload = curr.substr('exports:'.length);
    if (payload.startsWith('[')) {
      exportsToCall = JSON.parse(payload);
    } else {
      exportsToCall = payload ? payload.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 & 0xffffffffn) | 0) + ' ' + (Number(x >> 32n) | 0)
  } else if (typeof x === 'object') {
    // This may be one of the externref imports, in which case we can print its
    // payload.
    if (Object.hasOwn(x, 'payload')) {
      return 'externref(' + x.payload + ')';
    }
    // Or maybe this is a JS error we caught.
    if (x instanceof Error) {
      return 'jserror';
    }
    // If this is a Wasm object, we can't access its type or any of its
    // internal structure, which might have been changed by optimizations
    // anyway. It might have a configured prototype, though, and that
    // prototype may be an imported externref global we can identify by the
    // payload we gave it.
    return 'object(' + printed(Object.getPrototypeOf(x)) + ')';
  } else if (typeof x === 'function') {
    // We cannot print function names because they might have been changed by
    // optimizations.
    return 'function';
  } else {
    // A number. Print the whole thing.
    assert(typeof x === 'number');
    return '' + x;
  }
}

// Fuzzer integration.
function logValue(x, y) {
  console.log('[LoggingExternalInterface logging ' + printed(x, y) + ']');
}

function logRef(ref) {
  // Look for VM bugs by using the reference in an API (note: we cannot do
  // +ref or ref+'' as those trap).
  JSON.stringify(ref);
  // If not null, try to read a property, which might exercise an
  // interesting code path.
  if (ref) {
    ref.foobar;
  }
  // Finally, log normally as with all other loggers.
  logValue(ref);
}

// 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 = {};

// The raw exports, uninstrumented by JSPI. This is necessary as we need the
// JSPI wrapping when calling from JS, but when calling from wasm, the exports
// need to be linked to imports directly, in the raw form.
var rawExports = {};

// 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);
}

// wasm2js does not define RuntimeError, so use that to check for it. wasm2js
// overrides the entire WebAssembly object with a polyfill, so we know exactly
// what it contains, and we need to handle some things differently below.
var wasm2js = !WebAssembly.RuntimeError;

// 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.
    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 baseImports = {
  '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,
    'log-anyref': logRef,
    'log-funcref': logRef,
    'log-contref': logRef,
    'log-externref': logRef,

    // 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': (actual, expected, id) => {
      console.log(`[LoggingExternalInterface log-branch ${actual} ${expected} ${id}]`);
      return 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 = baseImports['fuzzing-support']['wasmtag'] = new WebAssembly.Tag({
    'parameters': ['i32']
  });

  // The JSTag that represents a JS tag.
  baseImports['fuzzing-support']['jstag'] = WebAssembly.JSTag;

  // This allows j2wasm content to run in the fuzzer.
  baseImports['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']) {
    baseImports['fuzzing-support'][name] =
      new WebAssembly.Suspending(baseImports['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) {
  baseImports['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';
      }
    }
  });
}

function makeImports(module) {
  // Reflect on the imports to add necessary externref globals.
  if (WebAssembly.Module.imports === undefined) {
    // We must be running with wasm2js, in which case reference types must not
    // be enabled and there are no externref globals.
    return baseImports;
  }
  // Add missing imported immutable externref globals.
  // TODO: Support more kinds of imported globals, but this would require being
  // able to reflect more precisely on the global externtypes.
  for (var {module, name, kind} of WebAssembly.Module.imports(module)) {
    if (kind == 'global') {
      if (!baseImports[module]) {
        baseImports[module] = {};
      }
      if (!baseImports[module][name]) {
        // Compute a payload from the import names. This must be kept in sync
        // with execution-results.h.
        var payload = 0;
        for (var name of [module, name]) {
          for (var c of name) {
            payload = (payload + c.charCodeAt(0)) % 251;
          }
        }
        baseImports[module][name] = { payload };
      }
    }
  }
  return baseImports;
}

// Compile and instantiate a wasm file. Receives the binary to build, and
// whether it is the second one.
function build(binary, isSecond) {
  var module = new WebAssembly.Module(binary);

  var imports = makeImports(module);

  if (isSecond) {
    assert(secondBinary);
    // Provide the primary module's exports to the secondary. This is a direct
    // wasm-wasm link, so we use the raw exports.
    imports['primary'] = rawExports;
  }

  var instance;
  try {
    instance = new WebAssembly.Instance(module, imports);
  } catch (e) {
    console.log('exception thrown: failed to instantiate module:', e);
    return;
  }

  // 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];
    rawExports[key] = value;
    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 from exportList, which has both a name and a
      // value.
      name = e.name;
      value = e.value;
    }

    // Check for a global. Note we must be careful in wasm2js mode, where we
    // can't do instanceof here (the wasm polyfill there doesn't have such
    // things). In wasm2js we strip global exports to avoid needing to handle
    // them here (using stub-unsupported-js).
    if (!wasm2js && (value instanceof WebAssembly.Global)) {
      // We can log a global value and do other operations to check for bugs.
      // First, do some operations on the Global wrapper itself.
      JSON.stringify(value);
      value.foobar;

      // Log it at the right time later using a lambda. Note that we can't just
      // capture |value| for the lambda, as the loop modifies it.
      (() => {
        var global = value;
        value = () => {
          // Time to log. Look at the exported value itself, not the global
          // wrapper.
          let actualValue;
          try {
            actualValue = global.value;
          } catch (e) {
            if (e.message.startsWith('get WebAssembly.Global.value')) {
              // Just log a string instead of a value we cannot access from JS,
              // like an exnref. Note we don't need matching code on the C++
              // side in execution-results.h because illegal exports are pruned
              // anyhow if we are going to compare execution in JS to C++.
              actualValue = '<illegal value>';
            } else {
              throw e;
            }
          }
          if (typeof actualValue === 'object') {
            // logRef can do a little more than logValue, so use it when possible.
            logRef(actualValue);
          } else {
            logValue(actualValue);
          }
        };
      })();
    }

    if (typeof value !== 'function') {
      // Nothing we can call.
      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] export ${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();