320d98709f
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}
456 lines
11 KiB
JavaScript
456 lines
11 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 Mutation Db.
|
|
*/
|
|
|
|
const crypto = require('crypto');
|
|
const fs = require('fs');
|
|
const fsPath = require('path');
|
|
|
|
const babelGenerator = require('@babel/generator').default;
|
|
const babelTraverse = require('@babel/traverse').default;
|
|
const babelTypes = require('@babel/types');
|
|
const globals = require('globals');
|
|
|
|
const random = require('./random.js');
|
|
|
|
const globalIdentifiers = new Set(Object.keys(globals.builtin));
|
|
const propertyNames = new Set([
|
|
// Parsed from https://github.com/tc39/ecma262/blob/master/spec.html
|
|
'add',
|
|
'anchor',
|
|
'apply',
|
|
'big',
|
|
'bind',
|
|
'blink',
|
|
'bold',
|
|
'buffer',
|
|
'byteLength',
|
|
'byteOffset',
|
|
'BYTES_PER_ELEMENT',
|
|
'call',
|
|
'catch',
|
|
'charAt',
|
|
'charCodeAt',
|
|
'clear',
|
|
'codePointAt',
|
|
'compile',
|
|
'concat',
|
|
'constructor',
|
|
'copyWithin',
|
|
'__defineGetter__',
|
|
'__defineSetter__',
|
|
'delete',
|
|
'endsWith',
|
|
'entries',
|
|
'every',
|
|
'exec',
|
|
'fill',
|
|
'filter',
|
|
'find',
|
|
'findIndex',
|
|
'fixed',
|
|
'flags',
|
|
'fontcolor',
|
|
'fontsize',
|
|
'forEach',
|
|
'get',
|
|
'getDate',
|
|
'getDay',
|
|
'getFloat32',
|
|
'getFloat64',
|
|
'getFullYear',
|
|
'getHours',
|
|
'getInt16',
|
|
'getInt32',
|
|
'getInt8',
|
|
'getMilliseconds',
|
|
'getMinutes',
|
|
'getMonth',
|
|
'getSeconds',
|
|
'getTime',
|
|
'getTimezoneOffset',
|
|
'getUint16',
|
|
'getUint32',
|
|
'getUint8',
|
|
'getUTCDate',
|
|
'getUTCDay',
|
|
'getUTCFullYear',
|
|
'getUTCHours',
|
|
'getUTCMilliseconds',
|
|
'getUTCMinutes',
|
|
'getUTCMonth',
|
|
'getUTCSeconds',
|
|
'getYear',
|
|
'global',
|
|
'has',
|
|
'hasInstance',
|
|
'hasOwnProperty',
|
|
'ignoreCase',
|
|
'includes',
|
|
'indexOf',
|
|
'isConcatSpreadable',
|
|
'isPrototypeOf',
|
|
'italics',
|
|
'iterator',
|
|
'join',
|
|
'keys',
|
|
'lastIndexOf',
|
|
'length',
|
|
'link',
|
|
'localeCompare',
|
|
'__lookupGetter__',
|
|
'__lookupSetter__',
|
|
'map',
|
|
'match',
|
|
'match',
|
|
'message',
|
|
'multiline',
|
|
'name',
|
|
'next',
|
|
'normalize',
|
|
'padEnd',
|
|
'padStart',
|
|
'pop',
|
|
'propertyIsEnumerable',
|
|
'__proto__',
|
|
'prototype',
|
|
'push',
|
|
'reduce',
|
|
'reduceRight',
|
|
'repeat',
|
|
'replace',
|
|
'replace',
|
|
'return',
|
|
'reverse',
|
|
'search',
|
|
'search',
|
|
'set',
|
|
'set',
|
|
'setDate',
|
|
'setFloat32',
|
|
'setFloat64',
|
|
'setFullYear',
|
|
'setHours',
|
|
'setInt16',
|
|
'setInt32',
|
|
'setInt8',
|
|
'setMilliseconds',
|
|
'setMinutes',
|
|
'setMonth',
|
|
'setSeconds',
|
|
'setTime',
|
|
'setUint16',
|
|
'setUint32',
|
|
'setUint8',
|
|
'setUTCDate',
|
|
'setUTCFullYear',
|
|
'setUTCHours',
|
|
'setUTCMilliseconds',
|
|
'setUTCMinutes',
|
|
'setUTCMonth',
|
|
'setUTCSeconds',
|
|
'setYear',
|
|
'shift',
|
|
'size',
|
|
'slice',
|
|
'slice',
|
|
'small',
|
|
'some',
|
|
'sort',
|
|
'source',
|
|
'species',
|
|
'splice',
|
|
'split',
|
|
'split',
|
|
'startsWith',
|
|
'sticky',
|
|
'strike',
|
|
'sub',
|
|
'subarray',
|
|
'substr',
|
|
'substring',
|
|
'sup',
|
|
'test',
|
|
'then',
|
|
'throw',
|
|
'toDateString',
|
|
'toExponential',
|
|
'toFixed',
|
|
'toGMTString',
|
|
'toISOString',
|
|
'toJSON',
|
|
'toLocaleDateString',
|
|
'toLocaleLowerCase',
|
|
'toLocaleString',
|
|
'toLocaleTimeString',
|
|
'toLocaleUpperCase',
|
|
'toLowerCase',
|
|
'toPrecision',
|
|
'toPrimitive',
|
|
'toString',
|
|
'toStringTag',
|
|
'toTimeString',
|
|
'toUpperCase',
|
|
'toUTCString',
|
|
'trim',
|
|
'unicode',
|
|
'unscopables',
|
|
'unshift',
|
|
'valueOf',
|
|
'values',
|
|
]);
|
|
|
|
const MAX_DEPENDENCIES = 2;
|
|
|
|
class Expression {
|
|
constructor(type, source, isStatement, originalPath,
|
|
dependencies, needsSuper) {
|
|
this.type = type;
|
|
this.source = source;
|
|
this.isStatement = isStatement;
|
|
this.originalPath = originalPath;
|
|
this.dependencies = dependencies;
|
|
this.needsSuper = needsSuper;
|
|
}
|
|
}
|
|
|
|
function dedupKey(expression) {
|
|
if (!expression.dependencies) {
|
|
return expression.source;
|
|
}
|
|
|
|
let result = expression.source;
|
|
for (let dependency of expression.dependencies) {
|
|
result = result.replace(new RegExp(dependency, 'g'), 'ID');
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function _markSkipped(path) {
|
|
while (path) {
|
|
path.node.__skipped = true;
|
|
path = path.parentPath;
|
|
}
|
|
}
|
|
|
|
class MutateDbWriter {
|
|
constructor(outputDir) {
|
|
this.seen = new Set();
|
|
this.outputDir = fsPath.resolve(outputDir);
|
|
this.index = {
|
|
statements: [],
|
|
superStatements: [],
|
|
all: [],
|
|
};
|
|
}
|
|
|
|
process(source) {
|
|
let self = this;
|
|
|
|
let varIndex = 0;
|
|
|
|
// First pass to collect dependency information.
|
|
babelTraverse(source.ast, {
|
|
Super(path) {
|
|
while (path) {
|
|
path.node.__needsSuper = true;
|
|
path = path.parentPath;
|
|
}
|
|
},
|
|
|
|
YieldExpression(path) {
|
|
// Don't include yield expressions in DB.
|
|
_markSkipped(path);
|
|
},
|
|
|
|
Identifier(path) {
|
|
if (globalIdentifiers.has(path.node.name) &&
|
|
path.node.name != 'eval') {
|
|
// Global name.
|
|
return;
|
|
}
|
|
|
|
if (propertyNames.has(path.node.name) &&
|
|
path.parentPath.isMemberExpression() &&
|
|
path.parentKey !== 'object') {
|
|
// Builtin property name.
|
|
return;
|
|
}
|
|
|
|
let binding = path.scope.getBinding(path.node.name);
|
|
if (!binding) {
|
|
// Unknown dependency. Don't handle this.
|
|
_markSkipped(path);
|
|
return;
|
|
}
|
|
|
|
let newName;
|
|
if (path.node.name.startsWith('VAR_')) {
|
|
newName = path.node.name;
|
|
} else if (babelTypes.isFunctionDeclaration(binding.path.node) ||
|
|
babelTypes.isFunctionExpression(binding.path.node) ||
|
|
babelTypes.isDeclaration(binding.path.node) ||
|
|
babelTypes.isFunctionExpression(binding.path.node)) {
|
|
// Unknown dependency. Don't handle this.
|
|
_markSkipped(path);
|
|
return;
|
|
} else {
|
|
newName = 'VAR_' + varIndex++;
|
|
path.scope.rename(path.node.name, newName);
|
|
}
|
|
|
|
// Mark all parents as having a dependency.
|
|
while (path) {
|
|
path.node.__idDependencies = path.node.__idDependencies || [];
|
|
if (path.node.__idDependencies.length <= MAX_DEPENDENCIES) {
|
|
path.node.__idDependencies.push(newName);
|
|
}
|
|
path = path.parentPath;
|
|
}
|
|
}
|
|
});
|
|
|
|
babelTraverse(source.ast, {
|
|
Expression(path) {
|
|
if (!path.parentPath.isExpressionStatement()) {
|
|
return;
|
|
}
|
|
|
|
if (path.node.__skipped ||
|
|
(path.node.__idDependencies &&
|
|
path.node.__idDependencies.length > MAX_DEPENDENCIES)) {
|
|
return;
|
|
}
|
|
|
|
if (path.isIdentifier() || path.isMemberExpression() ||
|
|
path.isConditionalExpression() ||
|
|
path.isBinaryExpression() || path.isDoExpression() ||
|
|
path.isLiteral() ||
|
|
path.isObjectExpression() || path.isArrayExpression()) {
|
|
// Skip:
|
|
// - Identifiers.
|
|
// - Member expressions (too many and too context dependent).
|
|
// - Conditional expressions (too many and too context dependent).
|
|
// - Binary expressions (too many).
|
|
// - Literals (too many).
|
|
// - Object/array expressions (too many).
|
|
return;
|
|
}
|
|
|
|
if (path.isAssignmentExpression()) {
|
|
if (!babelTypes.isMemberExpression(path.node.left)) {
|
|
// Skip assignments that aren't to properties.
|
|
return;
|
|
}
|
|
|
|
if (babelTypes.isIdentifier(path.node.left.object)) {
|
|
if (babelTypes.isNumericLiteral(path.node.left.property)) {
|
|
// Skip VAR[\d+] = ...;
|
|
// There are too many and they generally aren't very useful.
|
|
return;
|
|
}
|
|
|
|
if (babelTypes.isStringLiteral(path.node.left.property) &&
|
|
!propertyNames.has(path.node.left.property.value)) {
|
|
// Skip custom properties. e.g.
|
|
// VAR["abc"] = ...;
|
|
// There are too many and they generally aren't very useful.
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (path.isCallExpression() &&
|
|
babelTypes.isIdentifier(path.node.callee) &&
|
|
!globalIdentifiers.has(path.node.callee.name)) {
|
|
// Skip VAR(...) calls since there's too much context we're missing.
|
|
return;
|
|
}
|
|
|
|
if (path.isUnaryExpression() && path.node.operator == '-') {
|
|
// Skip -... since there are too many.
|
|
return;
|
|
}
|
|
|
|
// Make the template.
|
|
let generated = babelGenerator(path.node, { concise: true }).code;
|
|
let expression = new Expression(
|
|
path.node.type,
|
|
generated,
|
|
path.parentPath.isExpressionStatement(),
|
|
source.relPath,
|
|
path.node.__idDependencies,
|
|
Boolean(path.node.__needsSuper));
|
|
|
|
// Try to de-dupe similar expressions.
|
|
let key = dedupKey(expression);
|
|
if (self.seen.has(key)) {
|
|
return;
|
|
}
|
|
|
|
// Write results.
|
|
let dirPath = fsPath.join(self.outputDir, expression.type);
|
|
if (!fs.existsSync(dirPath)) {
|
|
fs.mkdirSync(dirPath);
|
|
}
|
|
|
|
let sha1sum = crypto.createHash('sha1');
|
|
sha1sum.update(key);
|
|
|
|
let filePath = fsPath.join(dirPath, sha1sum.digest('hex') + '.json');
|
|
fs.writeFileSync(filePath, JSON.stringify(expression));
|
|
|
|
let relPath = fsPath.relative(self.outputDir, filePath);
|
|
|
|
// Update index.
|
|
self.seen.add(key);
|
|
self.index.all.push(relPath);
|
|
|
|
if (expression.needsSuper) {
|
|
self.index.superStatements.push(relPath);
|
|
} else {
|
|
self.index.statements.push(relPath);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
writeIndex() {
|
|
fs.writeFileSync(
|
|
fsPath.join(this.outputDir, 'index.json'),
|
|
JSON.stringify(this.index));
|
|
}
|
|
}
|
|
|
|
class MutateDb {
|
|
constructor(outputDir) {
|
|
this.outputDir = fsPath.resolve(outputDir);
|
|
this.index = JSON.parse(
|
|
fs.readFileSync(fsPath.join(outputDir, 'index.json'), 'utf-8'));
|
|
}
|
|
|
|
getRandomStatement({canHaveSuper=false} = {}) {
|
|
let choices;
|
|
if (canHaveSuper) {
|
|
choices = random.randInt(0, 1) ?
|
|
this.index.all : this.index.superStatements;
|
|
} else {
|
|
choices = this.index.statements;
|
|
}
|
|
|
|
let path = fsPath.join(
|
|
this.outputDir, choices[random.randInt(0, choices.length - 1)]);
|
|
return JSON.parse(fs.readFileSync(path), 'utf-8');
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
MutateDb: MutateDb,
|
|
MutateDbWriter: MutateDbWriter,
|
|
}
|