Upload Kmake

This commit is contained in:
Gorochu
2026-05-26 23:36:42 -07:00
parent ba051b2f74
commit 555ec72358
41615 changed files with 13344630 additions and 1 deletions

View File

@ -0,0 +1,257 @@
// Copyright 2025 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 Analyzer.
* Mutator that acquires additional information used in subsequent mutators.
* This mutator doesn't change the structure, but only marks nodes with
* meta information.
*/
'use strict';
const assert = require('assert');
const babelTypes = require('@babel/types');
const common = require('./common.js');
const mutator = require('./mutator.js');
// Additional resource needed if the AsyncIterator ID is found in the code.
const ASYNC_ITERATOR_ID = 'AsyncIterator'
const ASYNC_ITERATOR_RESOURCE = 'async_iterator.js'
// State for keeping track of loops and loop bodies during traversal.
const LoopState = Object.freeze({
LOOP: Symbol("loop"),
BLOCK: Symbol("block"),
});
// We consider loops with more iterations than this as large. This can be used
// to reduce some mutations in such loops.
const LARGE_LOOP_THRESHOLD = 1000;
/**
* Combine multiple enter/exit visitor pairs. Also single enter or exit
* methods are possible.
*/
function compound(...visitors) {
const enters = [];
const exits = [];
for (const visitor of visitors) {
if ("enter" in visitor) enters.push(visitor.enter);
if ("exit" in visitor) exits.push(visitor.exit);
}
return {
enter: enters,
// Exit in reverse order so that the corresponding enter/exit pairs
// operate on a stack.
exit: exits.reverse(),
};
}
/**
* Returns true for numeric values with or without an unary minus compound.
*/
function isNumber(node) {
return (
(
// Direct valid number literal.
babelTypes.isLiteral(node) &&
!isNaN(node.value)
) ||
(
// A number literal wrapped with an unary minus.
babelTypes.isUnaryExpression(node) &&
node.operator === '-' &&
babelTypes.isLiteral(node.argument) &&
!isNaN(node.argument.value)
)
);
}
/**
* Extracts the numeric value from a node. We assume it is a number as
* determined by the function above.
*/
function getNumber(node) {
// Direct valid number literal.
if (babelTypes.isLiteral(node)) {
assert(!isNaN(node.value));
return node.value;
}
// A number literal wrapped with an unary minus.
assert(babelTypes.isUnaryExpression(node));
assert(node.operator === '-');
assert(babelTypes.isLiteral(node.argument));
assert(!isNaN(node.argument.value));
return -node.argument.value;
}
/**
* Resolve two nodes in any order as [identifier, number] if possible,
* [undefined, undefined] otherwise.
*/
function idAndNumber(node1, node2) {
if(!node1 || !node2) {
return [undefined, undefined];
}
if (babelTypes.isIdentifier(node1) && isNumber(node2)) {
return [node1.name, getNumber(node2)];
}
if (isNumber(node1) && babelTypes.isIdentifier(node2)) {
return [node2.name, getNumber(node1)];
}
return [undefined, undefined];
}
/**
* Return true if we approximate this node as a large for-loop. We
* heuristically look at simple loop initializers and conditions only.
*
* As this is only an approximation, it will determine some large loops
* as small and vice versa. Hence the result of this should only be used
* for soft logical changes during fuzzing, e.g. behind a probability.
*/
function approximateLargeForLoop(node) {
assert(babelTypes.isForStatement(node));
// The initializer must be a single declaration of the form:
// `{var|let|const} {decl}`.
if (!node.init ||
!babelTypes.isVariableDeclaration(node.init) ||
node.init.declarations.length != 1) {
return false;
}
// The declaration must assign a number to an identifier: `id = num`.
const decl = node.init.declarations[0];
if (!decl ||
!babelTypes.isVariableDeclarator(decl) ||
!babelTypes.isIdentifier(decl.id) ||
!decl.init ||
!isNumber(decl.init)) {
return false;
}
// The test part must be a single binary expression. We don't care which
// operator, e.g. `A < B`.
const initID = decl.id.name;
const initNumber = getNumber(decl.init);
if (!node.test || !babelTypes.isBinaryExpression(node.test)) {
return false;
}
// The two operants must be an identifier and a number in any order. E.g.:
// `id < num`.
const [testID, testNumber] = idAndNumber(node.test.left, node.test.right);
if (testID === undefined || testNumber === undefined) {
return false;
}
// If we test the same identifier we initialized and if the numeric
// difference is large enough, we approximate this as large. We ignore
// increments.
return (
initID === testID &&
Math.abs(initNumber - testNumber) >= LARGE_LOOP_THRESHOLD);
}
class ContextAnalyzer extends mutator.Mutator {
constructor() {
super();
this.loopStack = [];
}
/**
* Return true if the traversal is currently within a loop test part.
*/
isLoopTest() {
return this.loopStack.at(-1) === LoopState.LOOP;
}
get visitor() {
const thisMutator = this;
const loopStack = this.loopStack;
// Maintain the state if we're in a loop.
const loopStatement = {
enter(path) {
loopStack.push(LoopState.LOOP);
},
exit(path) {
assert(loopStack.pop() === LoopState.LOOP);
}
};
// Mark functions that contain empty infinite loops.
const emptyInfiniteLoop = {
enter(path) {
if (common.isInfiniteLoop(path.node) &&
path.node.body &&
babelTypes.isBlockStatement(path.node.body) &&
!path.node.body.body.length) {
const fun = path.findParent((p) => p.isFunctionDeclaration());
if (fun && fun.node && fun.node.id &&
babelTypes.isIdentifier(fun.node.id) &&
common.isFunctionIdentifier(fun.node.id.name)) {
thisMutator.context.infiniteFunctions.add(fun.node.id.name);
}
}
},
};
const largeForLoop = {
enter(path) {
if (approximateLargeForLoop(path.node)) {
common.setLargeLoop(path.node);
}
},
};
return {
Identifier(path) {
if (path.node.name === ASYNC_ITERATOR_ID) {
thisMutator.context.extraResources.add(ASYNC_ITERATOR_RESOURCE);
}
if (thisMutator.isLoopTest() &&
common.isVariableIdentifier(path.node.name)) {
thisMutator.context.loopVariables.add(path.node.name);
}
},
FunctionDeclaration(path) {
if (path.node.id && babelTypes.isIdentifier(path.node.id)) {
if (common.isFunctionIdentifier(path.node.id.name) &&
path.node.async) {
thisMutator.context.asyncFunctions.add(path.node.id.name);
}
}
},
WhileStatement: compound(loopStatement, emptyInfiniteLoop),
DoWhileStatement: compound(loopStatement, emptyInfiniteLoop),
ForStatement: compound(loopStatement, largeForLoop),
BlockStatement: {
enter(path) {
loopStack.push(LoopState.BLOCK);
},
exit(path) {
assert(loopStack.pop() === LoopState.BLOCK);
}
},
// Mark nodes containing a yield expression up to the encolsing
// generator function declaration.
YieldExpression(path) {
for (let parent = path.parentPath;
parent && !parent.isFunctionDeclaration();
parent = parent.parentPath) {
common.setContainsYield(parent.node);
}
},
};
}
}
module.exports = {
ContextAnalyzer: ContextAnalyzer,
};

