xref: /PHP-Parser/tools/fuzzing/target.php (revision 4b497045)
1<?php declare(strict_types=1);
2
3/** @var PhpFuzzer\Fuzzer $fuzzer */
4
5use PhpParser\Node\Expr;
6use PhpParser\Node\Scalar;
7use PhpParser\Node\Stmt;
8use PhpParser\NodeVisitor;
9
10if (class_exists(PhpParser\Parser\Php7::class)) {
11    echo "The PHP-Parser target can only be used with php-fuzzer.phar,\n";
12    echo "otherwise there is a conflict with php-fuzzer's own use of PHP-Parser.\n";
13    exit(1);
14}
15
16$autoload = __DIR__ . '/../../vendor/autoload.php';
17if (!file_exists($autoload)) {
18    echo "Cannot find PHP-Parser installation in " . __DIR__ . "/PHP-Parser\n";
19    exit(1);
20}
21
22require $autoload;
23
24$lexer = new PhpParser\Lexer();
25$parser = new PhpParser\Parser\Php7($lexer);
26$prettyPrinter = new PhpParser\PrettyPrinter\Standard();
27$nodeDumper = new PhpParser\NodeDumper();
28$visitor = new class extends PhpParser\NodeVisitorAbstract {
29    private const CAST_NAMES = [
30        'int', 'integer',
31        'double', 'float', 'real',
32        'string', 'binary',
33        'array', 'object',
34        'bool', 'boolean',
35        'unset',
36    ];
37
38
39    private $tokens;
40    public $hasProblematicConstruct;
41
42    public function setTokens(array $tokens) {
43        $this->tokens = $tokens;
44    }
45
46    public function beforeTraverse(array $nodes) {
47        $this->hasProblematicConstruct = false;
48    }
49
50    public function leaveNode(PhpParser\Node $node) {
51        // We don't precisely preserve nop statements.
52        if ($node instanceof Stmt\Nop) {
53            return NodeVisitor::REMOVE_NODE;
54        }
55
56        // We don't precisely preserve redundant trailing commas in array destructuring.
57        if ($node instanceof Expr\List_) {
58            while (!empty($node->items) && $node->items[count($node->items) - 1] === null) {
59                array_pop($node->items);
60            }
61        }
62
63        // For T_NUM_STRING the parser produced negative integer literals. Convert these into
64        // a unary minus followed by a positive integer.
65        if ($node instanceof Scalar\Int_ && $node->value < 0) {
66            if ($node->value === \PHP_INT_MIN) {
67                // PHP_INT_MIN == -PHP_INT_MAX - 1
68                return new Expr\BinaryOp\Minus(
69                    new Expr\UnaryMinus(new Scalar\Int_(\PHP_INT_MAX)),
70                    new Scalar\Int_(1));
71            }
72            return new Expr\UnaryMinus(new Scalar\Int_(-$node->value));
73        }
74
75        // If a constant with the same name as a cast operand occurs inside parentheses, it will
76        // be parsed back as a cast. E.g. "foo(int)" will fail to parse, because the argument is
77        // interpreted as a cast. We can run into this with inputs like "foo(int\n)", where the
78        // newline is not preserved.
79        if ($node instanceof Expr\ConstFetch && $node->name->isUnqualified() &&
80            in_array($node->name->toLowerString(), self::CAST_NAMES)
81        ) {
82            $this->hasProblematicConstruct = true;
83        }
84
85        // The parser does not distinguish between use X and use \X, as they are semantically
86        // equivalent. However, use \keyword is legal PHP, while use keyword is not, so we inspect
87        // tokens to detect this situation here.
88        if ($node instanceof Stmt\Use_ && $node->uses[0]->name->isUnqualified() &&
89            $this->tokens[$node->uses[0]->name->getStartTokenPos()]->is(\T_NAME_FULLY_QUALIFIED)
90        ) {
91            $this->hasProblematicConstruct = true;
92        }
93        if ($node instanceof Stmt\GroupUse && $node->prefix->isUnqualified() &&
94            $this->tokens[$node->prefix->getStartTokenPos()]->is(\T_NAME_FULLY_QUALIFIED)
95        ) {
96            $this->hasProblematicConstruct = true;
97        }
98    }
99};
100$traverser = new PhpParser\NodeTraverser();
101$traverser->addVisitor($visitor);
102
103$fuzzer->setTarget(function(string $input) use($lexer, $parser, $prettyPrinter, $nodeDumper, $visitor, $traverser) {
104    $stmts = $parser->parse($input);
105    $printed = $prettyPrinter->prettyPrintFile($stmts);
106
107    $visitor->setTokens($lexer->getTokens());
108    $stmts = $traverser->traverse($stmts);
109    if ($visitor->hasProblematicConstruct) {
110        return;
111    }
112
113    try {
114        $printedStmts = $parser->parse($printed);
115    } catch (PhpParser\Error $e) {
116        throw new Error("Failed to parse pretty printer output");
117    }
118
119    $visitor->setTokens($lexer->getTokens());
120    $printedStmts = $traverser->traverse($printedStmts);
121    $same = $nodeDumper->dump($stmts) == $nodeDumper->dump($printedStmts);
122    if (!$same && !preg_match('/<\?php<\?php/i', $input)) {
123        throw new Error("Result after pretty printing differs");
124    }
125});
126
127$fuzzer->setMaxLen(1024);
128$fuzzer->addDictionary(__DIR__ . '/php.dict');
129$fuzzer->setAllowedExceptions([PhpParser\Error::class]);
130