target.php 4.6 KB

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