Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement multi-core fuzzer #348

Merged
merged 6 commits into from May 17, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
63 changes: 63 additions & 0 deletions fuzzers/corpus_based_fuzzer.ts
@@ -0,0 +1,63 @@
import { FuzzCase, IFuzzer } from 'fuzzer';
import { mutateCharactersInPlace, mutateString, rand, randomLanguage } from 'mutators';
import { sync } from 'slimdom-sax-parser';

/**
* Implements a corpus-based {@link Fuzzer}.
*/
export default class CorpusBasedFuzzer implements IFuzzer {
private corpus: string[];
private documentNode: any;

constructor(corpus: string[]) {
this.corpus = corpus;
}

globalInit(): void {
this.documentNode = sync(
'<xml> \
<title>xpath.playground.fontoxml.com</title> \
<summary>This is a learning tool for XML, XPath and XQuery.</summary> \
<tips> \
<tip id="edit">You can edit everything on the left</tip> \
<tip id="examples">You can access more examples from a menu in the top right</tip> \
<tip id="permalink">Another button there lets you share your test using an URL</tip> \
</tips> \
</xml>'
);
}

isExpectedError(error: Error): boolean {
// Not interested in static errors, we're looking for crashes
if (error.message.startsWith('XPST')) {
return true;
}

// Not interested in type errors, we're looking for crashes
if (error.message.startsWith('XPTY')) {
return true;
}

// Not interested in function errors, we're looking for crashes
if (error.message.startsWith('FORG')) {
return true;
}

// Did not expect error
return false;
}

prepareCase(): FuzzCase {
// Select an expression from the corpus
let expression = this.corpus[rand(this.corpus.length)];

// Mutate the input using a simple character mutation
expression = mutateString(expression);
expression = mutateCharactersInPlace(expression);

// Select a random language
const language = randomLanguage();

return new FuzzCase(expression, language, this.documentNode);
}
}
156 changes: 156 additions & 0 deletions fuzzers/engine.ts
@@ -0,0 +1,156 @@
import { IFuzzer } from 'fuzzer';
import os from 'os';
import readline from 'readline';
import { isMainThread, parentPort, Worker as ThreadWorker, workerData } from 'worker_threads';

enum WorkerMessageTypes {
Online = 'online',
Progress = 'progress',
Crash = 'crash',
}

/**
* Multi-threaded fuzzing engine.
*/
export default class Engine<TFuzzer extends IFuzzer> {
run(fuzzer: TFuzzer, filename: string): void {
if (isMainThread) {
this.run_main(filename);
} else {
this.run_worker(fuzzer);
}
}

private run_main(filename: string): void {
devatwork marked this conversation as resolved.
Show resolved Hide resolved
let workersOnline = 0;
let totalCases = 0;
const uniqueStacks = new Set();

// Start all the workers
const numOfCpus = os.cpus().length;
process.stdout.write(`Main thread, launching ${numOfCpus} workers\n`);
for (const tid of Array(numOfCpus).keys()) {
const worker = new ThreadWorker(
`
require('tsconfig-paths/register');
require('ts-node/register');
require(require('worker_threads').workerData.runThisFileInTheWorker);
`,
{
env: {
TS_NODE_PROJECT: './fuzzers/tsconfig.json'
},
eval: true,
workerData: {
runThisFileInTheWorker: filename,
tid
}
}
);
worker.on('message', msg => {
switch (msg.type) {
case WorkerMessageTypes.Online: {
workersOnline += 1;
break;
}
case WorkerMessageTypes.Progress: {
totalCases += msg.totalCases;
break;
}
case WorkerMessageTypes.Crash: {
// Not interested in duplicate stack traces
// Must do this again because workers do not coordinate
if (uniqueStacks.has(msg.stack)) {
return;
}
uniqueStacks.add(msg.stack);

// Print the error
process.stdout.write(
`\n\n!!! \x1b[31mCrash found\x1b[0m !!!\nSelector: ${msg.selector}\nLanguage: ${msg.language}\n${msg.stack}\n\n`
);
break;
}
}
});
worker.on('error', err => {
process.stderr.write(`Unexpected error of ${tid} with error ${err}\n`);
});
worker.on('exit', code => {
process.stderr.write(`Unexpected exit of ${tid} with exit code ${code}\n`);
});
}

// Print stats
let startTime = process.hrtime();
setInterval(() => {
// Only start the timer when at least one worker is online
if (workersOnline === 0) {
startTime = process.hrtime();
}

const elapsed = process.hrtime(startTime);
const fcps = (totalCases / elapsed[0]).toFixed(2);
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0, null);
process.stdout.write(
`\x1b[0m\t[Workers: \x1b[32m${workersOnline}\x1b[0m] [Total cases: \x1b[32m${totalCases}\x1b[0m] [fcps: \x1b[32m${fcps}\x1b[0m] [Unique crashes: \x1b[31m${uniqueStacks.size}\x1b[0m]`
);
}, 1000);
}

private run_worker(fuzzer: TFuzzer): void {
// Init the fuzzer
fuzzer.globalInit();

// Report worker as online
const tid = workerData.tid;
parentPort.postMessage({
type: WorkerMessageTypes.Online,
tid
});

let deltaCases = 0;
const uniqueStacks = new Set();
while (true) {
// Report progress
if (deltaCases === 1000) {
parentPort.postMessage({
type: WorkerMessageTypes.Progress,
totalCases: deltaCases,
tid
});
deltaCases = 0;
}
deltaCases += 1;

// Init case
const fuzzCase = fuzzer.prepareCase();

// Execute the case
try {
fuzzCase.run();
} catch (error) {
// Test if this is an expected error
if (fuzzer.isExpectedError(error)) {
continue;
}

// Not interested in duplicate stack traces
if (uniqueStacks.has(error.stack)) {
continue;
}
uniqueStacks.add(error.stack);

// Report the error
parentPort.postMessage({
type: WorkerMessageTypes.Crash,
selector: fuzzCase.selector,
language: fuzzCase.language,
stack: error.stack,
tid
});
}
}
}
}