View File

@ -0,0 +1,111 @@
// 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 {
get visitor() {
const thisMutator = this;
return {
ArrayExpression(path) {
const elements = path.node.elements;
if (!random.choose(thisMutator.settings.MUTATE_ARRAYS) ||
elements.length > module.exports.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 = {
MAX_ARRAY_LENGTH: MAX_ARRAY_LENGTH,
ArrayMutator: ArrayMutator,
};

View File

@ -0,0 +1,145 @@
// Copyright 2025 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 Closure remover.
*
* This transforms top-level parameterless closures and inlines their code
* into the enclosing program scope. Such code is more accessible to subsequent
* mutations.
*/
const assert = require('assert');
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');
/**
* Returns true if this function expression can be transformed.
*/
function canTransformFunctionExpression(node, referencedFunctionIds) {
return (
// Needs to be a kind of function expression.
(babelTypes.isFunctionExpression(node) ||
babelTypes.isArrowFunctionExpression(node)) &&
// Can't contain return statements. We mark these during the first
// pass of the analysis.
!node.__returns &&
// We just don't support these.
!node.async &&
!node.expression &&
!node.generator &&
// Can't have parameters.
node.params.length == 0 &&
// Is either anonymous or doesn't reference its identifier. We track
// references during the first pass of the analysis.
(!node.id ||
(babelTypes.isIdentifier(node.id) &&
!referencedFunctionIds.has(node.id.name))) &&
// Has a block body.
node.body &&
babelTypes.isBlockStatement(node.body) &&
// The block is not empty.
node.body.body &&
node.body.body.length
);
}
/**
* Returns true if this closure can be transformed.
*/
function canTransformClosure(path, referencedFunctionIds) {
return (
// We only transform top-level nodes.
path.parentPath.isProgram() &&
// Expect a call-expression statement.
path.node.expression &&
babelTypes.isCallExpression(path.node.expression) &&
// Can't pass arguments.
path.node.expression.arguments.length == 0 &&
// Must call an inlined function expression.
path.node.expression.callee &&
canTransformFunctionExpression(
path.node.expression.callee, referencedFunctionIds)
);
}
// We access the body assuming it has been checked before with
// `isRemovableFunctionExpression`.
function getFunctionBody(path) {
return path.node.expression.callee.body.body;
}
class ClosureRemover extends mutator.Mutator {
get visitor() {
if (!random.choose(this.settings.TRANSFORM_CLOSURES)) {
return [];
}
const thisMutator = this;
const referencedFunctionIds = new Set();
const functionStack = [];
let transformationCount = 0;
// Maintain function expression stack. During the traversal, return
// statements belong to the function on the top of the stack.
const functionExpression = {
enter(path) {
functionStack.push(path.node);
},
exit(path) {
const node = functionStack.pop()
assert(babelTypes.isFunctionExpression(node) ||
babelTypes.isArrowFunctionExpression(node));
}
};
return [{
// First pass to mark functions with return statements and to mark
// referenced function identifiers.
ArrowFunctionExpression: functionExpression,
FunctionExpression: functionExpression,
Identifier(path) {
if (path.isReferencedIdentifier() &&
common.isFunctionIdentifier(path.node.name)) {
referencedFunctionIds.add(path.node.name);
}
},
ReturnStatement(path) {
const currentFunction = functionStack.at(-1);
if (currentFunction) {
currentFunction.__returns = true;
}
path.skip();
}
}, {
// Second pass to transform closures.
ExpressionStatement(path) {
if (!canTransformClosure(path, referencedFunctionIds)) {
path.skip();
return;
}
for (const node of getFunctionBody(path)) {
path.insertBefore(node);
}
path.remove();
transformationCount++;
},
Program: {
exit(path) {
thisMutator.annotate(
path.node,
`Transformed ${transformationCount} closures.`);
}
}
}];
}
}
module.exports = {
ClosureRemover: ClosureRemover,
}

View File

@ -0,0 +1,487 @@
// 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;
// If chosen, a strict execution order is used for finding available
// variables, which however limits the choices. The probability is
// rather low than high, as violations can only occur if calls of
// enclosing functions are also moved up. Most of these rare violations
// would be wrapped by the try-catch mutator.
const STRICT_EXECUTION_ORDER_PROB = 0.3;
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 isInfiniteLoop(node) {
// Approximates if node is a trivial infinite while loop. Assumes that node
// is either a while or dowhile statement.
return (node.test &&
babelTypes.isLiteral(node.test) &&
node.test.value === true);
}
/**
* Return if `currentPath` might execute before `otherPath`.
*
* This is more permissive for function expressions, whose lexical location
* has more available identifiers without leading to wrong code when those
* are used within the body.
*
* Behind a probabiliy, this is also more permissive for function declarations.
* There, however, depending on where the function call might happen,
* the resulting code can cause reference errors.
*
* The permissive logic for functions and function expressions is only
* implemented for one level. It could also be implemented for further nested
* levels.
*
* Example 1:
* foo();
* let a;
* function foo() { using `a` here might cause a reference error}
*
* Example 2:
* let a;
* (function () { using `a` here probably can't cause a reference error})();
*/
function executesBefore(currentPath, otherPath) {
if (!currentPath.willIMaybeExecuteBefore(otherPath)) {
return false;
}
// TODO(machenbach): The enclosing function info could be cached across
// several calls to executesBefore in the loop below.
const enclosingFunExpr = currentPath.findParent(
x => x.isFunctionExpression() || x.isArrowFunctionExpression());
if (enclosingFunExpr &&
!enclosingFunExpr.willIMaybeExecuteBefore(otherPath)) {
return false;
}
if (random.choose(module.exports.STRICT_EXECUTION_ORDER_PROB)) {
return true;
}
const enclosingFunDecl = currentPath.findParent(
x => x.isFunctionDeclaration());
if (enclosingFunDecl &&
!enclosingFunDecl.willIMaybeExecuteBefore(otherPath)) {
return false;
}
return true;
}
function* _availableIdentifierNamesGen(path, filter) {
// TODO(ochang): Consider globals that aren't declared with let/var etc.
const allBindings = path.scope.getAllBindings();
for (const key of Object.keys(allBindings)) {
if (!filter(key)) {
continue;
}
if (filter === isVariableIdentifier &&
executesBefore(path, allBindings[key].path)) {
continue;
}
yield key;
}
}
function _availableIdentifiers(path, filter) {
const available = new Array();
for (const name of _availableIdentifierNamesGen(path, filter)) {
available.push(_identifier(name));
}
return available;
}
function availableVariables(path) {
return _availableIdentifiers(path, isVariableIdentifier);
}
function availableFunctions(path) {
return _availableIdentifiers(path, isFunctionIdentifier);
}
function availableFunctionNames(path) {
const available = new Set([]);
for (const name of _availableIdentifierNamesGen(
path, isFunctionIdentifier)) {
available.add(name);
}
return available;
}
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;
}
function getOriginalPath(node) {
// Original path is invalid in cloned nodes.
if (node !== node.__self) {
return undefined;
}
return node.__path;
}
function setLargeLoop(node) {
node.__is_large_loop = true;
}
function isLargeLoop(node) {
return Boolean(node.__is_large_loop);
}
function setContainsYield(node) {
node.__contains_yield = true;
}
function containsYield(node) {
return Boolean(node.__contains_yield);
}
// 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 = {
STRICT_EXECUTION_ORDER_PROB: STRICT_EXECUTION_ORDER_PROB,
callRandomFunction: callRandomFunction,
concatFlags: concatFlags,
concatPrograms: concatPrograms,
containsYield: containsYield,
availableVariables: availableVariables,
availableFunctions: availableFunctions,
availableFunctionNames: availableFunctionNames,
randomFunction: randomFunction,
randomVariable: randomVariable,
isInForLoopCondition: isInForLoopCondition,
isInWhileLoop: isInWhileLoop,
isInfiniteLoop: isInfiniteLoop,
isLargeLoop: isLargeLoop,
isLargeNode: isLargeNode,
isVariableIdentifier: isVariableIdentifier,
isFunctionIdentifier: isFunctionIdentifier,
nearbyRandomNumber: nearbyRandomNumber,
randomArguments: randomArguments,
randomInterestingNonNumber: randomInterestingNonNumber,
randomInterestingNumber: randomInterestingNumber,
randomObject: randomObject,
randomProperty: randomProperty,
randomSeed: randomSeed,
randomValue: randomValue,
setContainsYield: setContainsYield,
setLargeLoop: setLargeLoop,
getOriginalPath: getOriginalPath,
setOriginalPath: setOriginalPath,
getSourceLoc: getSourceLoc,
setSourceLoc: setSourceLoc,
}

View File

@ -0,0 +1,182 @@
// 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 babelTraverse = require('@babel/traverse').default;
const babelTypes = require('@babel/types');
const babylon = require('@babel/parser');
const common = require('./common.js');
const random = require('../random.js');
const mutator = require('./mutator.js');
const sourceHelpers = require('../source_helpers.js');
const tryCatch = require('./try_catch.js');
const BABYLON_RELAXED_SUPER_OPTIONS = Object.assign(
{}, sourceHelpers.BABYLON_OPTIONS);
BABYLON_RELAXED_SUPER_OPTIONS['allowSuperOutsideMethod'] = true;
// Drop desired try-catch wrapping only with a small probability.
const WRAP_TC_IF_NEEDED_PROB = 0.8;
/**
* Check if a snippet containing `super` can be inserted at `path`
* without creating a syntax error.
*
* Syntax errors would be raised if super is used within a class that
* doesn't have a superclass or if a super() call is used outside of
* a constructor. Member expressions like super.x are allowed.
*/
function validateSuper(path, source) {
const ast = babylon.parse(source, BABYLON_RELAXED_SUPER_OPTIONS);
// TODO(389069288): The DB currently only stores if a snippet contains
// super. We could move this further differentiation into the DB.
// But since it's rarely used, we just re-parse the snippet here on the fly
// for now.
// Furthermore: Some snippets might actually provide a self-contained
// `super`, properly enclosed by an extending class. For these we could be
// more permissive, but they are rare.
let call = false;
babelTraverse(ast, {
Super(path) {
if (babelTypes.isCallExpression(path.parent)) {
call = true;
}
},
});
const surroundingMethod = path.findParent(x => x.isClassMethod());
const surroundingClass = surroundingMethod.findParent(x => x.isClass());
return surroundingClass && surroundingClass.node.superClass &&
(!call || surroundingMethod.node.kind == 'constructor');
}
/**
* Checks if the expression contains a member expression with an identifier.
*
* These get extra try-catch wrapping as such accesses are often wrong out of
* context. The try-catch mutator doesn't always wrap all expressions.
*/
function needsTryCatch(source) {
const ast = babylon.parse(source, BABYLON_RELAXED_SUPER_OPTIONS);
// TODO(389069288): We could precalculate this and store it in the DB
// together with the snippets.
let member = false;
babelTraverse(ast, {
MemberExpression(path) {
if (babelTypes.isIdentifier(path.node.property)) {
member = true;
}
},
});
return member;
}
class CrossOverMutator extends mutator.Mutator {
constructor(settings, db) {
super(settings);
this._db = db;
}
// For testing.
db() {
return this._db;
}
createInsertion(path, expression) {
if (expression.needsSuper &&
!validateSuper(path, expression.source)) {
return undefined;
}
// Insert the statement.
let toInsert = babelTemplate(
expression.source,
sourceHelpers.BABYLON_REPLACE_VAR_OPTIONS);
const dependencies = {};
const expressionDependencies = expression.dependencies;
if (expressionDependencies) {
const variables = common.availableVariables(path);
if (variables.length < expressionDependencies.length) {
return undefined;
}
const chosenVariables = random.sample(
variables, expressionDependencies.length);
for (const [index, dependency] of expressionDependencies.entries()) {
dependencies[dependency] = chosenVariables[index];
}
}
try {
toInsert = toInsert(dependencies);
} catch (e) {
if (this.settings.testing) {
// Fail early in tests.
throw e;
}
console.log('ERROR: Failed to parse:', expression.source);
console.log(e);
return undefined;
}
if (random.choose(WRAP_TC_IF_NEEDED_PROB) &&
needsTryCatch(expression.source)) {
toInsert = tryCatch.wrapTryCatch(toInsert);
}
this.annotate(
toInsert,
'Crossover from ' + expression.originalPath);
return toInsert;
}
get visitor() {
const thisMutator = this;
return [{
ForStatement(path) {
// Cross-over insertions in large loops often lead to timeouts.
if (common.isLargeLoop(path.node) &&
random.choose(mutator.SKIP_LARGE_LOOP_MUTATION_PROB)) {
path.skip();
}
},
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});
const toInsert = thisMutator.createInsertion(path, randomExpression);
if (!toInsert) return;
if (random.choose(0.5)) {
thisMutator.insertBeforeSkip(path, toInsert);
} else {
thisMutator.insertAfterSkip(path, toInsert);
}
path.skip();
},
}, {
}];
}
}
module.exports = {
CrossOverMutator: CrossOverMutator,
};

