Commit 320d9870 authored by Michael Achenbach's avatar Michael Achenbach Committed by Commit Bot

Open source js-fuzzer

This is a JavaScript fuzzer originally authored by Oliver Chang. It
is a mutation based fuzzer using Babel code transformations. For more
information see the included README.md.

The original code was altered:
- Add new V8 copyright headers.
- Make the test expectation generator aware of the headers.
- Fix file endings for presubmit checks.
- Fix `npm test` on fresh checkout with a new fake DB.
- Make test skipping work with new v8/tools location.
- OWNERS file.
- New title section in README.md.

No-Try: true
Bug: chromium:1109770
Change-Id: Ie71752c0a37491a50500c49060a3c526716ef933
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2320330
Commit-Queue: Michael Achenbach <machenbach@chromium.org>
Reviewed-by: 's avatarMaya Lekova <mslekova@chromium.org>
Cr-Commit-Position: refs/heads/master@{#69164}
parent 8130e54d
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
module.exports = {
"env": {
"node": true,
"commonjs": true,
"es6": true,
"mocha": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
}
};
/node_modules
/ochang_js_fuzzer*
/db/
/output.zip
/output/
/workdir/
set noparent
file:../../../INFRA_OWNERS
msarms@chromium.org
mslekova@chromium.org
ochang@chromium.org
# COMPONENT: Infra>Client>V8
# JS-Fuzzer
Javascript fuzzer for stand-alone shells like D8, Chakra, JSC or Spidermonkey.
Original author: Oliver Chang
# Building
This fuzzer may require versions of node that are newer than available on
ClusterFuzz, so we use [pkg](https://github.com/zeit/pkg) to create a self
contained binary) out of this.
## Prereqs
You need to intall nodejs and npm. Run `npm install` in this directory.
## Fuzzing DB
This fuzzer requires a fuzzing DB. To build one, get the latest web_tests.zip
from `gs://clusterfuzz-data/web_tests.zip` and run:
```bash
$ mkdir db
$ node build_db.js -i /path/to/web_tests -o db chakra v8 spidermonkey WebKit/JSTests
```
This may take a while. Optionally test the fuzzing DB with:
```bash
$ node test_db.js -i db
```
## Building fuzzer
Then, to build the fuzzer,
```bash
$ ./node_modules/.bin/pkg -t node10-linux-x64 .
```
Replace "linux" with either "win" or "macos" for those platforms.
This builds a binary named `ochang_js_fuzzer` for Linux / macOS OR
`ochang_js_fuzzer.exe` for Windows.
## Packaging
Use `./package.sh`, `./package.sh win` or `./package.sh macos` to build and
create the `output.zip` archive or use these raw commands:
```bash
$ mkdir output
$ cd output
$ ln -s ../db db
$ ln -s ../ochang_js_fuzzer run
$ zip -r /path/output.zip *
```
**NOTE**: Add `.exe` to `ochang_js_fuzzer` and `run` filename above if archiving
for Windows platform.
# Development
Run the tests with:
```bash
$ npm test
```
When test expectations change, generate them with:
```bash
$ GENERATE=1 npm test
```
# Generating exceptional configurations
Tests that fail to parse or show very bad performance can be automatically
skipped or soft-skipped with the following script (takes >1h):
```bash
$ WEB_TESTS=/path/to/web_tests OUTPUT=/path/to/output/folder ./gen_exceptions.sh
```
# Experimenting (limited to differential fuzzing)
To locally evaluate the fuzzer, setup a work directory as follows:
```bash
$ workdir/
$ workdir/app_dir
$ workdir/fuzzer
$ workdir/input
$ workdir/output
```
The `app_dir` folder can be a symlink or should contain the bundled
version of `d8` with all files required for execution.
The copy the packaged `ochang_js_fuzzer` executable and the `db` folder
to the `fuzzer` directory or use a symlink.
The `input` directory is the root folder of the corpus, i.e. pointing
to the unzipped data of `gs://clusterfuzz-data/web_tests.zip`.
The `output` directory is expected to be empty. It'll contain all
output of the fuzzing session. Start the experiments with:
```bash
$ # Around ~40000 corresponds to 24h of fuzzing on a workstation.
$ NUM_RUNS = 40000
$ python tools/workbench.py $NUM_RUNS
```
You can check current stats with:
```bash
$ cat workdir/output/stats.json | python -m json.tool
```
When failures are found, you can forge minimization command lines with:
```bash
$ MINIMIZER_PATH = path/to/minimizer
$ python tools/minimize.py $MINIMIZER_PATH
```
The path should point to a local checkout of the [minimizer](https://chrome-internal.googlesource.com/chrome/tools/clusterfuzz/+/refs/heads/master/src/python/bot/minimizer/).
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Collect JS nodes.
*/
const program = require('commander');
const corpus = require('./corpus.js');
const db = require('./db.js');
const path = require('path');
const sourceHelpers = require('./source_helpers.js');
function main() {
Error.stackTraceLimit = Infinity;
program
.version('0.0.1')
.option('-i, --input_dir <path>', 'Input directory.')
.option('-o, --output_dir <path>', 'Output directory.')
.parse(process.argv);
if (!program.args.length) {
console.log('Need to specify corpora.');
return;
}
if (!program.output_dir) {
console.log('Need to specify output dir.');
return;
}
const mutateDb = new db.MutateDbWriter(program.output_dir);
const expressions = new Set();
const inputDir = path.resolve(program.input_dir);
for (const corpusName of program.args) {
const curCorpus = new corpus.Corpus(inputDir, corpusName);
for (const relPath of curCorpus.relFiles()) {
let source;
try {
source = sourceHelpers.loadSource(inputDir, relPath);
} catch (e) {
console.log(e);
continue;
}
if (!source) {
continue;
}
try{
mutateDb.process(source, expressions);
} catch (e) {
console.log(e);
}
}
}
mutateDb.writeIndex();
}
main();
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Corpus
*/
const program = require('commander');
const fs = require('fs');
const path = require('path');
const exceptions = require('./exceptions.js');
const random = require('./random.js');
const sourceHelpers = require('./source_helpers.js');
function* walkDirectory(directory, filter) {
// Generator for recursively walk a directory.
for (const filePath of fs.readdirSync(directory)) {
const currentPath = path.join(directory, filePath);
const stat = fs.lstatSync(currentPath);
if (stat.isFile()) {
if (!filter || filter(currentPath)) {
yield currentPath;
}
continue;
}
if (stat.isDirectory()) {
for (let childFilePath of walkDirectory(currentPath, filter)) {
yield childFilePath;
}
}
}
}
class Corpus {
// Input corpus.
constructor(inputDir, corpusName, extraStrict=false) {
this.inputDir = inputDir;
this.extraStrict = extraStrict;
// Filter for permitted JS files.
function isPermittedJSFile(absPath) {
return (absPath.endsWith('.js') &&
!exceptions.isTestSkippedAbs(absPath));
}
// Cache relative paths of all files in corpus.
this.skippedFiles = [];
this.softSkippedFiles = [];
this.permittedFiles = [];
const directory = path.join(inputDir, corpusName);
for (const absPath of walkDirectory(directory, isPermittedJSFile)) {
const relPath = path.relative(this.inputDir, absPath);
if (exceptions.isTestSkippedRel(relPath)) {
this.skippedFiles.push(relPath);
} else if (exceptions.isTestSoftSkippedAbs(absPath) ||
exceptions.isTestSoftSkippedRel(relPath)) {
this.softSkippedFiles.push(relPath);
} else {
this.permittedFiles.push(relPath);
}
}
random.shuffle(this.softSkippedFiles);
random.shuffle(this.permittedFiles);
}
// Relative paths of all files in corpus.
*relFiles() {
for (const relPath of this.permittedFiles) {
yield relPath;
}
for (const relPath of this.softSkippedFiles) {
yield relPath;
}
}
// Relative paths of all files in corpus including generated skipped.
*relFilesForGenSkipped() {
for (const relPath of this.relFiles()) {
yield relPath;
}
for (const relPath of this.skippedFiles) {
yield relPath;
}
}
/**
* Returns "count" relative test paths, randomly selected from soft-skipped
* and permitted files. Permitted files have a 4 times higher chance to
* be chosen.
*/
getRandomTestcasePaths(count) {
return random.twoBucketSample(
this.softSkippedFiles, this.permittedFiles, 4, count);
}
loadTestcase(relPath, strict, label) {
const start = Date.now();
try {
const source = sourceHelpers.loadSource(this.inputDir, relPath, strict);
if (program.verbose) {
const duration = Date.now() - start;
console.log(`Parsing ${relPath} ${label} took ${duration} ms.`);
}
return source;
} catch (e) {
console.log(`WARNING: failed to ${label} parse ${relPath}`);
console.log(e);
}
return undefined;
}
*loadTestcases(relPaths) {
for (const relPath of relPaths) {
if (this.extraStrict) {
// When re-generating the files marked sloppy, we additionally test if
// the file parses in strict mode.
this.loadTestcase(relPath, true, 'strict');
}
const source = this.loadTestcase(relPath, false, 'sloppy');
if (source) {
yield source;
}
}
}
getRandomTestcases(count) {
return Array.from(this.loadTestcases(this.getRandomTestcasePaths(count)));
}
getAllTestcases() {
return this.loadTestcases(this.relFilesForGenSkipped());
}
}
module.exports = {
Corpus: Corpus,
walkDirectory: walkDirectory,
}
This diff is collapsed.
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Script mutator for differential fuzzing.
*/
'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const common = require('./mutators/common.js');
const random = require('./random.js');
const sourceHelpers = require('./source_helpers.js');
const { filterDifferentialFuzzFlags } = require('./exceptions.js');
const { DifferentialFuzzMutator, DifferentialFuzzSuppressions } = require(
'./mutators/differential_fuzz_mutator.js');
const { ScriptMutator } = require('./script_mutator.js');
const USE_ORIGINAL_FLAGS_PROB = 0.2;
/**
* Randomly chooses a configuration from experiments. The configuration
* parameters are expected to be passed from a bundled V8 build. Constraints
* mentioned below are enforced by PRESUBMIT checks on the V8 side.
*
* @param {Object[]} experiments List of tuples (probability, first config name,
* second config name, second d8 name). The probabilities are integers in
* [0,100]. We assume the sum of all probabilities is 100.
* @param {Object[]} additionalFlags List of tuples (probability, flag strings).
* Probability is in [0,1).
* @return {string[]} List of flags for v8_foozzie.py.
*/
function chooseRandomFlags(experiments, additionalFlags) {
// Add additional flags to second config based on experiment percentages.
const extra_flags = [];
for (const [p, flags] of additionalFlags) {
if (random.choose(p)) {
for (const flag of flags.split(' ')) {
extra_flags.push('--second-config-extra-flags=' + flag);
}
}
}
// Calculate flags determining the experiment.
let acc = 0;
const threshold = random.random() * 100;
for (let [prob, first_config, second_config, second_d8] of experiments) {
acc += prob;
if (acc > threshold) {
return [
'--first-config=' + first_config,
'--second-config=' + second_config,
'--second-d8=' + second_d8,
].concat(extra_flags);
}
}
// Unreachable.
assert(false);
}
function loadJSONFromBuild(name) {
assert(process.env.APP_DIR);
const fullPath = path.join(path.resolve(process.env.APP_DIR), name);
return JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
}
function hasMjsunit(dependencies) {
return dependencies.some(dep => dep.relPath.endsWith('mjsunit.js'));
}
function hasJSTests(dependencies) {
return dependencies.some(dep => dep.relPath.endsWith('jstest_stubs.js'));
}
class DifferentialScriptMutator extends ScriptMutator {
constructor(settings, db_path) {
super(settings, db_path);
// Mutators for differential fuzzing.
this.differential = [
new DifferentialFuzzSuppressions(settings),
new DifferentialFuzzMutator(settings),
];
// Flag configurations from the V8 build directory.
this.experiments = loadJSONFromBuild('v8_fuzz_experiments.json');
this.additionalFlags = loadJSONFromBuild('v8_fuzz_flags.json');
}
/**
* Performes the high-level mutation and afterwards adds flags for the
* v8_foozzie.py harness.
*/
mutateMultiple(inputs) {
const result = super.mutateMultiple(inputs);
const originalFlags = [];
// Keep original JS flags in some cases. Let the harness pass them to
// baseline _and_ comparison run.
if (random.choose(USE_ORIGINAL_FLAGS_PROB)) {
for (const flag of filterDifferentialFuzzFlags(result.flags)) {
originalFlags.push('--first-config-extra-flags=' + flag);
originalFlags.push('--second-config-extra-flags=' + flag);
}
}
// Add flags for the differnetial-fuzzing settings.
const fuzzFlags = chooseRandomFlags(this.experiments, this.additionalFlags);
result.flags = fuzzFlags.concat(originalFlags);
return result;
}
/**
* Mutatates a set of inputs.
*
* Additionally we prepare inputs by tagging each with the original source
* path for later printing. The mutated sources are post-processed by the
* differential-fuzz mutators, adding extra printing and other substitutions.
*/
mutateInputs(inputs) {
inputs.forEach(input => common.setOriginalPath(input, input.relPath));
const result = super.mutateInputs(inputs);
this.differential.forEach(mutator => mutator.mutate(result));
return result;
}
/**
* Adds extra dependencies for differential fuzzing.
*/
resolveDependencies(inputs) {
const dependencies = super.resolveDependencies(inputs);
// The suppression file neuters functions not working with differential
// fuzzing. It can also be used to temporarily silence some functionality
// leading to dupes of an active bug.
dependencies.push(
sourceHelpers.loadResource('differential_fuzz_suppressions.js'));
// Extra printing and tracking functionality.
dependencies.push(
sourceHelpers.loadResource('differential_fuzz_library.js'));
// Make Chakra tests print more.
dependencies.push(
sourceHelpers.loadResource('differential_fuzz_chakra.js'));
if (hasMjsunit(dependencies)) {
// Make V8 tests print more. We guard this as the functionality
// relies on mjsunit.js.
dependencies.push(sourceHelpers.loadResource('differential_fuzz_v8.js'));
}
if (hasJSTests(dependencies)) {
dependencies.push(
sourceHelpers.loadResource('differential_fuzz_jstest.js'));
}
return dependencies;
}
}
module.exports = {
DifferentialScriptMutator: DifferentialScriptMutator,
};
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Blacklists for fuzzer.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const random = require('./random.js');
const {generatedSloppy, generatedSoftSkipped, generatedSkipped} = require(
'./generated/exceptions.js');
const SKIPPED_FILES = [
// Disabled for unexpected test behavior, specific to d8 shell.
'd8-os.js',
'd8-readbuffer.js',
// Passes JS flags.
'd8-arguments.js',
// Slow tests or tests that are too large to be used as input.
/numops-fuzz-part.*.js/,
'regexp-pcre.js',
'unicode-test.js',
'unicodelctest.js',
'unicodelctest-no-optimization.js',
// Unsupported modules.
/^modules.*\.js/,
// Unsupported property escapes.
/^regexp-property-.*\.js/,
// Bad testcases that just loads a script that always throws errors.
'regress-444805.js',
'regress-crbug-489597.js',
'regress-crbug-620253.js',
// Just recursively loads itself.
'regress-8510.js',
];
const SKIPPED_DIRECTORIES = [
// Slow tests or tests that are too large to be used as input.
'embenchen',
'poppler',
'sqlite',
// Causes lots of failures.
'test262',
// Unavailable debug.Debug.
'v8/test/debugger',
'v8/test/inspector',
// Unsupported modules.
'v8/test/js-perf-test/Modules',
// Contains tests expected to error out on parsing.
'v8/test/message',
// Needs specific dependencies for load of various tests.
'v8/test/mjsunit/tools',
// Unsupported e4x standard.
'mozilla/data/e4x',
// Bails out fast without ReadableStream support.
'spidermonkey/non262/ReadableStream',
];
// Files used with a lower probability.
const SOFT_SKIPPED_FILES = [
// Tests with large binary content.
/^binaryen.*\.js/,
// Tests slow to parse.
// CrashTests:
/^jquery.*\.js/,
// Spidermonkey:
'regress-308085.js',
'regress-74474-002.js',
'regress-74474-003.js',
// V8:
'object-literal.js',
];
// Flags that lead to false positives or that are already passed by default.
const DISALLOWED_FLAGS = [
// Disallowed because features prefixed with "experimental" are not
// stabilized yet and would cause too much noise when enabled.
/^--experimental-.*/,
// Disallowed due to noise. We explicitly add --es-staging to job
// definitions, and all of these features are staged before launch.
/^--harmony-.*/,
// Disallowed because they are passed explicitly on the command line.
'--allow-natives-syntax',
'--debug-code',
'--es-staging',
'--wasm-staging',
'--expose-gc',
'--expose_gc',
'--icu-data-file',
'--random-seed',
// Disallowed due to false positives.
'--check-handle-count',
'--expose-debug-as',
'--expose-natives-as',
'--expose-trigger-failure',
'--mock-arraybuffer-allocator',
'natives', // Used in conjuction with --expose-natives-as.
/^--trace-path.*/,
];
// Flags only used with 25% probability.
const LOW_PROB_FLAGS_PROB = 0.25;
const LOW_PROB_FLAGS = [
// Flags that lead to slow test performance.
/^--gc-interval.*/,
/^--deopt-every-n-times.*/,
];
// Flags printing data, leading to false positives in differential fuzzing.
const DISALLOWED_DIFFERENTIAL_FUZZ_FLAGS = [
/^--gc-interval.*/,
/^--perf.*/,
/^--print.*/,
/^--stress-runs.*/,
/^--trace.*/,
'--expose-externalize-string',
'--interpreted-frames-native-stack',
'--stress-opt',
'--validate-asm',
];
const ALLOWED_RUNTIME_FUNCTIONS = new Set([
// List of allowed runtime functions. Others will be replaced with no-ops.
'ArrayBufferDetach',
'DeoptimizeFunction',
'DeoptimizeNow',
'EnableCodeLoggingForTesting',
'GetUndetectable',
'HeapObjectVerify',
'IsBeingInterpreted',
'NeverOptimizeFunction',
'OptimizeFunctionOnNextCall',
'OptimizeOsr',
'PrepareFunctionForOptimization',
'SetAllocationTimeout',
'SimulateNewspaceFull',
]);
const MAX_FILE_SIZE_BYTES = 128 * 1024; // 128KB
const MEDIUM_FILE_SIZE_BYTES = 32 * 1024; // 32KB
function _findMatch(iterable, candidate) {
for (const entry of iterable) {
if (typeof entry === 'string') {
if (entry === candidate) {
return true;
}
} else {
if (entry.test(candidate)) {
return true;
}
}
}
return false;
}
function _doesntMatch(iterable, candidate) {
return !_findMatch(iterable, candidate);
}
// Convert Windows path separators.
function normalize(testPath) {
return path.normalize(testPath).replace(/\\/g, '/');
}
function isTestSkippedAbs(absPath) {
const basename = path.basename(absPath);
if (_findMatch(SKIPPED_FILES, basename)) {
return true;
}
const normalizedTestPath = normalize(absPath);
for (const entry of SKIPPED_DIRECTORIES) {
if (normalizedTestPath.includes(entry)) {
return true;
}
}
// Avoid OOM/hangs through huge inputs.
const stat = fs.statSync(absPath);
return (stat && stat.size >= MAX_FILE_SIZE_BYTES);
}
function isTestSkippedRel(relPath) {
return generatedSkipped.has(normalize(relPath));
}
// For testing.
function getSoftSkipped() {
return SOFT_SKIPPED_FILES;
}
// For testing.
function getGeneratedSoftSkipped() {
return generatedSoftSkipped;
}
// For testing.
function getGeneratedSloppy() {
return generatedSloppy;
}
function isTestSoftSkippedAbs(absPath) {
const basename = path.basename(absPath);
if (_findMatch(this.getSoftSkipped(), basename)) {
return true;
}
// Graylist medium size files.
const stat = fs.statSync(absPath);
return (stat && stat.size >= MEDIUM_FILE_SIZE_BYTES);
}
function isTestSoftSkippedRel(relPath) {
return this.getGeneratedSoftSkipped().has(normalize(relPath));
}
function isTestSloppyRel(relPath) {
return this.getGeneratedSloppy().has(normalize(relPath));
}
function filterFlags(flags) {
return flags.filter(flag => {
return (
_doesntMatch(DISALLOWED_FLAGS, flag) &&
(_doesntMatch(LOW_PROB_FLAGS, flag) ||
random.choose(LOW_PROB_FLAGS_PROB)));
});
}
function filterDifferentialFuzzFlags(flags) {
return flags.filter(
flag => _doesntMatch(DISALLOWED_DIFFERENTIAL_FUZZ_FLAGS, flag));
}
function isAllowedRuntimeFunction(name) {
if (process.env.APP_NAME != 'd8') {
return false;
}
return ALLOWED_RUNTIME_FUNCTIONS.has(name);
}
module.exports = {
filterDifferentialFuzzFlags: filterDifferentialFuzzFlags,
filterFlags: filterFlags,
getGeneratedSoftSkipped: getGeneratedSoftSkipped,
getGeneratedSloppy: getGeneratedSloppy,
getSoftSkipped: getSoftSkipped,
isAllowedRuntimeFunction: isAllowedRuntimeFunction,
isTestSkippedAbs: isTestSkippedAbs,
isTestSkippedRel: isTestSkippedRel,
isTestSoftSkippedAbs: isTestSoftSkippedAbs,
isTestSoftSkippedRel: isTestSoftSkippedRel,
isTestSloppyRel: isTestSloppyRel,
}
#!/usr/bin/env python
# Copyright 2020 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Launcher for the foozzie differential-fuzzing harness. Wraps foozzie
with Python2 for backwards-compatibility when bisecting.
"""
import os
import re
import subprocess
import sys
if __name__ == '__main__':
# In some cases or older versions, the python executable is passed as
# first argument. Let's be robust either way, with or without full
# path or version.
if re.match(r'.*python.*', sys.argv[1]):
args = sys.argv[2:]
else:
args = sys.argv[1:]
process = subprocess.Popen(['python2'] + args)
process.communicate()
sys.exit(process.returncode)
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Generate exceptions from full corpus test report.
*/
const program = require('commander');
const assert = require('assert');
const babelGenerator = require('@babel/generator').default;
const babelTemplate = require('@babel/template').default;
const babelTypes = require('@babel/types');
const fs = require('fs');
const p = require('path');
const prettier = require("prettier");
const SPLIT_LINES_RE = /^.*([\n\r]+|$)/gm;
const PARSE_RE = /^Parsing (.*) sloppy took (\d+) ms\.\n$/;
const MUTATE_RE = /^Mutating (.*) took (\d+) ms\.\n$/;
const PARSE_FAILED_RE = /^WARNING: failed to sloppy parse (.*)\n$/;
const PARSE_STRICT_FAILED_RE = /^WARNING: failed to strict parse (.*)\n$/;
const MUTATE_FAILED_RE = /^ERROR: Exception during mutate: (.*)\n$/;
// Add tests matching error regexp to result array.
function matchError(regexp, line, resultArray){
const match = line.match(regexp);
if (!match) return false;
const relPath = match[1];
assert(relPath);
resultArray.push(relPath);
return true;
}
// Sum up total duration of tests matching the duration regexp and
// map test -> duration in result map.
function matchDuration(regexp, line, resultMap){
const match = line.match(regexp);
if (!match) return false;
const relPath = match[1];
assert(relPath);
resultMap[relPath] = (resultMap[relPath] || 0) + parseInt(match[2]);
return true;
}
// Create lists of failed and slow tests from stdout of a fuzzer run.
function processFuzzOutput(outputFile){
const text = fs.readFileSync(outputFile, 'utf-8');
const lines = text.match(SPLIT_LINES_RE);
const failedParse = [];
const failedParseStrict = [];
const failedMutate = [];
const durationsMap = {};
for (const line of lines) {
if (matchError(PARSE_FAILED_RE, line, failedParse))
continue;
if (matchError(PARSE_STRICT_FAILED_RE, line, failedParseStrict))
continue;
if (matchError(MUTATE_FAILED_RE, line, failedMutate))
continue;
if (matchDuration(PARSE_RE, line, durationsMap))
continue;
if (matchDuration(MUTATE_RE, line, durationsMap))
continue;
}
// Tuples (absPath, duration).
const total = Object.entries(durationsMap);
// Tuples (absPath, duration) with 2s < duration <= 10s.
const slow = total.filter(t => t[1] > 2000 && t[1] <= 10000);
// Tuples (absPath, duration) with 10s < duration.
const verySlow = total.filter(t => t[1] > 10000);
// Assert there's nothing horribly wrong with the results.
// We have at least 2500 tests in the output.
assert(total.length > 2500);
// No more than 5% parse/mutation errors.
assert(failedParse.length + failedMutate.length < total.length / 20);
// No more than 10% slow tests
assert(slow.length < total.length / 10);
// No more than 2% very slow tests.
assert(verySlow.length < total.length / 50);
// Sort everything.
failedParse.sort();
failedParseStrict.sort();
failedMutate.sort();
function slowestFirst(a, b) {
return b[1] - a[1];
}
slow.sort(slowestFirst);
verySlow.sort(slowestFirst);
return [failedParse, failedParseStrict, failedMutate, slow, verySlow];
}
// List of string literals of failed tests.
function getLiteralsForFailed(leadingComment, failedList) {
const result = failedList.map(path => babelTypes.stringLiteral(path));
if (result.length) {
babelTypes.addComment(result[0], 'leading', leadingComment);
}
return result;
}
// List of string literals of slow tests with duration comments.
function getLiteralsForSlow(leadingComment, slowList) {
const result = slowList.map(([path, duration]) => {
const literal = babelTypes.stringLiteral(path);
babelTypes.addComment(
literal, 'trailing', ` ${duration / 1000}s`, true);
return literal;
});
if (result.length) {
babelTypes.addComment(result[0], 'leading', leadingComment);
}
return result;
}
function main() {
program
.version('0.0.1')
.parse(process.argv);
if (!program.args.length) {
console.log('Need to specify stdout reports of fuzz runs.');
return;
}
let skipped = [];
let softSkipped = [];
let sloppy = [];
for (const outputFile of program.args) {
const [failedParse, failedParseStrict, failedMutate, slow, verySlow] = (
processFuzzOutput(outputFile));
const name = p.basename(outputFile, p.extname(outputFile));
// Skip tests that fail to parse/mutate or are very slow.
skipped = skipped.concat(getLiteralsForFailed(
` Tests with parse errors from ${name} `, failedParse));
skipped = skipped.concat(getLiteralsForFailed(
` Tests with mutation errors from ${name} `, failedMutate));
skipped = skipped.concat(getLiteralsForSlow(
` Very slow tests from ${name} `, verySlow));
// Soft-skip slow but not very slow tests.
softSkipped = softSkipped.concat(getLiteralsForSlow(
` Slow tests from ${name} `, slow));
// Mark sloppy tests.
sloppy = sloppy.concat(getLiteralsForFailed(
` Tests requiring sloppy mode from ${name} `, failedParseStrict));
}
const fileTemplate = babelTemplate(`
/**
* @fileoverview Autogenerated exceptions. Created with gen_exceptions.js.
*/
'use strict';
const skipped = SKIPPED;
const softSkipped = SOFTSKIPPED;
const sloppy = SLOPPY;
module.exports = {
generatedSkipped: new Set(skipped),
generatedSoftSkipped: new Set(softSkipped),
generatedSloppy: new Set(sloppy),
}
`, {preserveComments: true});
const skippedArray = babelTypes.arrayExpression(skipped);
const softSkippedArray = babelTypes.arrayExpression(softSkipped);
const sloppyArray = babelTypes.arrayExpression(sloppy);
const statements = fileTemplate({
SKIPPED: skippedArray,
SOFTSKIPPED: softSkippedArray,
SLOPPY: sloppyArray,
});
const resultProgram = babelTypes.program(statements);
const code = babelGenerator(resultProgram, { comments: true }).code;
const prettyCode = prettier.format(code, { parser: "babel" });
fs.writeFileSync('generated/exceptions.js', prettyCode);
}
main();
#!/bin/bash
# Copyright 2020 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
APP_NAME=d8 node run.js -i $WEB_TESTS -o $OUTPUT -z -v -e -c chakra > chakra.log
APP_NAME=d8 node run.js -i $WEB_TESTS -o $OUTPUT -z -v -e -c v8 > v8.log
APP_NAME=d8 node run.js -i $WEB_TESTS -o $OUTPUT -z -v -e -c spidermonkey > spidermonkey.log
APP_NAME=d8 node run.js -i $WEB_TESTS -o $OUTPUT -z -v -e -c WebKit/JSTests > jstests.log
APP_NAME=d8 node run.js -i $WEB_TESTS -o $OUTPUT -z -v -e -c CrashTests > crashtests.log
node gen_exceptions.js v8.log spidermonkey.log chakra.log jstests.log crashtests.log
This diff is collapsed.
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Mutator for array expressions.
*/
'use strict';
const babelTypes = require('@babel/types');
const common = require('./common.js');
const mutator = require('./mutator.js');
const random = require('../random.js');
// Blueprint for choosing the maximum number of mutations. Bias towards
// performing only one mutation.
const MUTATION_CHOICES = [1, 1, 1, 1, 1, 2, 2, 2, 3];
const MAX_ARRAY_LENGTH = 50;
class ArrayMutator extends mutator.Mutator {
constructor(settings) {
super();
this.settings = settings;
}
get visitor() {
const thisMutator = this;
return {
ArrayExpression(path) {
const elements = path.node.elements;
if (!random.choose(thisMutator.settings.MUTATE_ARRAYS) ||
elements.length > MAX_ARRAY_LENGTH) {
return;
}
// Annotate array expression with the action taken, indicating
// if we also replaced elements.
function annotate(message, replace) {
if (replace) message += ' (replaced)';
thisMutator.annotate(path.node, message);
}
// Add or replace elements at a random index.
function randomSplice(replace, ...args) {
// Choose an index that's small enough to replace all desired items.
const index = random.randInt(0, elements.length - replace);
elements.splice(index, replace, ...args);
}
function duplicateElement(replace) {
const element = random.single(elements);
if (!element || common.isLargeNode(element)) {
return;
}
annotate('Duplicate an element', replace);
randomSplice(replace, babelTypes.cloneDeep(element));
}
function insertRandomValue(replace) {
annotate('Insert a random value', replace);
randomSplice(replace, common.randomValue(path));
}
function insertHole(replace) {
annotate('Insert a hole', replace);
randomSplice(replace, null);
}
function removeElements(count) {
annotate('Remove elements');
randomSplice(random.randInt(1, count));
}
function shuffle() {
annotate('Shuffle array');
random.shuffle(elements);
}
// Mutation options. Repeated mutations have a higher probability.
const mutations = [
() => duplicateElement(1),
() => duplicateElement(1),
() => duplicateElement(1),
() => duplicateElement(0),
() => duplicateElement(0),
() => insertRandomValue(1),
() => insertRandomValue(1),
() => insertRandomValue(0),
() => insertHole(1),
() => insertHole(0),
() => removeElements(1),
() => removeElements(elements.length),
shuffle,
];
// Perform several mutations.
const count = random.single(MUTATION_CHOICES);
for (let i = 0; i < count; i++) {
random.single(mutations)();
}
// Don't recurse on nested arrays.
path.skip();
},
}
}
}
module.exports = {
ArrayMutator: ArrayMutator,
};
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Common mutator utilities.
*/
const babelTemplate = require('@babel/template').default;
const babelTypes = require('@babel/types');
const babylon = require('@babel/parser');
const sourceHelpers = require('../source_helpers.js');
const random = require('../random.js');
const INTERESTING_NUMBER_VALUES = [
-1, -0.0, 0, 1,
// Float values.
-0.000000000000001, 0.000000000000001,
// Special values.
NaN, +Infinity, -Infinity,
// Boundaries of int, signed, unsigned, SMI (near +/- 2^(30, 31, 32).
0x03fffffff, 0x040000000, 0x040000001,
-0x03fffffff, -0x040000000, -0x040000001,
0x07fffffff, 0x080000000, 0x080000001,
-0x07fffffff, -0x080000000, -0x080000001,
0x0ffffffff, 0x100000000, 0x100000001,
-0x0ffffffff, -0x100000000, -0x100000001,
// Boundaries of maximum safe integer (near +/- 2^53).
9007199254740990, 9007199254740991, 9007199254740992,
-9007199254740990, -9007199254740991, -9007199254740992,
// Boundaries of double.
5e-324, 1.7976931348623157e+308,
-5e-324,-1.7976931348623157e+308,
]
const INTERESTING_NON_NUMBER_VALUES = [
// Simple arrays.
'[]',
'Array(0x8000).fill("a")',
// Simple object.
'{}',
'{a: "foo", b: 10, c: {}}',
// Simple strings.
'"foo"',
'""',
// Simple regex.
'/0/',
'"/0/"',
// Simple symbol.
'Symbol("foo")',
// Long string.
'Array(0x8000).join("a")',
// Math.PI
'Math.PI',
// Others.
'false',
'true',
'undefined',
'null',
'this',
'this[0]',
'this[1]',
// Empty function.
'(function() {return 0;})',
// Objects with functions.
'({toString:function(){return "0";}})',
'({valueOf:function(){return 0;}})',
'({valueOf:function(){return "0";}})',
// Objects for primitive types created using new.
'(new Boolean(false))',
'(new Boolean(true))',
'(new String(""))',
'(new Number(0))',
'(new Number(-0))',
]
const LARGE_NODE_SIZE = 100;
const MAX_ARGUMENT_COUNT = 10;
function _identifier(identifier) {
return babelTypes.identifier(identifier);
}
function _numericLiteral(number) {
return babelTypes.numericLiteral(number);
}
function _unwrapExpressionStatement(value) {
if (babelTypes.isExpressionStatement(value)) {
return value.expression;
}
return value;
}
function isVariableIdentifier(name) {
return /__v_[0-9]+/.test(name);
}
function isFunctionIdentifier(name) {
return /__f_[0-9]+/.test(name);
}
function isInForLoopCondition(path) {
// Return whether if we're in the init/test/update parts of a for loop (but
// not the body). Mutating variables in the init/test/update will likely
// modify loop variables and cause infinite loops.
const forStatementChild = path.find(
p => p.parent && babelTypes.isForStatement(p.parent));
return (forStatementChild && forStatementChild.parentKey !== 'body');
}
function isInWhileLoop(path) {
// Return whether if we're in a while loop.
const whileStatement = path.find(p => babelTypes.isWhileStatement(p));
return Boolean(whileStatement);
}
function _availableIdentifiers(path, filter) {
// TODO(ochang): Consider globals that aren't declared with let/var etc.
const available = new Array();
const allBindings = path.scope.getAllBindings();
for (const key of Object.keys(allBindings)) {
if (!filter(key)) {
continue;
}
if (filter === isVariableIdentifier &&
path.willIMaybeExecuteBefore(allBindings[key].path)) {
continue;
}
available.push(_identifier(key));
}
return available;
}
function availableVariables(path) {
return _availableIdentifiers(path, isVariableIdentifier);
}
function availableFunctions(path) {
return _availableIdentifiers(path, isFunctionIdentifier);
}
function randomVariable(path) {
return random.single(availableVariables(path));
}
function randomFunction(path) {
return random.single(availableFunctions(path));
}
function randomSeed() {
return random.randInt(0, 2**20);
}
function randomObject(seed) {
if (seed === undefined) {
seed = randomSeed();
}
const template = babelTemplate('__getRandomObject(SEED)');
return template({
SEED: _numericLiteral(seed),
}).expression;
}
function randomProperty(identifier, seed) {
if (seed === undefined) {
seed = randomSeed();
}
const template = babelTemplate('__getRandomProperty(IDENTIFIER, SEED)');
return template({
IDENTIFIER: identifier,
SEED: _numericLiteral(seed),
}).expression;
}
function randomArguments(path) {
const numArgs = random.randInt(0, MAX_ARGUMENT_COUNT);
const args = [];
for (let i = 0; i < numArgs; i++) {
args.push(randomValue(path));
}
return args.map(_unwrapExpressionStatement);
}
function randomValue(path) {
const probability = random.random();
if (probability < 0.01) {
const randomFunc = randomFunction(path);
if (randomFunc) {
return randomFunc;
}
}
if (probability < 0.25) {
const randomVar = randomVariable(path);
if (randomVar) {
return randomVar;
}
}
if (probability < 0.5) {
return randomInterestingNumber();
}
if (probability < 0.75) {
return randomInterestingNonNumber();
}
return randomObject();
}
function callRandomFunction(path, identifier, seed) {
if (seed === undefined) {
seed = randomSeed();
}
let args = [
identifier,
_numericLiteral(seed)
];
args = args.map(_unwrapExpressionStatement);
args = args.concat(randomArguments(path));
return babelTypes.callExpression(
babelTypes.identifier('__callRandomFunction'),
args);
}
function nearbyRandomNumber(value) {
const probability = random.random();
if (probability < 0.9) {
return _numericLiteral(value + random.randInt(-0x10, 0x10));
} else if (probability < 0.95) {
return _numericLiteral(value + random.randInt(-0x100, 0x100));
} else if (probability < 0.99) {
return _numericLiteral(value + random.randInt(-0x1000, 0x1000));
}
return _numericLiteral(value + random.randInt(-0x10000, 0x10000));
}
function randomInterestingNumber() {
const value = random.single(INTERESTING_NUMBER_VALUES);
if (random.choose(0.05)) {
return nearbyRandomNumber(value);
}
return _numericLiteral(value);
}
function randomInterestingNonNumber() {
return babylon.parseExpression(random.single(INTERESTING_NON_NUMBER_VALUES));
}
function concatFlags(inputs) {
const flags = new Set();
for (const input of inputs) {
for (const flag of input.flags || []) {
flags.add(flag);
}
}
return Array.from(flags.values());
}
function concatPrograms(inputs) {
// Concatentate programs.
const resultProgram = babelTypes.program([]);
const result = babelTypes.file(resultProgram, [], null);
for (const input of inputs) {
const ast = input.ast.program;
resultProgram.body = resultProgram.body.concat(ast.body);
resultProgram.directives = resultProgram.directives.concat(ast.directives);
}
// TODO(machenbach): Concat dependencies here as soon as they are cached.
const combined = new sourceHelpers.ParsedSource(
result, '', '', concatFlags(inputs));
// If any input file is sloppy, the combined result is sloppy.
combined.sloppy = inputs.some(input => input.isSloppy());
return combined;
}
function setSourceLoc(source, index, total) {
const noop = babelTypes.noop();
noop.__loc = index / total;
noop.__self = noop;
source.ast.program.body.unshift(noop);
}
function getSourceLoc(node) {
// Source location is invalid in cloned nodes.
if (node !== node.__self) {
return undefined;
}
return node.__loc;
}
function setOriginalPath(source, originalPath) {
const noop = babelTypes.noop();
noop.__path = originalPath;
noop.__self = noop;
source.ast.program.body.unshift(noop);
}
function getOriginalPath(node) {
// Original path is invalid in cloned nodes.
if (node !== node.__self) {
return undefined;
}
return node.__path;
}
// Estimate the size of a node in raw source characters.
function isLargeNode(node) {
// Ignore array holes inserted by us (null) or previously cloned nodes
// (they have no start/end).
if (!node || node.start === undefined || node.end === undefined ) {
return false;
}
return node.end - node.start > LARGE_NODE_SIZE;
}
module.exports = {
callRandomFunction: callRandomFunction,
concatFlags: concatFlags,
concatPrograms: concatPrograms,
availableVariables: availableVariables,
availableFunctions: availableFunctions,
randomFunction: randomFunction,
randomVariable: randomVariable,
isInForLoopCondition: isInForLoopCondition,
isInWhileLoop: isInWhileLoop,
isLargeNode: isLargeNode,
isVariableIdentifier: isVariableIdentifier,
isFunctionIdentifier: isFunctionIdentifier,
nearbyRandomNumber: nearbyRandomNumber,
randomArguments: randomArguments,
randomInterestingNonNumber: randomInterestingNonNumber,
randomInterestingNumber: randomInterestingNumber,
randomObject: randomObject,
randomProperty: randomProperty,
randomSeed: randomSeed,
randomValue: randomValue,
getOriginalPath: getOriginalPath,
setOriginalPath: setOriginalPath,
getSourceLoc: getSourceLoc,
setSourceLoc: setSourceLoc,
}
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Expression mutator.
*/
'use strict';
const babelTemplate = require('@babel/template').default;
const common = require('./common.js');
const random = require('../random.js');
const mutator = require('./mutator.js');
const sourceHelpers = require('../source_helpers.js');
class CrossOverMutator extends mutator.Mutator {
constructor(settings, db) {
super();
this.settings = settings;
this.db = db;
}
get visitor() {
const thisMutator = this;
return [{
ExpressionStatement(path) {
if (!random.choose(thisMutator.settings.MUTATE_CROSSOVER_INSERT)) {
return;
}
const canHaveSuper = Boolean(path.findParent(x => x.isClassMethod()));
const randomExpression = thisMutator.db.getRandomStatement(
{canHaveSuper: canHaveSuper});
// Insert the statement.
var templateOptions = Object.assign({}, sourceHelpers.BABYLON_OPTIONS);
templateOptions['placeholderPattern'] = /^VAR_[0-9]+$/;
let toInsert = babelTemplate(
randomExpression.source,
templateOptions);
const dependencies = {};
if (randomExpression.dependencies) {
const variables = common.availableVariables(path);
if (!variables.length) {
return;
}
for (const dependency of randomExpression.dependencies) {
dependencies[dependency] = random.single(variables);
}
}
try {
toInsert = toInsert(dependencies);
} catch (e) {
if (thisMutator.settings.testing) {
// Fail early in tests.
throw e;
}
console.log('ERROR: Failed to parse:', randomExpression.source);
console.log(e);
return;
}
thisMutator.annotate(
toInsert,
'Crossover from ' + randomExpression.originalPath);
if (random.choose(0.5)) {
thisMutator.insertBeforeSkip(path, toInsert);
} else {
thisMutator.insertAfterSkip(path, toInsert);
}
path.skip();
},
}, {
}];
}
}
module.exports = {
CrossOverMutator: CrossOverMutator,
};
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Mutator for differential fuzzing.
*/
'use strict';
const babelTemplate = require('@babel/template').default;
const babelTypes = require('@babel/types');
const common = require('./common.js');
const mutator = require('./mutator.js');
const random = require('../random.js');
// Templates for various statements.
const incCaught = babelTemplate('__caught++;');
const printValue = babelTemplate('print(VALUE);');
const printCaught = babelTemplate('print("Caught: " + __caught);');
const printHash = babelTemplate('print("Hash: " + __hash);');
const prettyPrint = babelTemplate('__prettyPrint(ID);');
const prettyPrintExtra = babelTemplate('__prettyPrintExtra(ID);');
// This section prefix is expected by v8_foozzie.py. Existing prefixes
// (e.g. from CrashTests) are cleaned up with CLEANED_PREFIX.
const SECTION_PREFIX = 'v8-foozzie source: ';
const CLEANED_PREFIX = 'v***************e: ';
/**
* Babel statement for calling deep printing from the fuzz library.
*/
function prettyPrintStatement(variable) {
return prettyPrint({ ID: babelTypes.cloneDeep(variable) });
}
/**
* As above, but using the "extra" variant, which will reduce printing
* after too many calls to prevent I/O flooding.
*/
function prettyPrintExtraStatement(variable) {
return prettyPrintExtra({ ID: babelTypes.cloneDeep(variable) });
}
/**
* Mutator for suppressing known and/or unfixable issues.
*/
class DifferentialFuzzSuppressions extends mutator.Mutator {
get visitor() {
let thisMutator = this;
return {
// Clean up strings containing the magic section prefix. Those can come
// e.g. from CrashTests and would confuse the deduplication in
// v8_foozzie.py.
StringLiteral(path) {
if (path.node.value.startsWith(SECTION_PREFIX)) {
const postfix = path.node.value.substring(SECTION_PREFIX.length);
path.node.value = CLEANED_PREFIX + postfix;
thisMutator.annotate(path.node, 'Replaced magic string');
}
},
// Known precision differences: https://crbug.com/1063568
BinaryExpression(path) {
if (path.node.operator == '**') {
path.node.operator = '+';
thisMutator.annotate(path.node, 'Replaced **');
}
},
// Unsupported language feature: https://crbug.com/1020573
MemberExpression(path) {
if (path.node.property.name == "arguments") {
let replacement = common.randomVariable(path);
if (!replacement) {
replacement = babelTypes.thisExpression();
}
thisMutator.annotate(replacement, 'Replaced .arguments');
thisMutator.replaceWithSkip(path, replacement);
}
},
};
}
}
/**
* Mutator for tracking original input files and for extra printing.
*/
class DifferentialFuzzMutator extends mutator.Mutator {
constructor(settings) {
super();
this.settings = settings;
}
/**
* Looks for the dummy node that marks the beginning of an input file
* from the corpus.
*/
isSectionStart(path) {
return !!common.getOriginalPath(path.node);
}
/**
* Create print statements for printing the magic section prefix that's
* expected by v8_foozzie.py to differentiate different source files.
*/
getSectionHeader(path) {
const orig = common.getOriginalPath(path.node);
return printValue({
VALUE: babelTypes.stringLiteral(SECTION_PREFIX + orig),
});
}
/**
* Create statements for extra printing at the end of a section. We print
* the number of caught exceptions, a generic hash of all observed values
* and the contents of all variables in scope.
*/
getSectionFooter(path) {
const variables = common.availableVariables(path);
const statements = variables.map(prettyPrintStatement);
statements.unshift(printCaught());
statements.unshift(printHash());
const statement = babelTypes.tryStatement(
babelTypes.blockStatement(statements),
babelTypes.catchClause(
babelTypes.identifier('e'),
babelTypes.blockStatement([])));
this.annotate(statement, 'Print variables and exceptions from section');
return statement;
}
/**
* Helper for printing the contents of several variables.
*/
printVariables(path, nodes) {
const statements = [];
for (const node of nodes) {
if (!babelTypes.isIdentifier(node) ||
!common.isVariableIdentifier(node.name))
continue;
statements.push(prettyPrintExtraStatement(node));
}
if (statements.length) {
this.annotate(statements[0], 'Extra variable printing');
this.insertAfterSkip(path, statements);
}
}
get visitor() {
const thisMutator = this;
const settings = this.settings;
return {
// Replace existing normal print statements with deep printing.
CallExpression(path) {
if (babelTypes.isIdentifier(path.node.callee) &&
path.node.callee.name == 'print') {
path.node.callee = babelTypes.identifier('__prettyPrintExtra');
thisMutator.annotate(path.node, 'Pretty printing');
}
},
// Either print or track caught exceptions, guarded by a probability.
CatchClause(path) {
const probability = random.random();
if (probability < settings.DIFF_FUZZ_EXTRA_PRINT &&
path.node.param &&
babelTypes.isIdentifier(path.node.param)) {
const statement = prettyPrintExtraStatement(path.node.param);
path.node.body.body.unshift(statement);
} else if (probability < settings.DIFF_FUZZ_TRACK_CAUGHT) {
path.node.body.body.unshift(incCaught());
}
},
// Insert section headers and footers between the contents of two
// original source files. We detect the dummy no-op nodes that were
// previously tagged with the original path of the file.
Noop(path) {
if (!thisMutator.isSectionStart(path)) {
return;
}
const header = thisMutator.getSectionHeader(path);
const footer = thisMutator.getSectionFooter(path);
thisMutator.insertBeforeSkip(path, footer);
thisMutator.insertBeforeSkip(path, header);
},
// Additionally we print one footer in the end.
Program: {
exit(path) {
const footer = thisMutator.getSectionFooter(path);
path.node.body.push(footer);
},
},
// Print contents of variables after assignments, guarded by a
// probability.
ExpressionStatement(path) {
if (!babelTypes.isAssignmentExpression(path.node.expression) ||
!random.choose(settings.DIFF_FUZZ_EXTRA_PRINT)) {
return;
}
const left = path.node.expression.left;
if (babelTypes.isMemberExpression(left)) {
thisMutator.printVariables(path, [left.object]);
} else {
thisMutator.printVariables(path, [left]);
}
},
// Print contents of variables after declaration, guarded by a
// probability.
VariableDeclaration(path) {
if (babelTypes.isLoop(path.parent) ||
!random.choose(settings.DIFF_FUZZ_EXTRA_PRINT)) {
return;
}
const identifiers = path.node.declarations.map(decl => decl.id);
thisMutator.printVariables(path, identifiers);
},
};
}
}
module.exports = {
DifferentialFuzzMutator: DifferentialFuzzMutator,
DifferentialFuzzSuppressions: DifferentialFuzzSuppressions,
};
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Expression mutator.
*/
'use strict';
const babelTypes = require('@babel/types');
const random = require('../random.js');
const mutator = require('./mutator.js');
class ExpressionMutator extends mutator.Mutator {
constructor(settings) {
super();
this.settings = settings;
}
get visitor() {
const thisMutator = this;
return {
ExpressionStatement(path) {
if (!random.choose(thisMutator.settings.MUTATE_EXPRESSIONS)) {
return;
}
const probability = random.random();
if (probability < 0.7) {
const repeated = babelTypes.cloneDeep(path.node);
thisMutator.annotate(repeated, 'Repeated');
thisMutator.insertBeforeSkip(path, repeated);
} else if (path.key > 0) {
// Get a random previous sibling.
const prev = path.getSibling(random.randInt(0, path.key - 1));
if (!prev || !prev.node) {
return;
}
// Either select a previous or the current node to clone.
const [selected, destination] = random.shuffle([prev, path]);
if (selected.isDeclaration()) {
return;
}
const cloned = babelTypes.cloneDeep(selected.node);
thisMutator.annotate(cloned, 'Cloned sibling');
if (random.choose(0.5)) {
thisMutator.insertBeforeSkip(destination, cloned);
} else {
thisMutator.insertAfterSkip(destination, cloned);
}
}
},
};
}
}
module.exports = {
ExpressionMutator: ExpressionMutator,
};
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Function calls mutator.
*/
'use strict';
const babelTemplate = require('@babel/template').default;
const babelTypes = require('@babel/types');
const common = require('./common.js');
const random = require('../random.js');
const mutator = require('./mutator.js');
function _liftExpressionsToStatements(path, nodes) {
// If the node we're replacing is an expression in an expression statement,
// lift the replacement nodes into statements too.
if (!babelTypes.isExpressionStatement(path.parent)) {
return nodes;
}
return nodes.map(n => babelTypes.expressionStatement(n));
}
class FunctionCallMutator extends mutator.Mutator {
constructor(settings) {
super();
this.settings = settings;
}
get visitor() {
const thisMutator = this;
return {
CallExpression(path) {
if (!babelTypes.isIdentifier(path.node.callee)) {
return;
}
if (!common.isFunctionIdentifier(path.node.callee.name)) {
return;
}
if (!random.choose(thisMutator.settings.MUTATE_FUNCTION_CALLS)) {
return;
}
const probability = random.random();
if (probability < 0.5) {
const randFunc = common.randomFunction(path);
if (randFunc) {
thisMutator.annotate(
path.node,
`Replaced ${path.node.callee.name} with ${randFunc.name}`);
path.node.callee = randFunc;
}
} else if (probability < 0.7 && thisMutator.settings.engine == 'V8') {
const prepareTemplate = babelTemplate(
'__V8BuiltinPrepareFunctionForOptimization(ID)');
const optimizeTemplate = babelTemplate(
'__V8BuiltinOptimizeFunctionOnNextCall(ID)');
const nodes = [
prepareTemplate({
ID: babelTypes.cloneDeep(path.node.callee),
}).expression,
babelTypes.cloneDeep(path.node),
babelTypes.cloneDeep(path.node),
optimizeTemplate({
ID: babelTypes.cloneDeep(path.node.callee),
}).expression,
];
thisMutator.annotate(
path.node,
`Optimizing ${path.node.callee.name}`);
if (!babelTypes.isExpressionStatement(path.parent)) {
nodes.push(path.node);
thisMutator.replaceWithSkip(
path, babelTypes.sequenceExpression(nodes));
} else {
thisMutator.insertBeforeSkip(
path, _liftExpressionsToStatements(path, nodes));
}
} else if (probability < 0.85 &&
thisMutator.settings.engine == 'V8') {
const template = babelTemplate(
'__V8BuiltinDeoptimizeFunction(ID)');
const insert = _liftExpressionsToStatements(path, [
template({
ID: babelTypes.cloneDeep(path.node.callee),
}).expression,
]);
thisMutator.annotate(
path.node,
`Deoptimizing ${path.node.callee.name}`);
thisMutator.insertAfterSkip(path, insert);
} else {
const template = babelTemplate(
'runNearStackLimit(() => { return CALL });');
thisMutator.annotate(
path.node,
`Run to stack limit ${path.node.callee.name}`);
thisMutator.replaceWithSkip(
path,
template({
CALL: path.node,
}).expression);
}
path.skip();
},
}
}
}
module.exports = {
FunctionCallMutator: FunctionCallMutator,
};
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Mutator
*/
'use strict';
const babelTraverse = require('@babel/traverse').default;
const babelTypes = require('@babel/types');
class Mutator {
get visitor() {
return null;
}
_traverse(ast, visitor) {
let oldEnter = null;
if (Object.prototype.hasOwnProperty.call(visitor, 'enter')) {
oldEnter = visitor['enter'];
}
// Transparently skip nodes that are marked.
visitor['enter'] = (path) => {
if (this.shouldSkip(path.node)) {
path.skip();
return;
}
if (oldEnter) {
oldEnter(path);
}
}
babelTraverse(ast, visitor);
}
mutate(source) {
if (Array.isArray(this.visitor)) {
for (const visitor of this.visitor) {
this._traverse(source.ast, visitor);
}
} else {
this._traverse(source.ast, this.visitor);
}
}
get _skipPropertyName() {
return '__skip' + this.constructor.name;
}
shouldSkip(node) {
return Boolean(node[this._skipPropertyName]);
}
skipMutations(node) {
// Mark a node to skip further mutations of the same kind.
if (Array.isArray(node)) {
for (const item of node) {
item[this._skipPropertyName] = true;
}
} else {
node[this._skipPropertyName] = true;
}
return node;
}
insertBeforeSkip(path, node) {
this.skipMutations(node);
path.insertBefore(node);
}
insertAfterSkip(path, node) {
this.skipMutations(node);
path.insertAfter(node);
}
replaceWithSkip(path, node) {
this.skipMutations(node);
path.replaceWith(node);
}
replaceWithMultipleSkip(path, node) {
this.skipMutations(node);
path.replaceWithMultiple(node);
}
annotate(node, message) {
babelTypes.addComment(
node, 'leading', ` ${this.constructor.name}: ${message} `);
}
}
module.exports = {
Mutator: Mutator,
}
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Normalizer.
* This renames variables so that we don't have collisions when combining
* different files. It also simplifies other logic when e.g. determining the
* type of an identifier.
*/
'use strict';
const babelTypes = require('@babel/types');
const mutator = require('./mutator.js');
class NormalizerContext {
constructor() {
this.funcIndex = 0;
this.varIndex = 0;
this.classIndex = 0;
}
}
class IdentifierNormalizer extends mutator.Mutator {
constructor() {
super();
this.context = new NormalizerContext();
}
get visitor() {
const context = this.context;
const renamed = new WeakSet();
const globalMappings = new Map();
return [{
Scope(path) {
for (const [name, binding] of Object.entries(path.scope.bindings)) {
if (renamed.has(binding.identifier)) {
continue;
}
renamed.add(binding.identifier);
if (babelTypes.isClassDeclaration(binding.path.node) ||
babelTypes.isClassExpression(binding.path.node)) {
path.scope.rename(name, '__c_' + context.classIndex++);
} else if (babelTypes.isFunctionDeclaration(binding.path.node) ||
babelTypes.isFunctionExpression(binding.path.node)) {
path.scope.rename(name, '__f_' + context.funcIndex++);
} else {
path.scope.rename(name, '__v_' + context.varIndex++);
}
}
},
AssignmentExpression(path) {
// Find assignments for which we have no binding in the scope. We assume
// that these are globals which are local to our script (which weren't
// declared with var/let/const etc).
const ids = path.getBindingIdentifiers();
for (const name in ids) {
if (!path.scope.getBinding(name)) {
globalMappings.set(name, '__v_' + context.varIndex++);
}
}
}
}, {
// Second pass to rename globals that weren't declared with
// var/let/const etc.
Identifier(path) {
if (!globalMappings.has(path.node.name)) {
return;
}
if (path.scope.getBinding(path.node.name)) {
// Don't rename if there is a binding that hides the global.
return;
}
path.node.name = globalMappings.get(path.node.name);
}
}];
}
}
module.exports = {
IdentifierNormalizer: IdentifierNormalizer,
};
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Numbers mutator.
*/
'use strict';
const babelTypes = require('@babel/types');
const common = require('./common.js');
const random = require('../random.js');
const mutator = require('./mutator.js');
const MIN_SAFE_INTEGER = -9007199254740991;
const MAX_SAFE_INTEGER = 9007199254740991;
function isObjectKey(path) {
return (path.parent &&
babelTypes.isObjectMember(path.parent) &&
path.parent.key === path.node);
}
function createRandomNumber(value) {
// TODO(ochang): Maybe replace with variable.
const probability = random.random();
if (probability < 0.01) {
return babelTypes.numericLiteral(
random.randInt(MIN_SAFE_INTEGER, MAX_SAFE_INTEGER));
} else if (probability < 0.06) {
return common.randomInterestingNumber();
} else {
return common.nearbyRandomNumber(value);
}
}
class NumberMutator extends mutator.Mutator {
constructor(settings) {
super();
this.settings = settings;
}
ignore(path) {
return !random.choose(this.settings.MUTATE_NUMBERS) ||
common.isInForLoopCondition(path) ||
common.isInWhileLoop(path);
}
randomReplace(path, value, forcePositive=false) {
const randomNumber = createRandomNumber(value);
if (forcePositive) {
randomNumber.value = Math.abs(randomNumber.value);
}
this.annotate(
path.node,
`Replaced ${value} with ${randomNumber.value}`);
this.replaceWithSkip(path, randomNumber);
}
get visitor() {
const thisMutator = this;
return {
NumericLiteral(path) {
if (thisMutator.ignore(path)) {
return;
}
// We handle negative unary expressions separately to replace the whole
// expression below. E.g. -5 is UnaryExpression(-, NumericLiteral(5)).
if (path.parent && babelTypes.isUnaryExpression(path.parent) &&
path.parent.operator === '-') {
return;
}
// Enfore positive numbers if the literal is the key of an object
// property or method. Negative keys cause syntax errors.
const forcePositive = isObjectKey(path);
thisMutator.randomReplace(path, path.node.value, forcePositive);
},
UnaryExpression(path) {
if (thisMutator.ignore(path)) {
return;
}
// Handle the case we ignore above.
if (path.node.operator === '-' &&
babelTypes.isNumericLiteral(path.node.argument)) {
thisMutator.randomReplace(path, -path.node.argument.value);
}
}
};
}
}
module.exports = {
NumberMutator: NumberMutator,
};
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Mutator for object expressions.
*/
'use strict';
const babelTypes = require('@babel/types');
const common = require('./common.js');
const mutator = require('./mutator.js');
const random = require('../random.js');
const MAX_PROPERTIES = 50;
/**
* Turn the key of an object property into a string literal.
*/
function keyToString(key) {
if (babelTypes.isNumericLiteral(key)) {
return babelTypes.stringLiteral(key.value.toString());
}
if (babelTypes.isIdentifier(key)) {
return babelTypes.stringLiteral(key.name);
}
// Already a string literal.
return key;
}
class ObjectMutator extends mutator.Mutator {
constructor(settings) {
super();
this.settings = settings;
}
get visitor() {
const thisMutator = this;
return {
ObjectExpression(path) {
const properties = path.node.properties;
if (!random.choose(thisMutator.settings.MUTATE_OBJECTS) ||
properties.length > MAX_PROPERTIES) {
return;
}
// Use the indices of object properties for mutations. We ignore
// getters and setters.
const propertyIndicies = [];
for (const [index, property] of properties.entries()) {
if (babelTypes.isObjectProperty(property)) {
propertyIndicies.push(index);
}
}
// The mutations below require at least one property.
if (!propertyIndicies.length) {
return;
}
// Annotate object expression with the action taken.
function annotate(message) {
thisMutator.annotate(path.node, message);
}
function getOneRandomProperty() {
return properties[random.single(propertyIndicies)];
}
function getTwoRandomProperties() {
const [a, b] = random.sample(propertyIndicies, 2);
return [properties[a], properties[b]];
}
function swapPropertyValues() {
if (propertyIndicies.length > 1) {
annotate('Swap properties');
const [a, b] = getTwoRandomProperties();
[a.value, b.value] = [b.value, a.value];
}
}
function duplicatePropertyValue() {
if (propertyIndicies.length > 1) {
const [a, b] = random.shuffle(getTwoRandomProperties());
if (common.isLargeNode(b.value)) {
return;
}
annotate('Duplicate a property value');
a.value = babelTypes.cloneDeep(b.value);
}
}
function insertRandomValue() {
annotate('Insert a random value');
const property = getOneRandomProperty();
property.value = common.randomValue(path);
}
function stringifyKey() {
annotate('Stringify a property key');
const property = getOneRandomProperty();
property.key = keyToString(property.key);
}
function removeProperty() {
annotate('Remove a property');
properties.splice(random.single(propertyIndicies), 1);
}
// Mutation options. Repeated mutations have a higher probability.
const mutations = [
swapPropertyValues,
swapPropertyValues,
duplicatePropertyValue,
duplicatePropertyValue,
insertRandomValue,
insertRandomValue,
removeProperty,
stringifyKey,
];
// Perform mutation.
random.single(mutations)();
},
}
}
}
module.exports = {
ObjectMutator: ObjectMutator,
};
This diff is collapsed.
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Variables mutator.
*/
'use strict';
const babelTypes = require('@babel/types');
const common = require('./common.js');
const random = require('../random.js');
const mutator = require('./mutator.js');
function _isInFunctionParam(path) {
const child = path.find(p => p.parent && babelTypes.isFunction(p.parent));
return child && child.parentKey === 'params';
}
class VariableMutator extends mutator.Mutator {
constructor(settings) {
super();
this.settings = settings;
}
get visitor() {
const thisMutator = this;
return {
Identifier(path) {
if (!random.choose(thisMutator.settings.MUTATE_VARIABLES)) {
return;
}
if (!common.isVariableIdentifier(path.node.name)) {
return;
}
// Don't mutate variables that are being declared.
if (babelTypes.isVariableDeclarator(path.parent)) {
return;
}
// Don't mutate function params.
if (_isInFunctionParam(path)) {
return;
}
if (common.isInForLoopCondition(path) ||
common.isInWhileLoop(path)) {
return;
}
const randVar = common.randomVariable(path);
if (!randVar) {
return;
}
const newName = randVar.name;
thisMutator.annotate(
path.node,
`Replaced ${path.node.name} with ${newName}`);
path.node.name = newName;
}
};
}
}
module.exports = {
VariableMutator: VariableMutator,
};
This diff is collapsed.
{
"name": "ochang_js_fuzzer",
"version": "1.0.0",
"description": "",
"main": "run.js",
"scripts": {
"test": "APP_NAME=d8 mocha"
},
"bin": "run.js",
"author": "ochang@google.com",
"license": "ISC",
"dependencies": {
"@babel/generator": "^7.1.3",
"@babel/template": "^7.1.2",
"@babel/traverse": "^7.1.4",
"@babel/types": "^7.1.3",
"@babel/parser": "^7.1.3",
"commander": "^2.11.0",
"globals": "^10.1.0",
"tempfile": "^3.0.0",
"tempy": "^0.5.0"
},
"devDependencies": {
"eslint": "^6.8.0",
"mocha": "^3.5.3",
"pkg": "^4.3.4",
"prettier": "2.0.5",
"sinon": "^4.0.0"
},
"pkg": {
"assets": "resources/**/*"
}
}
#!/bin/bash
# Copyright 2020 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
OS="linux"
OS_LABEL="Linux"
SUFFIX=""
if [[ -n "$1" && $1 == "win" ]]; then
OS="win"
OS_LABEL="Windows"
SUFFIX=".exe"
elif [[ -n "$1" && $1 == "macos" ]]; then
OS="macos"
OS_LABEL="MacOS"
fi
echo "Building and packaging for $OS_LABEL..."
(set -x; $DIR/node_modules/.bin/pkg -t node10-$OS-x64 $DIR)
rm -rf $DIR/output > /dev/null 2>&1 || true
rm $DIR/output.zip > /dev/null 2>&1 || true
mkdir $DIR/output
cd $DIR/output
ln -s ../db db
ln -s ../ochang_js_fuzzer$SUFFIX run$SUFFIX
ln -s ../foozzie_launcher.py foozzie_launcher.py
echo "Creating $DIR/output.zip"
(set -x; zip -r $DIR/output.zip * > /dev/null)
This diff is collapsed.
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
function debug(msg) {
__prettyPrintExtra(msg);
}
function shouldBe(_a) {
__prettyPrintExtra((typeof _a == "function" ? _a() : eval(_a)));
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment