Files
2026-05-26 23:36:42 -07:00

258 lines
7.5 KiB
JavaScript

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