View File

@ -0,0 +1,225 @@
// 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/380147861
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,
};

View File

@ -0,0 +1,61 @@
// 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 {
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. Bias
// towards the former, as the other way can lead to use of undefined
// variables.
const [selected, destination] = random.single(
[[prev, path], [prev, path], [prev, path], [path, prev]]);
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,
};

View File

@ -0,0 +1,181 @@
// 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');
const REPLACE_CROSS_ASYNC_PROB = 0.2;
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));
}
/**
* All functions in the path's scope that keep the same async property.
* A different property (e.g. sync to async) is used only with a small
* probability. Additionally this doesn't include the current function.
*/
function availableReplacementFunctionNames(path, context) {
const available = common.availableFunctionNames(path);
const oldFunc = path.node.callee.name;
// The available set is a fresh set which we can mutate here.
available.delete(oldFunc);
// Disregard infinitely running functions.
for (const name of context.infiniteFunctions.values()) {
available.delete(name);
}
// If we don't care to maintain the async property, just use all functions.
if (random.choose(module.exports.REPLACE_CROSS_ASYNC_PROB)) {
return Array.from(available.values());
}
// Return an array with functions of the same async property as the current.
const oldIsAsync = context.asyncFunctions.has(oldFunc);
let result = new Array();
for (const name of available.values()) {
if (oldIsAsync == context.asyncFunctions.has(name)) {
result.push(name);
}
}
return result;
}
class FunctionCallMutator extends mutator.Mutator {
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.3) {
const replacement = random.single(availableReplacementFunctionNames(
path, thisMutator.context));
if (replacement) {
thisMutator.annotate(
path.node,
`Replaced ${path.node.callee.name} with ${replacement}`);
path.node.callee = babelTypes.identifier(replacement);
}
} else if (probability < 0.7 && thisMutator.settings.engine == 'v8') {
const prepareTemplate = babelTemplate(
'__V8BuiltinPrepareFunctionForOptimization(ID)');
const optimizationMode = random.choose(0.7) ? 'Function' : 'Maglev';
const optimizeTemplate = babelTemplate(
`__V8BuiltinOptimize${optimizationMode}OnNextCall(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.8 && thisMutator.settings.engine == 'v8') {
const template = babelTemplate(
'__V8BuiltinCompileBaseline(ID)');
const nodes = [
template({
ID: babelTypes.cloneDeep(path.node.callee),
}).expression,
];
thisMutator.annotate(
nodes[0],
`Compiling baseline ${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.9 &&
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 = {
REPLACE_CROSS_ASYNC_PROB: REPLACE_CROSS_ASYNC_PROB,
FunctionCallMutator: FunctionCallMutator,
};

View File

@ -0,0 +1,125 @@
// 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');
const SKIP_LARGE_LOOP_MUTATION_PROB = 0.75;
/**
* Container for any state that lives throughout one fuzz-test output
* generation. Can be used to collect information during parsing and
* use it later in mutators.
*/
class MutationContext {
constructor () {
this.asyncFunctions = new Set();
this.infiniteFunctions = new Set();
this.extraResources = new Set();
this.loopVariables = new Set();
}
}
const EMPTY_DEFAULT_CONTEXT = new MutationContext();
class Mutator {
constructor(settings) {
this.settings = settings;
this.context = EMPTY_DEFAULT_CONTEXT;
}
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, context=EMPTY_DEFAULT_CONTEXT) {
this.context = context;
const result = this.visitor;
if (Array.isArray(result)) {
for (const visitor of result) {
this._traverse(source.ast, visitor);
}
} else {
this._traverse(source.ast, result);
}
}
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 = {
SKIP_LARGE_LOOP_MUTATION_PROB: SKIP_LARGE_LOOP_MUTATION_PROB,
Mutator: Mutator,
MutationContext: MutationContext,
}

View File

@ -0,0 +1,122 @@
// 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. This also normalizes all loop bodies with blocks.
*/
'use strict';
const babelTypes = require('@babel/types');
const common = require('./common.js');
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.normalizerContext = new NormalizerContext();
}
get visitor() {
const normalizerContext = this.normalizerContext;
const renamed = new WeakSet();
const globalMappings = new Map();
const loopStatement = {
enter(path) {
if (path.node.body && !babelTypes.isBlockStatement(path.node.body)) {
path.node.body = babelTypes.blockStatement([path.node.body]);
}
}
};
return [{
Scope(path) {
for (const [name, binding] of Object.entries(path.scope.bindings)) {
if (renamed.has(binding.identifier)) {
continue;
}
renamed.add(binding.identifier);
// Prefix with __JSF in the first pass to avoid collisions with
// leftover identifier names from old fuzzer output. A collision with
// __JSF would be extremely rare, hence we don't add extra logic.
if (babelTypes.isClassDeclaration(binding.path.node) ||
babelTypes.isClassExpression(binding.path.node)) {
path.scope.rename(name, '__JSF__c_' + normalizerContext.classIndex++);
} else if (babelTypes.isFunctionDeclaration(binding.path.node) ||
babelTypes.isFunctionExpression(binding.path.node)) {
path.scope.rename(name, '__JSF__f_' + normalizerContext.funcIndex++);
} else {
path.scope.rename(name, '__JSF__v_' + normalizerContext.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_' + normalizerContext.varIndex++);
}
}
},
WhileStatement: loopStatement,
DoWhileStatement: loopStatement,
ForStatement: loopStatement,
}, {
// Second pass to re-normalize already renamed identifiers to their final
// JS_Fuzzer name pattern.
Scope(path) {
for (const [name, binding] of Object.entries(path.scope.bindings)) {
if (name.startsWith('__JSF__')) {
path.scope.rename(name, name.substring(5));
}
}
},
// 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;
}
if (path.node.name === "constructor" &&
babelTypes.isClassMethod(path.parent) &&
path.parent.key == path.node) {
// Don't touch constructors.
return;
}
path.node.name = globalMappings.get(path.node.name);
},
}];
}
}
module.exports = {
IdentifierNormalizer: IdentifierNormalizer,
};

