\n";
22 |
23 | $prefix = Util::getCommonPathPrefix(array_keys($fileInfos));
24 | $totalNumCovered = 0;
25 | $totalNumTotal = 0;
26 | ksort($fileInfos);
27 | foreach ($fileInfos as $path => $fileInfo) {
28 | $posToBlockIndex = array_flip($fileInfo->blockIndexToPos);
29 | ksort($posToBlockIndex);
30 |
31 | $code = file_get_contents($path);
32 | $result = '';
33 | $lastPos = 0;
34 | $numCovered = 0;
35 | $numTotal = count($posToBlockIndex);
36 | foreach ($posToBlockIndex as $pos => $blockIndex) {
37 | $result .= htmlspecialchars(\substr($code, $lastPos, $pos - $lastPos));
38 | $covered = isset($seenBlocks[$blockIndex]);
39 | $numCovered += $covered;
40 | $color = $covered ? "green" : "red";
41 | $result .= '' . $code[$pos] . '';
42 | $lastPos = $pos + 1;
43 | }
44 | $result .= htmlspecialchars(\substr($code, $lastPos));
45 | $result .= '
';
46 |
47 | $shortPath = str_replace($prefix, '', $path);
48 | $outPath = $this->outDir . '/' . $shortPath . '.html';
49 | @mkdir(dirname($outPath), 0777, true);
50 | file_put_contents($outPath, $result);
51 |
52 | $overview .= <<
54 | $shortPath |
55 | $numCovered/$numTotal |
56 |
57 | HTML;
58 |
59 | $totalNumCovered += $numCovered;
60 | $totalNumTotal += $numTotal;
61 | }
62 |
63 | $overview .= "Total | $totalNumCovered/$totalNumTotal |
";
64 | $overview .= '
';
65 | file_put_contents($this->outDir . '/index.html', $overview);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/DictionaryParser.php:
--------------------------------------------------------------------------------
1 |
8 | */
9 | public function parse(string $code): array {
10 | $lines = explode("\n", $code);
11 | $dictionary = [];
12 | foreach ($lines as $idx => $line) {
13 | $line = trim($line);
14 | if (\strlen($line) === 0) {
15 | continue;
16 | }
17 |
18 | if ($line[0] === '#') {
19 | continue;
20 | }
21 |
22 | $regex = '/(?:\w+=)?"([^"\\\\]*(?:(?:\\\\(?:["\\\\]|x[0-9a-zA-Z]{2}))[^"\\\\]*)*)"/';
23 | if (!preg_match($regex, $line, $match)) {
24 | throw new \Exception('Line ' . ($idx+1) . ' of dictionary is invalid');
25 | }
26 |
27 | $escapedDictEntry = $match[1];
28 | $dictionary[] = preg_replace_callback('/\\\\(["\\\\]|x[0-9a-zA-Z]{2})/', function($match) {
29 | $escape = $match[1];
30 | if ($escape[0] === 'x') {
31 | return chr(hexdec(substr($escape, 1)));
32 | }
33 | return $escape;
34 | }, $escapedDictEntry);
35 | }
36 | return $dictionary;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Fuzzer.php:
--------------------------------------------------------------------------------
1 | */
32 | private array $fileInfos = [];
33 | private ?string $lastInput = null;
34 |
35 | private int $runs = 0;
36 | private int $lastInterestingRun = 0;
37 | private int $initialFeatures;
38 | private float $startTime;
39 | private int $mutationDepthLimit = 5;
40 | private int $maxRuns = PHP_INT_MAX;
41 | private int $lenControlFactor = 200;
42 | private int $timeout = 3;
43 |
44 | // Counts all crashes, including duplicates
45 | private int $crashes = 0;
46 | private int $maxCrashes = 100;
47 |
48 | public function __construct() {
49 | $this->outputDir = getcwd();
50 | $this->instrumentor = new Instrumentor(
51 | FuzzingContext::class, PhpVersion::getHostVersion());
52 | $this->rng = new RNG();
53 | $this->config = new Config();
54 | $this->mutator = new Mutator($this->rng, $this->config->dictionary);
55 | $this->corpus = new Corpus();
56 |
57 | // Instrument everything apart from our src/ directory.
58 | $fileFilter = FileFilter::createAllWhitelisted();
59 | $fileFilter->addBlackList(__DIR__);
60 | // Only intercept file:// streams. Interception of phar:// streams may run into
61 | // incorrect stat() handling during path resolution in PHP.
62 | $protocols = ['file'];
63 | $this->interceptor = new Interceptor(function(string $path) use($fileFilter) {
64 | if (!$fileFilter->test($path)) {
65 | return null;
66 | }
67 |
68 | $code = file_get_contents($path);
69 | $fileInfo = new FileInfo();
70 | $instrumentedCode = $this->instrumentor->instrument($code, $fileInfo);
71 | $this->fileInfos[$path] = $fileInfo;
72 | return $instrumentedCode;
73 | }, $protocols);
74 | }
75 |
76 | private function loadTarget(string $path): void {
77 | if (!is_file($path)) {
78 | throw new FuzzerException('Target "' . $path . '" does not exist');
79 | }
80 |
81 | $this->targetPath = $path;
82 | $this->startInstrumentation();
83 | // Unbind $this and make config available as $config variable.
84 | (static function(Config $config) use($path) {
85 | $fuzzer = $config; // For backwards compatibility.
86 | require $path;
87 | })($this->config);
88 | }
89 |
90 | public function setCorpusDir(string $path): void {
91 | $this->corpusDir = $path;
92 | if (!is_dir($this->corpusDir)) {
93 | throw new FuzzerException('Corpus directory "' . $this->corpusDir . '" does not exist');
94 | }
95 | }
96 |
97 | public function setCoverageDir(string $path): void {
98 | $this->coverageDir = $path;
99 | }
100 |
101 | public function startInstrumentation(): void {
102 | $this->interceptor->setUp();
103 | }
104 |
105 | public function fuzz(): void {
106 | if (!$this->loadCorpus()) {
107 | return;
108 | }
109 |
110 | // Start with a short maximum length, increase if we fail to make progress.
111 | $maxLen = min($this->config->maxLen, max(4, $this->corpus->getMaxLen()));
112 |
113 | // Don't count runs while loading the corpus.
114 | $this->runs = 0;
115 | $this->startTime = microtime(true);
116 | while ($this->runs < $this->maxRuns) {
117 | $origEntry = $this->corpus->getRandomEntry($this->rng);
118 | $input = $origEntry !== null ? $origEntry->input : "";
119 | $crossOverEntry = $this->corpus->getRandomEntry($this->rng);
120 | $crossOverInput = $crossOverEntry !== null ? $crossOverEntry->input : null;
121 | for ($m = 0; $m < $this->mutationDepthLimit; $m++) {
122 | $input = $this->mutator->mutate($input, $maxLen, $crossOverInput);
123 | $entry = $this->runInput($input);
124 | if ($entry->crashInfo) {
125 | if ($this->corpus->addCrashEntry($entry)) {
126 | $entry->storeAtPath($this->outputDir . '/crash-' . $entry->hash . '.txt');
127 | $this->printCrash('CRASH', $entry);
128 | } else {
129 | echo "DUPLICATE CRASH\n";
130 | }
131 | if (++$this->crashes >= $this->maxCrashes) {
132 | echo "Maximum of {$this->maxCrashes} crashes reached, aborting\n";
133 | return;
134 | }
135 | break;
136 | }
137 |
138 | $this->corpus->computeUniqueFeatures($entry);
139 | if ($entry->uniqueFeatures) {
140 | $this->corpus->addEntry($entry);
141 | $entry->storeAtPath($this->corpusDir . '/' . $entry->hash . '.txt');
142 |
143 | $this->lastInterestingRun = $this->runs;
144 | $this->printAction('NEW', $entry);
145 | break;
146 | }
147 |
148 | if ($origEntry !== null &&
149 | \strlen($entry->input) < \strlen($origEntry->input) &&
150 | $entry->hasAllUniqueFeaturesOf($origEntry)
151 | ) {
152 | // Preserve unique features of original entry,
153 | // even if they are not unique anymore at this point.
154 | $entry->uniqueFeatures = $origEntry->uniqueFeatures;
155 | if ($this->corpus->replaceEntry($origEntry, $entry)) {
156 | $entry->storeAtPath($this->corpusDir . '/' . $entry->hash . '.txt');
157 | }
158 | unlink($origEntry->path);
159 |
160 | $this->lastInterestingRun = $this->runs;
161 | $this->printAction('REDUCE', $entry);
162 | break;
163 | }
164 | }
165 |
166 | if ($maxLen < $this->config->maxLen) {
167 | // Increase max length if we haven't made progress in a while.
168 | $logMaxLen = (int) log($maxLen, 2);
169 | if (($this->runs - $this->lastInterestingRun) > $this->lenControlFactor * $logMaxLen) {
170 | $maxLen = min($this->config->maxLen, $maxLen + $logMaxLen);
171 | $this->lastInterestingRun = $this->runs;
172 | }
173 | }
174 | }
175 | }
176 |
177 | private function isAllowedException(\Throwable $e): bool {
178 | foreach ($this->config->allowedExceptions as $allowedException) {
179 | if ($e instanceof $allowedException) {
180 | return true;
181 | }
182 | }
183 | return false;
184 | }
185 |
186 | private function runInput(string $input): CorpusEntry {
187 | $this->runs++;
188 | if (\extension_loaded('pcntl')) {
189 | \pcntl_alarm($this->timeout);
190 | }
191 |
192 | // Remember the last input in case PHP generates a fatal error.
193 | $this->lastInput = $input;
194 | FuzzingContext::reset();
195 | $crashInfo = null;
196 | try {
197 | ($this->config->target)($input);
198 | } catch (\ParseError $e) {
199 | echo "PARSE ERROR $e\n";
200 | echo "INSTRUMENTATION BROKEN? -- ABORTING";
201 | exit(-1);
202 | } catch (\Throwable $e) {
203 | if (!$this->isAllowedException($e)) {
204 | $crashInfo = (string) $e;
205 | }
206 | }
207 |
208 | $features = $this->edgeCountsToFeatures(FuzzingContext::$edges);
209 | return new CorpusEntry($input, $features, $crashInfo);
210 | }
211 |
212 | /**
213 | * @param array