v8/tools/clusterfuzz/js_fuzzer/script_mutator.js
Michael Achenbach 320d98709f 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: Maya Lekova <mslekova@chromium.org>
Cr-Commit-Position: refs/heads/master@{#69164}
2020-07-31 11:34:39 +00:00

226 lines
6.9 KiB
JavaScript

// 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.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const common = require('./mutators/common.js');
const db = require('./db.js');
const sourceHelpers = require('./source_helpers.js');
const { AddTryCatchMutator } = require('./mutators/try_catch.js');
const { ArrayMutator } = require('./mutators/array_mutator.js');
const { CrossOverMutator } = require('./mutators/crossover_mutator.js');
const { ExpressionMutator } = require('./mutators/expression_mutator.js');
const { FunctionCallMutator } = require('./mutators/function_call_mutator.js');
const { IdentifierNormalizer } = require('./mutators/normalizer.js');
const { NumberMutator } = require('./mutators/number_mutator.js');
const { ObjectMutator } = require('./mutators/object_mutator.js');
const { VariableMutator } = require('./mutators/variable_mutator.js');
const { VariableOrObjectMutator } = require('./mutators/variable_or_object_mutation.js');
function defaultSettings() {
return {
ADD_VAR_OR_OBJ_MUTATIONS: 0.1,
DIFF_FUZZ_EXTRA_PRINT: 0.1,
DIFF_FUZZ_TRACK_CAUGHT: 0.4,
MUTATE_ARRAYS: 0.1,
MUTATE_CROSSOVER_INSERT: 0.05,
MUTATE_EXPRESSIONS: 0.1,
MUTATE_FUNCTION_CALLS: 0.1,
MUTATE_NUMBERS: 0.05,
MUTATE_OBJECTS: 0.1,
MUTATE_VARIABLES: 0.075,
};
}
class Result {
constructor(code, flags) {
this.code = code;
this.flags = flags;
}
}
class ScriptMutator {
constructor(settings, db_path=undefined) {
// Use process.cwd() to bypass pkg's snapshot filesystem.
this.mutateDb = new db.MutateDb(db_path || path.join(process.cwd(), 'db'));
this.mutators = [
new ArrayMutator(settings),
new ObjectMutator(settings),
new VariableMutator(settings),
new NumberMutator(settings),
new CrossOverMutator(settings, this.mutateDb),
new ExpressionMutator(settings),
new FunctionCallMutator(settings),
new VariableOrObjectMutator(settings),
new AddTryCatchMutator(settings),
];
}
_addMjsunitIfNeeded(dependencies, input) {
if (dependencies.has('mjsunit')) {
return;
}
if (!input.absPath.includes('mjsunit')) {
return;
}
// Find mjsunit.js
let mjsunitPath = input.absPath;
while (path.dirname(mjsunitPath) != mjsunitPath &&
path.basename(mjsunitPath) != 'mjsunit') {
mjsunitPath = path.dirname(mjsunitPath);
}
if (path.basename(mjsunitPath) == 'mjsunit') {
mjsunitPath = path.join(mjsunitPath, 'mjsunit.js');
dependencies.set('mjsunit', sourceHelpers.loadDependencyAbs(
input.baseDir, mjsunitPath));
return;
}
console.log('ERROR: Failed to find mjsunit.js');
}
_addSpiderMonkeyShellIfNeeded(dependencies, input) {
// Find shell.js files
const shellJsPaths = new Array();
let currentDir = path.dirname(input.absPath);
while (path.dirname(currentDir) != currentDir) {
const shellJsPath = path.join(currentDir, 'shell.js');
if (fs.existsSync(shellJsPath)) {
shellJsPaths.push(shellJsPath);
}
if (currentDir == 'spidermonkey') {
break;
}
currentDir = path.dirname(currentDir);
}
// Add shell.js dependencies in reverse to add ones that are higher up in
// the directory tree first.
for (let i = shellJsPaths.length - 1; i >= 0; i--) {
if (!dependencies.has(shellJsPaths[i])) {
const dependency = sourceHelpers.loadDependencyAbs(
input.baseDir, shellJsPaths[i]);
dependencies.set(shellJsPaths[i], dependency);
}
}
}
_addJSTestStubsIfNeeded(dependencies, input) {
if (dependencies.has('jstest_stubs') ||
!input.absPath.includes('JSTests')) {
return;
}
dependencies.set(
'jstest_stubs', sourceHelpers.loadResource('jstest_stubs.js'));
}
mutate(source) {
for (const mutator of this.mutators) {
mutator.mutate(source);
}
}
// Returns parsed dependencies for inputs.
resolveInputDependencies(inputs) {
const dependencies = new Map();
// Resolve test harness files.
inputs.forEach(input => {
try {
// TODO(machenbach): Some harness files contain load expressions
// that are not recursively resolved. We already remove them, but we
// also need to load the dependencies they point to.
this._addJSTestStubsIfNeeded(dependencies, input);
this._addMjsunitIfNeeded(dependencies, input)
this._addSpiderMonkeyShellIfNeeded(dependencies, input);
} catch (e) {
console.log(
'ERROR: Failed to resolve test harness for', input.relPath);
throw e;
}
});
// Resolve dependencies loaded within the input files.
inputs.forEach(input => {
try {
input.loadDependencies(dependencies);
} catch (e) {
console.log(
'ERROR: Failed to resolve dependencies for', input.relPath);
throw e;
}
});
// Map.values() returns values in insertion order.
return Array.from(dependencies.values());
}
// Combines input dependencies with fuzzer resources.
resolveDependencies(inputs) {
const dependencies = this.resolveInputDependencies(inputs);
// Add stubs for non-standard functions in the beginning.
dependencies.unshift(sourceHelpers.loadResource('stubs.js'));
// Add our fuzzing support helpers. This also overrides some common test
// functions from earlier dependencies that cause early bailouts.
dependencies.push(sourceHelpers.loadResource('fuzz_library.js'));
return dependencies;
}
// Normalizes, combines and mutates multiple inputs.
mutateInputs(inputs) {
const normalizerMutator = new IdentifierNormalizer();
for (const [index, input] of inputs.entries()) {
try {
normalizerMutator.mutate(input);
} catch (e) {
console.log('ERROR: Failed to normalize ', input.relPath);
throw e;
}
common.setSourceLoc(input, index, inputs.length);
}
// Combine ASTs into one. This is so that mutations have more context to
// cross over content between ASTs (e.g. variables).
const combinedSource = common.concatPrograms(inputs);
this.mutate(combinedSource);
return combinedSource;
}
mutateMultiple(inputs) {
// High level operation:
// 1) Compute dependencies from inputs.
// 2) Normalize, combine and mutate inputs.
// 3) Generate code with dependency code prepended.
const dependencies = this.resolveDependencies(inputs);
const combinedSource = this.mutateInputs(inputs);
const code = sourceHelpers.generateCode(combinedSource, dependencies);
const flags = common.concatFlags(dependencies.concat([combinedSource]));
return new Result(code, flags);
}
}
module.exports = {
defaultSettings: defaultSettings,
ScriptMutator: ScriptMutator,
};