View File

@ -0,0 +1,128 @@
// 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 arrayMutator = require('./array_mutator.js');
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;
// Large arrays cause too many mutations, which also wastes time in this
// visitor. Often, large arrays are output from Binaryen, which gets
// invalidated easily.
const SKIP_LARGE_ARRAYS_PROB = 0.9;
function isMethodOrObjectKey(path) {
return (path.parent &&
(babelTypes.isObjectMember(path.parent) ||
babelTypes.isClassMethod(path.parent) ||
babelTypes.isClassProperty(path.parent)) &&
path.parent.key === path.node);
}
function isExponentiationBase(path) {
return (path.parent &&
babelTypes.isBinaryExpression(path.parent) &&
path.parent.operator === '**' &&
path.parent.left === 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 {
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 {
ArrayExpression(path) {
if (path.node.elements.length > arrayMutator.MAX_ARRAY_LENGTH &&
random.choose(module.exports.SKIP_LARGE_ARRAYS_PROB)) {
path.skip();
}
},
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.
// TODO(389069288): We also enforce a positive base for
// exponentiations as stand-alone negative numbers cause syntax errors.
// We could support this case by constructing a negative number as an
// UnaryExpression (with -) wrapping a number, which should get
// Babel to parenthesize the expression.
const forcePositive = (
isMethodOrObjectKey(path) || isExponentiationBase(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 = {
SKIP_LARGE_ARRAYS_PROB: SKIP_LARGE_ARRAYS_PROB,
NumberMutator: NumberMutator,
};

View File

@ -0,0 +1,130 @@
// 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 {
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.sampleOfTwo(propertyIndicies);
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,
};

View File

@ -0,0 +1,269 @@
// 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 Try catch wrapper.
*/
const assert = require('assert');
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');
// Default target probability for skipping try-catch completely.
const DEFAULT_SKIP_PROB = 0.05;
// Default target probability to wrap only on toplevel, i.e. to not nest
// try-catch.
const DEFAULT_TOPLEVEL_PROB = 0.5;
// Probability to deviate from defaults and use extreme cases.
const IGNORE_DEFAULT_PROB = 0.03;
// We don't support 'using' and 'async using'. We wrap var with try-catch.
const WRAPPABLE_DECL_KINDS = new Set(['let', 'const']);
// This function is defined in resources/fuzz_library.js.
const WRAP_FUN = babelTemplate('__wrapTC(() => ID)');
function isFunction(node) {
return (babelTypes.isArrowFunctionExpression(node) ||
babelTypes.isFunctionExpression(node) ||
babelTypes.isFunctionDeclaration(node));
}
function isFunctionBody(path) {
const parent = path.parent;
return parent && isFunction(parent) && parent.body == path.node;
}
function hasFunctionDeclaration(body) {
return body.some((node) => babelTypes.isFunctionDeclaration(node));
}
function _rawTryCatch(node, catchBlock=[]) {
return babelTypes.tryStatement(
node,
babelTypes.catchClause(
babelTypes.identifier('e'),
babelTypes.blockStatement(catchBlock)));
}
function wrapTryCatch(node) {
return _rawTryCatch(babelTypes.blockStatement([node]));
}
function wrapBlockWithTryCatch(block) {
assert(block.body);
assert(block.body.length); // We don't wrap empty blocks.
const lastStatement = block.body.at(-1);
// If the block ended with the return of an object expression,
// we also return an empty object expression in the catch block,
// which reduces problems if the returned value is destructured.
let catchBlock = [];
if (babelTypes.isReturnStatement(lastStatement) &&
lastStatement.argument &&
babelTypes.isObjectExpression(lastStatement.argument)) {
catchBlock.push(
babelTypes.returnStatement(babelTypes.objectExpression([])));
}
return babelTypes.blockStatement([_rawTryCatch(block, catchBlock)]);
}
function skipReplaceVariableDeclarator(path) {
return (
// Uninitialized variable.
!path.node.init ||
// Simple initialization with a literal.
babelTypes.isLiteral(path.node.init) ||
// Wrapping a yield expression with an arrow function is
// syntactically wrong.
common.containsYield(path.node) ||
// Initialization with undefined.
(babelTypes.isIdentifier(path.node.init) &&
path.node.init.name == 'undefined') ||
// Consistency check.
!babelTypes.isVariableDeclaration(path.parent) ||
// Don't wrap variables in loop declarations.
babelTypes.isLoop(path.parentPath.parent) ||
// Only wrap supported kinds.
!WRAPPABLE_DECL_KINDS.has(path.parent.kind))
}
function replaceVariableDeclarator(path) {
let wrapped;
if (babelTypes.isAwaitExpression(path.node.init)) {
// The await can't remain in the inner arrow function of WRAP_FUN,
// we pull it outside of the wrapper instead. E.g.
// "await x" becomes "await __wrapTC(() => x)".
wrapped = babelTypes.AwaitExpression(
WRAP_FUN({ID: path.node.init.argument}).expression);
} else {
wrapped = WRAP_FUN({ID: path.node.init}).expression;
}
path.replaceWith(babelTypes.variableDeclarator(path.node.id, wrapped));
path.skip();
}
function replaceAndSkip(path) {
if (!babelTypes.isLabeledStatement(path.parent) ||
!babelTypes.isLoop(path.node)) {
// Don't wrap loops with labels as it makes continue
// statements syntactically invalid. We wrap the label
// instead below.
path.replaceWith(wrapTryCatch(path.node));
}
// Prevent infinite looping.
path.skip();
}
function replaceBlockStatementAndSkip(path) {
path.replaceWith(wrapBlockWithTryCatch(path.node));
path.skip();
}
class AddTryCatchMutator extends mutator.Mutator {
callWithProb(path, fun) {
const probability = random.random();
if (probability < this.skipProb * this.loc) {
// Entirely skip try-catch wrapper.
path.skip();
} else if (probability < (this.skipProb + this.toplevelProb)) {
// Only wrap on top-level.
fun(path);
}
}
get visitor() {
const thisMutator = this;
const accessStatement = {
enter(path) {
thisMutator.callWithProb(path, replaceAndSkip);
},
exit(path) {
// Apply nested wrapping (is only executed if not skipped above).
replaceAndSkip(path);
}
};
const handleInfiniteLoops = {
enter(path) {
// Just replace the outer loop and don't progress on the inside if
// it's an infinite loop, since sometimes exceptions are used to
// break out of the loop.
if (common.isInfiniteLoop(path.node)) {
replaceAndSkip(path);
}
},
};
return {
Program: {
enter(path) {
// Track original source location fraction in [0, 1).
thisMutator.loc = 0;
// Target probability for skipping try-catch.
thisMutator.skipProb = module.exports.DEFAULT_SKIP_PROB;
// Target probability for not nesting try-catch.
thisMutator.toplevelProb = module.exports.DEFAULT_TOPLEVEL_PROB;
// Maybe deviate from target probability for the entire test.
if (random.choose(module.exports.IGNORE_DEFAULT_PROB)) {
thisMutator.skipProb = random.uniform(0, 0.5);
thisMutator.toplevelProb = random.uniform(0, 1);
thisMutator.annotate(
path.node,
'Target skip probability ' + thisMutator.skipProb +
' and toplevel probability ' + thisMutator.toplevelProb);
}
}
},
Noop: {
enter(path) {
if (common.getSourceLoc(path.node)) {
thisMutator.loc = common.getSourceLoc(path.node);
}
},
},
BlockStatement(path) {
// It'd be superfluous to wrap existing try statements.
if (path.parent && babelTypes.isTryStatement(path.parent)) {
path.skip();
return;
}
// Wrap the entire body of a function instead of descending into all
// the function's nodes. This also includes return statements.
// We only do this with the lower top-level wrapping probability and
// don't provide an exit() function here. I.e. if we did descent into
// the child nodes, we don't additionally wrap the body.
// Don't wrap if there's a nested function declaration in the body as
// this might break legacy scope compatibility rules.
if (isFunctionBody(path) &&
path.node.body &&
path.node.body.length &&
!hasFunctionDeclaration(path.node.body)) {
thisMutator.callWithProb(path, replaceBlockStatementAndSkip);
}
},
DoWhileStatement: handleInfiniteLoops,
ExpressionStatement: accessStatement,
IfStatement: accessStatement,
LabeledStatement: {
enter(path) {
// Apply an extra try-catch around the label of a loop, since we
// ignore the loop itself if it has a label.
if (babelTypes.isLoop(path.node.body)) {
thisMutator.callWithProb(path, replaceAndSkip);
}
},
exit(path) {
// Apply nested wrapping (is only executed if not skipped above).
if (babelTypes.isLoop(path.node.body)) {
replaceAndSkip(path);
}
},
},
// This covers {While|DoWhile|ForIn|ForOf|For}Statement.
Loop: accessStatement,
SwitchStatement: accessStatement,
VariableDeclaration: {
enter(path) {
if (path.node.kind !== 'var' || babelTypes.isLoop(path.parent))
return;
thisMutator.callWithProb(path, replaceAndSkip);
},
exit(path) {
if (path.node.kind !== 'var' || babelTypes.isLoop(path.parent))
return;
// Apply nested wrapping (is only executed if not skipped above).
replaceAndSkip(path);
}
},
VariableDeclarator: {
enter(path) {
if (skipReplaceVariableDeclarator(path))
return;
thisMutator.callWithProb(path, replaceVariableDeclarator);
},
exit(path) {
if (skipReplaceVariableDeclarator(path))
return;
// Apply nested wrapping (is only executed if not skipped above).
replaceVariableDeclarator(path);
}
},
WhileStatement: handleInfiniteLoops,
WithStatement: accessStatement,
};
}
}
module.exports = {
DEFAULT_SKIP_PROB: DEFAULT_SKIP_PROB,
DEFAULT_TOPLEVEL_PROB: DEFAULT_TOPLEVEL_PROB,
IGNORE_DEFAULT_PROB: IGNORE_DEFAULT_PROB,
AddTryCatchMutator: AddTryCatchMutator,
wrapTryCatch: wrapTryCatch,
}

View File

@ -0,0 +1,104 @@
// 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';
}
// Skip possible loop-variable mutations with a high probability.
const SKIP_LOOP_VAR_PROB = 0.95;
/**
* Returns true if path appears on the left-hand side of a variable declarator.
*
* This also includes nesting:
* let lhs_var = rhs_var;
* let {prop: lhs_var} = rhs_var;
*/
function _isBeingDeclared(path) {
const child = path.find(
p => p.parent && babelTypes.isVariableDeclarator(p.parent));
return child && child.parent.id == child.node;
}
class VariableMutator extends mutator.Mutator {
get visitor() {
const thisMutator = this;
return {
AssignmentExpression(path) {
// Don't change assignments to loop variables, also outside of loop
// conditions.
if (babelTypes.isIdentifier(path.node.left) &&
common.isVariableIdentifier(path.node.left.name) &&
thisMutator.context.loopVariables.has(path.node.left.name) &&
random.choose(module.exports.SKIP_LOOP_VAR_PROB)) {
path.skip();
}
},
Identifier(path) {
if (!random.choose(thisMutator.settings.MUTATE_VARIABLES)) {
return;
}
if (!common.isVariableIdentifier(path.node.name)) {
return;
}
// Don't mutate variables on the left-hand side of a declaration.
if (_isBeingDeclared(path)) {
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;
// Don't assign to loop variables.
if (babelTypes.isAssignmentExpression(path.parent) &&
path.parent.left == path.node &&
thisMutator.context.loopVariables.has(newName) &&
random.choose(module.exports.SKIP_LOOP_VAR_PROB)) {
return;
}
thisMutator.annotate(
path.node,
`Replaced ${path.node.name} with ${newName}`);
path.node.name = newName;
}
};
}
}
module.exports = {
SKIP_LOOP_VAR_PROB: SKIP_LOOP_VAR_PROB,
VariableMutator: VariableMutator,
};

View File

@ -0,0 +1,183 @@
// 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 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');
const variableMutator = require('../mutators/variable_mutator.js');
const MAX_MUTATION_RECURSION_DEPTH = 5;
const CHOOSE_MAJOR_GC_PROB = 0.7;
// Stub for testing.
function chooseCallGC() {
return random.choose(0.3);
}
/**
* Append a GC call to an expression with a probability.
*/
function maybeGCTemplate(expression) {
let templ = expression;
if (module.exports.chooseCallGC()) {
if (random.choose(CHOOSE_MAJOR_GC_PROB)){
templ += ', __callGC(true)';
} else {
templ += ', __callGC(false)';
}
}
return babelTemplate(templ);
}
class VariableOrObjectMutator extends mutator.Mutator {
_randomVariableOrObject(path) {
const randomVar = common.randomVariable(path);
if (random.choose(0.05) || !randomVar) {
return common.randomObject();
}
return randomVar;
}
_randomVariableOrObjectMutations(path, recurseDepth=0) {
if (recurseDepth >= MAX_MUTATION_RECURSION_DEPTH) {
return new Array();
}
const probability = random.random();
if (probability < 0.3) {
const first = this._randomVariableOrObjectMutations(path, recurseDepth + 1);
const second = this._randomVariableOrObjectMutations(
path, recurseDepth + 1);
return first.concat(second);
}
const randVarOrObject = this._randomVariableOrObject(path);
const randProperty = common.randomProperty(randVarOrObject);
let newRandVarOrObject = randVarOrObject;
if (random.choose(0.2)) {
newRandVarOrObject = this._randomVariableOrObject(path);
}
const mutations = new Array();
if (probability < 0.4) {
const template = maybeGCTemplate('delete IDENTIFIER[PROPERTY]')
mutations.push(template({
IDENTIFIER: randVarOrObject,
PROPERTY: randProperty
}));
} else if (probability < 0.5) {
const template = maybeGCTemplate('IDENTIFIER[PROPERTY]')
mutations.push(template({
IDENTIFIER: randVarOrObject,
PROPERTY: randProperty
}));
} else if (probability < 0.6) {
const template = maybeGCTemplate('IDENTIFIER[PROPERTY] = RANDOM')
mutations.push(template({
IDENTIFIER: randVarOrObject,
PROPERTY: randProperty,
RANDOM: common.randomValue(path),
}));
} else if (probability < 0.7) {
mutations.push(
babelTypes.expressionStatement(
common.callRandomFunction(path, randVarOrObject)));
} else if (probability < 0.8) {
const template = maybeGCTemplate('VAR = IDENTIFIER')
var randomVar = common.randomVariable(path);
if (!randomVar) {
return mutations;
}
// Don't assign to loop variables.
if (this.context.loopVariables.has(randomVar.name) &&
random.choose(variableMutator.SKIP_LOOP_VAR_PROB)) {
return mutations;
}
mutations.push(template({
VAR: randomVar,
IDENTIFIER: randVarOrObject,
}));
} else if (probability < 0.9) {
const template = babelTemplate(
'if (IDENTIFIER != null && typeof(IDENTIFIER) == "object") ' +
'Object.defineProperty(IDENTIFIER, PROPERTY, {value: VALUE})')
mutations.push(template({
IDENTIFIER: newRandVarOrObject,
PROPERTY: randProperty,
VALUE: common.randomValue(path),
}));
} else {
const template = babelTemplate(
'if (IDENTIFIER != null && typeof(IDENTIFIER) == "object") ' +
'Object.defineProperty(IDENTIFIER, PROPERTY, {' +
'get: function() { GETTER_MUTATION ; return VALUE; },' +
'set: function(value) { SETTER_MUTATION; }' +
'})');
mutations.push(template({
IDENTIFIER: newRandVarOrObject,
PROPERTY: randProperty,
GETTER_MUTATION: this._randomVariableOrObjectMutations(
path, recurseDepth + 1),
SETTER_MUTATION: this._randomVariableOrObjectMutations(
path, recurseDepth + 1),
VALUE: common.randomValue(path),
}));
}
return mutations;
}
get visitor() {
const settings = this.settings;
const thisMutator = this;
return {
ForStatement(path) {
// Var/obj mutations in large loops often lead to timeouts.
if (common.isLargeLoop(path.node) &&
random.choose(mutator.SKIP_LARGE_LOOP_MUTATION_PROB)) {
path.skip();
}
},
ExpressionStatement(path) {
if (!random.choose(settings.ADD_VAR_OR_OBJ_MUTATIONS)) {
return;
}
const mutations = thisMutator._randomVariableOrObjectMutations(path);
thisMutator.annotate(mutations[0], 'Random mutation');
if (random.choose(0.5)) {
thisMutator.insertBeforeSkip(path, mutations);
} else {
thisMutator.insertAfterSkip(path, mutations);
}
path.skip();
}
};
}
}
module.exports = {
VariableOrObjectMutator: VariableOrObjectMutator,
chooseCallGC: chooseCallGC,
};