├── .editorconfig ├── .gitignore ├── LICENSE ├── demo.gif ├── etc ├── tsconfig.base.json └── tslint.base.json ├── lerna.json ├── package.json ├── packages ├── js-fuzz-cli │ ├── .npmignore │ ├── mocha.opts │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── reporters │ │ │ ├── index.ts │ │ │ └── json.ts │ ├── tsconfig.json │ └── tslint.json └── js-fuzz-core │ ├── .npmignore │ ├── mocha.opts │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── Cluster.ts │ ├── Corpus.ts │ ├── Math.ts │ ├── Protocol.ts │ ├── Serializer.ts │ ├── Stats.ts │ ├── Worker.ts │ ├── artifact-set │ │ ├── disk-artifact-set.ts │ │ ├── index.ts │ │ └── memory-artifact-set.ts │ ├── cluster-factory.ts │ ├── dependencies.ts │ ├── input │ │ ├── corpus.ts │ │ ├── input-generator.ts │ │ └── input.ts │ ├── instrumentation │ │ ├── coverage-instrumentor.test.ts │ │ ├── coverage-instrumentor.ts │ │ ├── hook-manager.ts │ │ ├── literal-extractor.test.ts │ │ └── literal-extractor.ts │ ├── mutations │ │ ├── algorithms │ │ │ ├── algorithms.test.ts │ │ │ ├── ascii-digit-replace-mutator.ts │ │ │ ├── ascii-number-replace-mutator.ts │ │ │ ├── copy-range-mutator.ts │ │ │ ├── duplicate-range-mutator.ts │ │ │ ├── flip-bit-mutator.ts │ │ │ ├── index.ts │ │ │ ├── insert-range-mutator.ts │ │ │ ├── random-byte-mutator.ts │ │ │ ├── remove-range-mutator.ts │ │ │ ├── replace-interesting-16-mutator.ts │ │ │ ├── replace-interesting-32-mutator.ts │ │ │ ├── replace-interesting-8-mutator.ts │ │ │ ├── swap-byte-mutator.ts │ │ │ ├── uint16-increment-mutator.ts │ │ │ ├── uint32-increment-mutator.ts │ │ │ ├── uint8-increment-mutator.ts │ │ │ └── util.ts │ │ ├── interesting-bits.ts │ │ └── mutator.ts │ ├── options.ts │ ├── protocol │ │ ├── fuzz.proto │ │ ├── protocol.ts │ │ ├── rw-buffer.test.ts │ │ ├── rw-buffer.ts │ │ └── types.ts │ └── runtime │ │ ├── coverage-hash.ts │ │ ├── inliner.ts │ │ ├── runtime-service-collection.ts │ │ └── runtime.ts │ ├── test │ ├── bench │ │ ├── hashStore.bench.js │ │ └── instrumentation.bench.js │ ├── fixture │ │ └── instrument │ │ │ ├── if-statements.after.txt │ │ │ ├── if-statements.before.txt │ │ │ ├── if-statements.literals.json │ │ │ ├── json2.after.txt │ │ │ ├── json2.before.txt │ │ │ ├── json2.literals.json │ │ │ ├── switch-statements.after.txt │ │ │ ├── switch-statements.before.txt │ │ │ ├── switch-statements.literals.json │ │ │ ├── ternary-statements.after.txt │ │ │ ├── ternary-statements.before.txt │ │ │ └── ternary-statements.literals.json │ └── util.js │ ├── tsconfig.json │ ├── tshook.js │ └── tslint.json ├── readme.md ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | end_of_line = lf 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (http://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directories 35 | node_modules 36 | jspm_packages 37 | 38 | # Optional npm cache directory 39 | .npm 40 | 41 | # Optional eslint cache 42 | .eslintcache 43 | 44 | # Optional REPL history 45 | .node_repl_history 46 | 47 | # Output of 'npm pack' 48 | *.tgz 49 | 50 | # Yarn Integrity file 51 | .yarn-integrity 52 | 53 | # for now, ignore the demo 54 | packages/js-fuzz-cli/demo 55 | 56 | **/dist 57 | **/coverage 58 | **/fuzz-output 59 | /.vscode 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Connor Peet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | ====================== 24 | 25 | Segments of this code build upon code previously written in go-fuzz and the 26 | American Fuzzy Lop, which are licensed under the Apache 2.0 license: 27 | https://github.com/dvyukov/go-fuzz/blob/master/LICENSE 28 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connor4312/js-fuzz/cb19be3062274dfc62d0f81e18c16b0b6f3f0006/demo.gif -------------------------------------------------------------------------------- /etc/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "forceConsistentCasingInFileNames": true, 4 | "sourceMap": true, 5 | "declaration": true, 6 | "noImplicitReturns": true, 7 | "noUnusedLocals": true, 8 | "noUnusedParameters": true, 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "moduleResolution": "node", 12 | "newLine": "LF", 13 | "module": "commonjs", 14 | "target": "esnext", 15 | "lib": ["es6", "es7"], 16 | "types": [ 17 | "node", 18 | "mocha" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /etc/tslint.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tslint", 3 | "extends": ["tslint:recommended", "tslint-config-prettier"], 4 | "rules": { 5 | "variable-name": false, 6 | "no-unused-expression": false, 7 | "object-literal-sort-keys": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "0.1.0" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@c4312/js-fuzz", 3 | "version": "0.1.0", 4 | "private": true, 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "lerna run build", 8 | "test": "lerna run test --stream --concurrency 1", 9 | "fmt": "lerna run fmt --parallel", 10 | "postinstall": "lerna bootstrap" 11 | }, 12 | "author": "Connor Peet ", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/chai": "^4.1.7", 16 | "@types/mocha": "^5.2.7", 17 | "@types/node": "^12.6.2", 18 | "chai": "^4.2.0", 19 | "lerna": "^3.15.0", 20 | "mocha": "^6.1.4", 21 | "npm-run-all": "^4.1.5", 22 | "rimraf": "^2.6.3", 23 | "ts-node": "^8.3.0", 24 | "tslint": "^5.18.0", 25 | "tslint-config-prettier": "^1.18.0", 26 | "typescript": "^3.5.3" 27 | }, 28 | "prettier": { 29 | "trailingComma": "all", 30 | "singleQuote": true, 31 | "printWidth": 100, 32 | "tabWidth": 2 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/js-fuzz-cli/.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/dist 3 | -------------------------------------------------------------------------------- /packages/js-fuzz-cli/mocha.opts: -------------------------------------------------------------------------------- 1 | --watch-extensions ts 2 | --require source-map-support/register 3 | --require ts-node/register 4 | --recursive 5 | src/**/*.test.ts 6 | -------------------------------------------------------------------------------- /packages/js-fuzz-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@c4312/js-fuzz-cli", 3 | "version": "0.1.0", 4 | "description": "An AFL-inspired genetic fuzz tester for JavaScript (command line)", 5 | "keywords": [ 6 | "afl", 7 | "american", 8 | "fuzzy", 9 | "lop", 10 | "fuzzer", 11 | "tester", 12 | "fuzz" 13 | ], 14 | "preferGlobal": true, 15 | "bin": { 16 | "js-fuzz": "dist/index.js" 17 | }, 18 | "author": "Connor Peet ", 19 | "homepage": "https://github.com/connor4312/js-fuzz/tree/master/packages/js-fuzz-cli#readme", 20 | "license": "MIT", 21 | "main": "dist/index.js", 22 | "dependencies": { 23 | "@c4312/js-fuzz-core": "*", 24 | "blessed-contrib": "^4.6.5", 25 | "blessed": "^0.1.81", 26 | "filesize": "^3.3.0", 27 | "joi": "^14.3.1", 28 | "parse-duration": "^0.1.1", 29 | "pretty-ms": "^2.1.0", 30 | "yargs": "^13.2.4" 31 | }, 32 | "devDependencies": { 33 | "@types/joi": "^14.3.3", 34 | "@types/yargs": "^13.0.0" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/connor4312/js-fuzz.git" 39 | }, 40 | "scripts": { 41 | "prepare": "npm run build", 42 | "build": "npm run clean && node-ex tsc", 43 | "clean": "node-ex rimraf coverage doc lib", 44 | "test": "node-ex npm-run-all --silent --parallel test:lint test:unit", 45 | "test:lint": "node-ex tslint --type-check --project tsconfig.json '{src,test}/**/*.ts'", 46 | "test:unit": "node-ex mocha --opts mocha.opts" 47 | }, 48 | "bugs": { 49 | "url": "https://github.com/connor4312/js-fuzz/issues" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/js-fuzz-cli/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | process.env.TERM = 'windows-ansi'; // hack for Cygwin/conemu 6 | 7 | import { Instrumenter, ClusterFactory, StatType } from 'js-fuzz-core'; 8 | import * as yargs from 'yargs'; 9 | import * as path from 'path'; 10 | import { cpus } from 'os'; 11 | import { readFileSync } from 'fs'; 12 | import { ReporterType, reporters } from './reporters'; 13 | 14 | const exit = (code: number) => { 15 | if (!process.stdout.write('')) { 16 | process.stdout.once('drain', () => process.exit(code)); 17 | } else { 18 | process.exit(code); 19 | } 20 | } 21 | 22 | yargs 23 | .env('JS_FUZZ') 24 | .command( 25 | 'fuzz ', 26 | 'Fuzz a file', 27 | builder => 28 | builder 29 | .positional('file', { 30 | describe: 'File to fuzz', 31 | type: 'string', 32 | required: true, 33 | }) 34 | .option('exclude', { 35 | alias: 'e', 36 | describe: 'List of package regexes that will be excluded from coverage analysis', 37 | type: 'array', 38 | default: [] as string[], 39 | }) 40 | .option('workers', { 41 | alias: 'w', 42 | default: cpus().length, 43 | describe: 'Number of worker processes to run', 44 | type: 'count', 45 | }) 46 | .option('timeout', { 47 | alias: 't', 48 | default: 100, 49 | describe: 'Number of milliseconds after which to fail tests if it does not return', 50 | type: 'count', 51 | }) 52 | .option('reporter', { 53 | alias: 'r', 54 | describe: 'Output formatter', 55 | type: 'string', 56 | choices: Object.values(ReporterType), 57 | default: ReporterType.Json, 58 | }), 59 | argv => { 60 | const c = new ClusterFactory({ 61 | target: path.resolve(process.cwd(), argv.file!), 62 | exclude: argv.exclude, 63 | workers: argv.workers, 64 | timeout: argv.timeout, 65 | }).start(); 66 | 67 | const reporter = reporters[argv.reporter](); 68 | process.once('SIGINT', () => c.shutdown('SIGINT')); 69 | process.once('SIGTERM', () => c.shutdown('SIGTERM')); 70 | 71 | c.stats.subscribe(stats => { 72 | reporter.write(stats); 73 | 74 | if (stats.type === StatType.FatalError) { 75 | exit(1); 76 | } 77 | 78 | if (stats.type === StatType.ShutdownComplete) { 79 | exit(0); 80 | } 81 | }); 82 | }, 83 | ) 84 | .command( 85 | 'instrument ', 86 | 'Prints the instrumented source of the given file', 87 | builder => 88 | builder 89 | .positional('file', { 90 | describe: 'File to instrument.', 91 | type: 'string', 92 | default: '-', 93 | }) 94 | .option('hashBits', { 95 | describe: 'Hashmap size in bits', 96 | type: 'number', 97 | default: 16, 98 | }) 99 | .options('deterministicKeys', { 100 | describe: 'Use deterministic keys for branch tagging', 101 | type: 'boolean', 102 | default: false, 103 | }) 104 | .options('hashName', { 105 | describe: 'Global variable name to store the hashmap in', 106 | type: 'string', 107 | default: '__coverage__', 108 | }), 109 | argv => { 110 | const contents = readFileSync(argv.file === '-' ? 0 : argv.file, 'utf-8'); 111 | const start = Date.now(); 112 | const { code, literals } = new Instrumenter().instrument(contents); 113 | const duration = Date.now() - start; 114 | process.stdout.write(code); 115 | process.stderr.write(`Duration: ${duration}ms\r\n`); 116 | process.stderr.write(`Literals: ${[...literals].map(l => JSON.stringify(l)).join(', ')}\r\n`); 117 | }, 118 | ) 119 | .strict() 120 | .help() 121 | .parse(); 122 | -------------------------------------------------------------------------------- /packages/js-fuzz-cli/src/reporters/index.ts: -------------------------------------------------------------------------------- 1 | import { Stat } from "js-fuzz-core"; 2 | import { JsonReporter } from "./json"; 3 | 4 | /** 5 | * Type that prettifies output information for the console. 6 | */ 7 | export interface IReporter { 8 | /** 9 | * Updates stats information from the fuzzer. 10 | */ 11 | write(stats: Stat): void; 12 | 13 | /** 14 | * Shuts down the reporter. 15 | */ 16 | close(): void; 17 | } 18 | 19 | export enum ReporterType { 20 | Json = 'json', 21 | } 22 | 23 | export const reporters: { [K in ReporterType]: () => IReporter } = { 24 | json: () => new JsonReporter(), 25 | } 26 | -------------------------------------------------------------------------------- /packages/js-fuzz-cli/src/reporters/json.ts: -------------------------------------------------------------------------------- 1 | import { IReporter } from '.'; 2 | import { Stat, StatType, WorkResult } from 'js-fuzz-core'; 3 | 4 | /** 5 | * Reporter that writes JSON to standard out. 6 | */ 7 | export class JsonReporter implements IReporter { 8 | private runs = 0; 9 | private crashers = 0; 10 | private printInterval?: NodeJS.Timer; 11 | 12 | public write(stats: Stat) { 13 | if (stats.type !== StatType.WorkerExecuted) { 14 | this.writeJson(stats); 15 | return; 16 | } 17 | 18 | if (this.printInterval === undefined) { 19 | this.setupPrintLoop(); 20 | } 21 | 22 | this.runs++; 23 | if (stats.summary.result === WorkResult.Error) { 24 | this.crashers++; 25 | } 26 | } 27 | 28 | public close() { 29 | if (this.printInterval !== undefined) { 30 | clearInterval(this.printInterval); 31 | } 32 | } 33 | 34 | private setupPrintLoop() { 35 | let lastPrint = Date.now(); 36 | let lastRuns = this.runs; 37 | 38 | this.printInterval = setInterval(() => { 39 | const now = Date.now(); 40 | const runsPerSecond = Math.round(((this.runs - lastRuns) / (now - lastPrint)) * 1000); 41 | this.writeJson({ type: 'summary', runs: this.runs, crashers: this.crashers, runsPerSecond }); 42 | lastRuns = this.runs; 43 | lastPrint = now; 44 | }, 1000); 45 | } 46 | 47 | protected writeJson(data: object) { 48 | process.stdout.write(JSON.stringify(data) + '\r\n'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/js-fuzz-cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../etc/tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/js-fuzz-cli/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tslint", 3 | "extends": ["../../etc/config/tslint.base.json"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/dist 3 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/mocha.opts: -------------------------------------------------------------------------------- 1 | --watch-extensions ts 2 | --require source-map-support/register 3 | --require tshook.js 4 | --recursive 5 | src/**/*.test.ts 6 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@c4312/js-fuzz-core", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@protobufjs/aspromise": { 8 | "version": "1.1.2", 9 | "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", 10 | "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" 11 | }, 12 | "@protobufjs/base64": { 13 | "version": "1.1.2", 14 | "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", 15 | "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" 16 | }, 17 | "@protobufjs/codegen": { 18 | "version": "2.0.4", 19 | "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", 20 | "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" 21 | }, 22 | "@protobufjs/eventemitter": { 23 | "version": "1.1.0", 24 | "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", 25 | "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" 26 | }, 27 | "@protobufjs/fetch": { 28 | "version": "1.1.0", 29 | "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", 30 | "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", 31 | "requires": { 32 | "@protobufjs/aspromise": "^1.1.1", 33 | "@protobufjs/inquire": "^1.1.0" 34 | } 35 | }, 36 | "@protobufjs/float": { 37 | "version": "1.0.2", 38 | "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", 39 | "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" 40 | }, 41 | "@protobufjs/inquire": { 42 | "version": "1.1.0", 43 | "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", 44 | "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" 45 | }, 46 | "@protobufjs/path": { 47 | "version": "1.1.2", 48 | "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", 49 | "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" 50 | }, 51 | "@protobufjs/pool": { 52 | "version": "1.1.0", 53 | "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", 54 | "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" 55 | }, 56 | "@protobufjs/utf8": { 57 | "version": "1.1.0", 58 | "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", 59 | "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" 60 | }, 61 | "@types/escodegen": { 62 | "version": "0.0.6", 63 | "resolved": "https://registry.npmjs.org/@types/escodegen/-/escodegen-0.0.6.tgz", 64 | "integrity": "sha1-UjCpznluBCzabwhtvxnyLqMwZZw=", 65 | "dev": true 66 | }, 67 | "@types/esprima": { 68 | "version": "4.0.2", 69 | "resolved": "https://registry.npmjs.org/@types/esprima/-/esprima-4.0.2.tgz", 70 | "integrity": "sha512-DKqdyuy7Go7ir6iKhZ0jUvgt/h9Q5zb9xS+fLeeXD2QSHv8gC6TimgujBBGfw8dHrpx4+u2HlMv7pkYOOfuUqg==", 71 | "dev": true, 72 | "requires": { 73 | "@types/estree": "*" 74 | } 75 | }, 76 | "@types/estraverse": { 77 | "version": "0.0.6", 78 | "resolved": "https://registry.npmjs.org/@types/estraverse/-/estraverse-0.0.6.tgz", 79 | "integrity": "sha1-Zp9833KreX5hJfjQD+0z1M8wwiE=", 80 | "dev": true, 81 | "requires": { 82 | "@types/estree": "*" 83 | } 84 | }, 85 | "@types/estree": { 86 | "version": "0.0.39", 87 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", 88 | "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", 89 | "dev": true 90 | }, 91 | "@types/long": { 92 | "version": "4.0.0", 93 | "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz", 94 | "integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==" 95 | }, 96 | "@types/minimatch": { 97 | "version": "3.0.3", 98 | "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", 99 | "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", 100 | "dev": true 101 | }, 102 | "@types/node": { 103 | "version": "10.14.12", 104 | "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.12.tgz", 105 | "integrity": "sha512-QcAKpaO6nhHLlxWBvpc4WeLrTvPqlHOvaj0s5GriKkA1zq+bsFBPpfYCvQhLqLgYlIko8A9YrPdaMHCo5mBcpg==" 106 | }, 107 | "balanced-match": { 108 | "version": "1.0.0", 109 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 110 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 111 | }, 112 | "brace-expansion": { 113 | "version": "1.1.11", 114 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 115 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 116 | "requires": { 117 | "balanced-match": "^1.0.0", 118 | "concat-map": "0.0.1" 119 | } 120 | }, 121 | "concat-map": { 122 | "version": "0.0.1", 123 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 124 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 125 | }, 126 | "deep-is": { 127 | "version": "0.1.3", 128 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", 129 | "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" 130 | }, 131 | "escodegen": { 132 | "version": "1.11.1", 133 | "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", 134 | "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", 135 | "requires": { 136 | "esprima": "^3.1.3", 137 | "estraverse": "^4.2.0", 138 | "esutils": "^2.0.2", 139 | "optionator": "^0.8.1", 140 | "source-map": "~0.6.1" 141 | }, 142 | "dependencies": { 143 | "esprima": { 144 | "version": "3.1.3", 145 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", 146 | "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" 147 | } 148 | } 149 | }, 150 | "esprima": { 151 | "version": "4.0.1", 152 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 153 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" 154 | }, 155 | "estraverse": { 156 | "version": "4.2.0", 157 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", 158 | "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" 159 | }, 160 | "esutils": { 161 | "version": "2.0.2", 162 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", 163 | "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" 164 | }, 165 | "fast-levenshtein": { 166 | "version": "2.0.6", 167 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 168 | "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" 169 | }, 170 | "filesize": { 171 | "version": "4.1.2", 172 | "resolved": "https://registry.npmjs.org/filesize/-/filesize-4.1.2.tgz", 173 | "integrity": "sha512-iSWteWtfNcrWQTkQw8ble2bnonSl7YJImsn9OZKpE2E4IHhXI78eASpDYUljXZZdYj36QsEKjOs/CsiDqmKMJw==" 174 | }, 175 | "inversify": { 176 | "version": "5.0.1", 177 | "resolved": "https://registry.npmjs.org/inversify/-/inversify-5.0.1.tgz", 178 | "integrity": "sha512-Ieh06s48WnEYGcqHepdsJUIJUXpwH5o5vodAX+DK2JA/gjy4EbEcQZxw+uFfzysmKjiLXGYwNG3qDZsKVMcINQ==" 179 | }, 180 | "levn": { 181 | "version": "0.3.0", 182 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", 183 | "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", 184 | "requires": { 185 | "prelude-ls": "~1.1.2", 186 | "type-check": "~0.3.2" 187 | } 188 | }, 189 | "long": { 190 | "version": "4.0.0", 191 | "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", 192 | "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" 193 | }, 194 | "minimatch": { 195 | "version": "3.0.4", 196 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 197 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 198 | "requires": { 199 | "brace-expansion": "^1.1.7" 200 | } 201 | }, 202 | "optionator": { 203 | "version": "0.8.2", 204 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", 205 | "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", 206 | "requires": { 207 | "deep-is": "~0.1.3", 208 | "fast-levenshtein": "~2.0.4", 209 | "levn": "~0.3.0", 210 | "prelude-ls": "~1.1.2", 211 | "type-check": "~0.3.2", 212 | "wordwrap": "~1.0.0" 213 | } 214 | }, 215 | "parse-duration": { 216 | "version": "0.1.1", 217 | "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-0.1.1.tgz", 218 | "integrity": "sha1-ExFN3JiRwezSgANiRFVN5DZHoiY=" 219 | }, 220 | "prelude-ls": { 221 | "version": "1.1.2", 222 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", 223 | "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" 224 | }, 225 | "protobufjs": { 226 | "version": "6.8.8", 227 | "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", 228 | "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==", 229 | "requires": { 230 | "@protobufjs/aspromise": "^1.1.2", 231 | "@protobufjs/base64": "^1.1.2", 232 | "@protobufjs/codegen": "^2.0.4", 233 | "@protobufjs/eventemitter": "^1.1.0", 234 | "@protobufjs/fetch": "^1.1.0", 235 | "@protobufjs/float": "^1.0.2", 236 | "@protobufjs/inquire": "^1.1.0", 237 | "@protobufjs/path": "^1.1.2", 238 | "@protobufjs/pool": "^1.1.0", 239 | "@protobufjs/utf8": "^1.1.0", 240 | "@types/long": "^4.0.0", 241 | "@types/node": "^10.1.0", 242 | "long": "^4.0.0" 243 | } 244 | }, 245 | "rxjs": { 246 | "version": "6.5.2", 247 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz", 248 | "integrity": "sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==", 249 | "requires": { 250 | "tslib": "^1.9.0" 251 | } 252 | }, 253 | "source-map": { 254 | "version": "0.6.1", 255 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 256 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 257 | "optional": true 258 | }, 259 | "split": { 260 | "version": "1.0.1", 261 | "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", 262 | "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", 263 | "requires": { 264 | "through": "2" 265 | } 266 | }, 267 | "through": { 268 | "version": "2.3.8", 269 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 270 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" 271 | }, 272 | "tslib": { 273 | "version": "1.10.0", 274 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", 275 | "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" 276 | }, 277 | "type-check": { 278 | "version": "0.3.2", 279 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", 280 | "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", 281 | "requires": { 282 | "prelude-ls": "~1.1.2" 283 | } 284 | }, 285 | "wordwrap": { 286 | "version": "1.0.0", 287 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", 288 | "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@c4312/js-fuzz-core", 3 | "version": "0.1.0", 4 | "description": "An AFL-inspired genetic fuzz tester for JavaScript (core library)", 5 | "keywords": [ 6 | "afl", 7 | "american", 8 | "fuzzy", 9 | "lop", 10 | "fuzzer", 11 | "tester", 12 | "fuzz" 13 | ], 14 | "author": "Connor Peet ", 15 | "homepage": "https://github.com/connor4312/js-fuzz/tree/master/packages/js-fuzz-core#readme", 16 | "license": "MIT", 17 | "main": "dist/index.js", 18 | "dependencies": { 19 | "escodegen": "^1.11.1", 20 | "esprima": "^4.0.1", 21 | "estraverse": "^4.2.0", 22 | "filesize": "^4.1.2", 23 | "fs-extra": "^8.1.0", 24 | "inversify": "^5.0.1", 25 | "minimatch": "^3.0.4", 26 | "parse-duration": "^0.1.1", 27 | "protobufjs": "^6.8.8", 28 | "randexp": "^0.5.3", 29 | "reflect-metadata": "^0.1.13", 30 | "rxjs": "^6.5.2", 31 | "split": "^1.0.1" 32 | }, 33 | "devDependencies": { 34 | "@types/escodegen": "^0.0.6", 35 | "@types/esprima": "^4.0.2", 36 | "@types/estraverse": "^0.0.6", 37 | "@types/fs-extra": "^8.0.0", 38 | "@types/minimatch": "^3.0.3" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "git+https://github.com/connor4312/js-fuzz.git" 43 | }, 44 | "scripts": { 45 | "prepare": "npm run build", 46 | "build": "npm run clean && node-ex tsc && pbjs src/protocol/fuzz.proto -t static-module > dist/protocol/fuzz.js", 47 | "clean": "node-ex rimraf coverage doc lib", 48 | "test": "node-ex npm-run-all --silent --parallel test:lint test:unit", 49 | "test:lint": "node-ex tslint --type-check --project tsconfig.json '{src,test}/**/*.ts'", 50 | "test:unit": "node-ex mocha --opts mocha.opts" 51 | }, 52 | "bugs": { 53 | "url": "https://github.com/connor4312/js-fuzz/issues" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/Cluster.ts: -------------------------------------------------------------------------------- 1 | import * as cp from 'child_process'; 2 | import { EventEmitter } from 'events'; 3 | import { Subject, Observable } from 'rxjs'; 4 | 5 | import { Corpus, Input } from './Corpus'; 6 | import { IPCCall, IWorkSummary, PacketKind, Protocol } from './Protocol'; 7 | import { ISerializer } from './Serializer'; 8 | import { Stat, StatType } from './Stats'; 9 | import { IFuzzOptions } from './options'; 10 | 11 | const split = require('split'); 12 | 13 | interface IManagerOptions { 14 | timeout: number; 15 | exclude: string[]; 16 | } 17 | 18 | /** 19 | * The Manager lives inside the Cluster and manages events for a single 20 | * fuzzing worker instance. 21 | */ 22 | class Manager extends EventEmitter { 23 | private timeoutHandle!: NodeJS.Timer; 24 | 25 | constructor( 26 | private readonly proc: cp.ChildProcess, 27 | private readonly proto: Protocol, 28 | private readonly options: IManagerOptions, 29 | private readonly corpus: Corpus, 30 | ) { 31 | super(); 32 | 33 | proc.stderr.pipe(split()).on('data', (message: string) => this.emit('log', message)); 34 | 35 | proto.attach(proc.stdout, proc.stdin); 36 | 37 | proto.on('message', (msg: IPCCall) => { 38 | switch (msg.kind) { 39 | case PacketKind.Ready: 40 | this.emit('ready'); 41 | break; 42 | case PacketKind.WorkSummary: 43 | this.clearTimeout(); 44 | this.emit('workSummary', msg); 45 | break; 46 | case PacketKind.WorkCoverage: 47 | this.emit('workCoverage', msg.coverage); 48 | break; 49 | case PacketKind.FoundLiterals: 50 | this.corpus.foundLiterals(msg.literals); 51 | break; 52 | default: 53 | throw new Error(`Unknown IPC call: ${msg.kind}`); 54 | } 55 | }); 56 | 57 | proto.on('error', (err: Error) => this.emit('error', err)); 58 | proc.on('error', err => this.emit('error', err)); 59 | 60 | proc.on('exit', code => { 61 | if (code !== 0) { 62 | this.emit('error', new Error(`Worker process exited with error code ${code}`)); 63 | } 64 | 65 | this.emit('close'); 66 | }); 67 | } 68 | 69 | /** 70 | * Sends new work to the working process. 71 | */ 72 | public send(work: Buffer) { 73 | this.setTimeout(); 74 | this.proto.write({ 75 | kind: PacketKind.DoWork, 76 | input: work, 77 | }); 78 | } 79 | 80 | /** 81 | * Sends new work to the working process. 82 | */ 83 | public requestCoverage(callback: (coverage: Buffer) => void) { 84 | this.proto.write({ kind: PacketKind.RequestCoverage }); 85 | this.once('workCoverage', callback); 86 | } 87 | 88 | /** 89 | * Kills the underlying process and returns a promise that's resolved 90 | * when the process exits. The process will be forcefully terminated 91 | * if it does not terminate within the given interval. 92 | */ 93 | public kill(timeout: number = 2000): Promise { 94 | this.clearTimeout(); 95 | this.proc.kill('SIGINT'); 96 | 97 | // Ask the process politely to shut down, send SIGKILL if it doesn't 98 | // clean up in a few seconds. This can happen if the event loop is 99 | // blocked and someone was naughty and registered a SIGINT handler. 100 | const killTimeout = setTimeout(() => this.proc.kill('SIGKILL'), timeout); 101 | this.proc.removeAllListeners('error'); 102 | this.removeAllListeners('error'); 103 | this.on('error', () => { 104 | /* noop */ 105 | }); 106 | this.proc.kill(); 107 | 108 | return new Promise(resolve => { 109 | this.proc.once('exit', () => { 110 | clearTimeout(killTimeout); 111 | this.emit('close'); 112 | resolve(); 113 | }); 114 | }); 115 | } 116 | 117 | private clearTimeout() { 118 | clearTimeout(this.timeoutHandle); 119 | } 120 | 121 | private setTimeout() { 122 | this.timeoutHandle = setTimeout(() => { 123 | this.emit('timeout'); 124 | this.kill(); 125 | }, this.options.timeout); 126 | } 127 | 128 | /** 129 | * Creates a new Worker instance and returns a promise that resolves 130 | * when it has signaled it's ready. 131 | */ 132 | public static Spawn(target: string, corpus: Corpus, options: IManagerOptions): Promise { 133 | return new Promise((resolve, reject) => { 134 | const worker = new Manager( 135 | cp.spawn('node', [`${__dirname}/Worker.js`, target, JSON.stringify(options.exclude)]), 136 | new Protocol(require('./fuzz')), 137 | options, 138 | corpus, 139 | ); 140 | worker.once('ready', () => resolve(worker)); 141 | worker.once('error', (err: Error) => reject(err)); 142 | }); 143 | } 144 | } 145 | /** 146 | * The Cluster coordinates multiple child 147 | */ 148 | export class Cluster { 149 | private workers: Manager[] = []; 150 | private corpus!: Corpus; 151 | private active = true; 152 | private statsSubject = new Subject(); 153 | 154 | public get stats(): Observable { 155 | return this.statsSubject; 156 | } 157 | 158 | constructor(private readonly options: IFuzzOptions, private readonly serializer: ISerializer) { 159 | this.start(); 160 | } 161 | 162 | /** 163 | * Emits the given stat to the output stream. 164 | */ 165 | private emitStat(stat: Stat) { 166 | this.statsSubject.next(stat); 167 | } 168 | 169 | /** 170 | * Emits the fatal error to the output stream. 171 | */ 172 | private emitFatalError(error: Error | string) { 173 | if (typeof error === 'string') { 174 | error = new Error(error); 175 | } 176 | 177 | this.emitStat({ 178 | type: StatType.FatalError, 179 | details: error.stack || error.message, 180 | error: error, 181 | }); 182 | } 183 | 184 | /** 185 | * Boots the cluster. 186 | */ 187 | public start() { 188 | this.serializer 189 | .loadCorpus() 190 | .then(corpus => { 191 | this.corpus = corpus; 192 | this.emitStat({ type: StatType.SpinUp, workerCount: this.options.workers }); 193 | 194 | const todo: Promise[] = []; 195 | for (let i = 0; i < this.options.workers; i += 1) { 196 | todo.push(this.spawn()); 197 | } 198 | 199 | return Promise.all(todo); 200 | }) 201 | .then(workers => { 202 | this.workers = workers; 203 | workers.forEach((worker, i) => this.monitorWorker(worker, i)); 204 | this.emitStat({ type: StatType.WorkersReady }); 205 | }) 206 | .catch(err => this.emitFatalError(err)); 207 | } 208 | 209 | /** 210 | * Stops the server. 211 | */ 212 | public shutdown(signal: string) { 213 | this.emitStat({ type: StatType.ShutdownStart, signal }); 214 | this.active = false; 215 | 216 | Promise.all( 217 | this.workers 218 | .filter(Boolean) 219 | .map(worker => worker.kill()) 220 | .concat(this.serializer.storeCorpus(this.corpus)), 221 | ) 222 | .then(() => this.emitStat({ type: StatType.ShutdownComplete })) 223 | .catch(e => this.emitFatalError(e)); 224 | } 225 | 226 | private spawn(): Promise { 227 | return Manager.Spawn(this.options.target, this.corpus, { 228 | exclude: this.options.exclude, 229 | timeout: this.options.timeout, 230 | }); 231 | } 232 | 233 | /** 234 | * Hooks into events on the worker and reboots it if it dies. 235 | */ 236 | private monitorWorker(worker: Manager, index: number) { 237 | let lastInput: Input; 238 | let lastWork: Buffer; 239 | const sendNextPacket = () => { 240 | lastInput = this.corpus.pickWeighted(); 241 | lastWork = this.corpus.mutate(lastInput); 242 | worker.send(lastWork); 243 | }; 244 | 245 | worker.on('log', (line: string) => { 246 | this.emitStat({ type: StatType.ProgramLogLine, worker: index, line }); 247 | }); 248 | 249 | worker.on('timeout', () => { 250 | this.serializer.storeTimeout(lastWork); 251 | this.emitStat({ type: StatType.WorkerTimeout, worker: index }); 252 | }); 253 | 254 | worker.on('workSummary', (result: IWorkSummary) => { 255 | if (!this.active) { 256 | return; 257 | } 258 | 259 | const next = new Input(lastWork, lastInput.depth + 1, result); 260 | this.emitStat({ type: StatType.WorkerExecuted, summary: result }); 261 | if (!this.corpus.isInterestedIn(next)) { 262 | return sendNextPacket(); 263 | } 264 | 265 | worker.requestCoverage(() => { 266 | if (!this.active) { 267 | return; 268 | } 269 | 270 | this.corpus.put(next); 271 | this.emitStat({ type: StatType.CoverageUpdate, branches: this.corpus.size() }); 272 | if (result.error) { 273 | this.serializer.storeCrasher(next); 274 | } 275 | 276 | sendNextPacket(); 277 | }); 278 | }); 279 | 280 | worker.once('error', (err: Error) => { 281 | if (!this.active) { 282 | return; 283 | } 284 | 285 | this.emitStat({ type: StatType.WorkerErrored, error: err, worker: index }); 286 | worker.kill(); 287 | this.spawn().then(next => { 288 | this.workers[index] = next; 289 | this.monitorWorker(next, index); 290 | }); 291 | }); 292 | 293 | sendNextPacket(); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/Corpus.ts: -------------------------------------------------------------------------------- 1 | import { IWorkSummary, PacketKind, WorkResult } from './Protocol'; 2 | import { Mutator } from './mutations/mutator'; 3 | 4 | 5 | /** 6 | * The hash store saves a bunch of Buffers and provides methods to efficiently 7 | * check to see if a given buffer is in the store yet. 8 | */ 9 | export class Corpus { 10 | private store: { 11 | [hash: string]: { 12 | input: Input, 13 | indexInRunning: number, 14 | }, 15 | } = Object.create(null); 16 | 17 | private mutator = new Mutator(); 18 | private runningScore: { runningScore: number, input: Input }[] = []; 19 | private totalExecutionTime = 0; 20 | private totalBranchCoverage = 0; 21 | private literals = new Set(); 22 | public totalScore = 0; 23 | 24 | public foundLiterals(literals: ReadonlyArray) { 25 | literals = literals.filter(l => !this.literals.has(l)); 26 | if (!literals.length) { 27 | return; 28 | } 29 | 30 | this.mutator.addLiterals(literals); 31 | for (const literal of literals) { 32 | this.literals.add(literal); 33 | } 34 | } 35 | 36 | /** 37 | * Returns if we're interested in getting a full summary report for the 38 | * the given hash. 39 | */ 40 | public isInterestedIn(input: Input) { 41 | const existing = this.store[input.summary.hash]; 42 | if (!existing) { 43 | return true; 44 | } 45 | 46 | return this.scoreInput(input) > this.scoreInput(existing.input); 47 | } 48 | 49 | /** 50 | * Returns an input, weighting by its score. 51 | */ 52 | public pickWeighted(): Input { 53 | if (this.runningScore.length === 0) { 54 | return zeroInput; 55 | } 56 | 57 | const running = this.runningScore; 58 | const targetScore = Math.random() * running[running.length - 1].runningScore; 59 | 60 | let i = 0; 61 | while (running[i].runningScore < targetScore) { 62 | i += 1; 63 | } 64 | 65 | return running[i].input; 66 | } 67 | 68 | /** 69 | * Returns all inputs stored in the corpus. 70 | */ 71 | public getAllInputs(): Input[] { 72 | return this.runningScore.map(r => r.input); 73 | } 74 | 75 | /** 76 | * Stores the work summary and the coverage file. 77 | */ 78 | public put(input: Input) { 79 | let index: number; 80 | const running = this.runningScore; 81 | const existing = this.store[input.summary.hash]; 82 | const score = this.scoreInput(input); 83 | 84 | // If we have an existing record, adjust all the counters to remove 85 | // the old record and add the new one. Otherwise, just add up the 86 | // new score, coverage, and runtime. 87 | if (existing) { 88 | this.totalBranchCoverage += input.summary.coverageSize - existing.input.summary.coverageSize; 89 | this.totalExecutionTime += input.summary.runtime - existing.input.summary.runtime; 90 | 91 | const delta = score - this.scoreInput(existing.input); 92 | index = existing.indexInRunning; 93 | for (let i = index + 1; i < running.length; i += 1) { 94 | running[i].runningScore += delta; 95 | } 96 | this.totalScore += delta; 97 | } else { 98 | index = running.length; 99 | this.totalBranchCoverage += input.summary.coverageSize; 100 | this.totalExecutionTime += input.summary.runtime; 101 | this.totalScore += score; 102 | } 103 | 104 | running[index] = { 105 | runningScore: index === 0 ? score : running[index - 1].runningScore + score, 106 | input, 107 | }; 108 | 109 | this.store[input.summary.hash] = { 110 | indexInRunning: index, 111 | input, 112 | }; 113 | } 114 | 115 | /** 116 | * Returns the number of items in our store. 117 | */ 118 | public size() { 119 | return this.runningScore.length; 120 | } 121 | 122 | private scoreInput(input: Input): number { 123 | const avgTime = this.totalExecutionTime / Math.min(1, this.runningScore.length); 124 | const avgCoverage = this.totalBranchCoverage / Math.min(1, this.runningScore.length); 125 | return input.getScore(avgTime, avgCoverage); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/Math.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Rounds a uint8 up to the next higher power of two, with zero remaining at 3 | * zero. About 5x faster than Math.* ops and we abuse this function a lot. 4 | * 5 | * From the bit twiddling hacks site: 6 | * http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 7 | */ 8 | export function roundUint8ToNextPowerOfTwo(value: number): number { 9 | value -= 1; 10 | value |= value >>> 1; 11 | value |= value >>> 2; 12 | value |= value >>> 4; 13 | value += 1; 14 | return value; 15 | } 16 | 17 | /** 18 | * Max int32 value. 19 | */ 20 | export const maxInt32 = 2147483647; 21 | 22 | let rng = Math.random; 23 | 24 | /** 25 | * Resets the random number generator to use. For use in testing. 26 | */ 27 | export function resetRandomNumberGenerator() { 28 | rng = Math.random; 29 | } 30 | 31 | /** 32 | * Sets the random number generator to use. For use in testing. 33 | */ 34 | export function setRandomNumberGenerator(generator: () => number) { 35 | rng = generator; 36 | } 37 | 38 | /** 39 | * Returns a random integer in the range [0, max), or [min, max) if two 40 | * arguments are provided. 41 | */ 42 | export function randn(max: number): number; 43 | export function randn(min: number, max: number): number; 44 | export function randn(a: number, b?: number): number { 45 | if (b === undefined) { 46 | return Math.floor(rng() * a); 47 | } 48 | 49 | return Math.floor(rng() * (b - a)) + a; 50 | } 51 | 52 | /** 53 | * Choses a random value from the array and returns it. 54 | */ 55 | export function pickOne(arr: ReadonlyArray): T { 56 | return arr[randn(arr.length)]; 57 | } 58 | 59 | /** 60 | * Creates an RNG. For use in testing. 61 | * @see https://en.wikipedia.org/wiki/Xorshift 62 | */ 63 | export function createRandomNumberGenerator(seed: number) { 64 | return () => { 65 | seed ^= seed << 13; 66 | seed ^= seed >> 7; 67 | seed ^= seed << 17; 68 | return Math.abs(seed) / maxInt32; 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/Protocol.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connor4312/js-fuzz/cb19be3062274dfc62d0f81e18c16b0b6f3f0006/packages/js-fuzz-core/src/Protocol.ts -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/Serializer.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'crypto'; 2 | import { existsSync, mkdirSync, readdir, readFile, writeFile } from 'fs'; 3 | 4 | import { Corpus, Input } from './Corpus'; 5 | import { PacketKind, WorkResult } from './Protocol'; 6 | 7 | export interface ISerializer { 8 | /** 9 | * Returns that a timeout occurred as a result of the provided input. 10 | */ 11 | storeTimeout(rawInput: Buffer): Promise; 12 | 13 | /** 14 | * Store the error that occurred as a result of the input. 15 | */ 16 | storeCrasher(input: Input): Promise; 17 | 18 | /** 19 | * Serializes the corpus and saves it on the disk. 20 | */ 21 | storeCorpus(corpus: Corpus): Promise; 22 | 23 | /** 24 | * Attempts to load a corpus from its serialized version. 25 | */ 26 | loadCorpus(): Promise; 27 | } 28 | 29 | function writeFileAsync(path: string, contents: string | Buffer): Promise { 30 | return new Promise((resolve, reject) => { 31 | writeFile(path, contents, err => { 32 | if (err) { 33 | reject(err); 34 | } else { 35 | resolve(); 36 | } 37 | }); 38 | }); 39 | } 40 | 41 | function readFileAsync(path: string): Promise { 42 | return new Promise((resolve, reject) => { 43 | readFile(path, (err, contents) => { 44 | if (err) { 45 | reject(err); 46 | } else { 47 | resolve(contents); 48 | } 49 | }); 50 | }); 51 | } 52 | 53 | function ensureDir(path: string) { 54 | if (!existsSync(path)) { 55 | mkdirSync(path); 56 | } 57 | } 58 | 59 | export class FileSerializer implements ISerializer { 60 | 61 | constructor(private root: string = process.cwd()) { 62 | ensureDir(`${root}/fuzz-output`); 63 | ensureDir(this.getCrashersDir()); 64 | ensureDir(this.getCorpusDir()); 65 | } 66 | 67 | public storeTimeout(rawInput: Buffer): Promise { 68 | return this.storeCrasherFile( 69 | 'timeout', 70 | 'The process timed out when processing this input', 71 | rawInput, 72 | ); 73 | } 74 | 75 | public storeCrasher(input: Input): Promise { 76 | return this.storeCrasherFile( 77 | input.summary.hash, 78 | input.summary.error!, 79 | input.input, 80 | ); 81 | } 82 | 83 | public storeCorpus(corpus: Corpus): Promise { 84 | return Promise.all( 85 | corpus 86 | .getAllInputs() 87 | .map(input => { 88 | return writeFileAsync( 89 | `${this.getCorpusDir()}/${input.summary.hash}`, 90 | input.serialize(), 91 | ); 92 | }), 93 | ).then(() => undefined); 94 | } 95 | 96 | public loadCorpus(): Promise { 97 | const corpus = new Corpus(); 98 | 99 | return new Promise((resolve, reject) => { 100 | readdir(this.getCorpusDir(), (err, files) => { 101 | if (err) { 102 | reject(err); 103 | } else { 104 | resolve(files); 105 | } 106 | }); 107 | }).then(files => { 108 | return Promise.all( 109 | files 110 | .map(file => `${this.getCorpusDir()}/${file}`) 111 | .map(file => readFileAsync(file)), 112 | ); 113 | }) 114 | .then(contents => { 115 | contents 116 | .map(data => { 117 | try { 118 | return Input.Deserialize(data.toString('utf8')); 119 | } catch (e) { 120 | // fall through 121 | } 122 | 123 | return new Input(data, 0, { 124 | kind: PacketKind.WorkSummary, 125 | result: WorkResult.Allow, 126 | coverageSize: 0, 127 | inputLength: data.length, 128 | hash: createHash('md5').update(data).digest('hex'), 129 | runtime: Infinity, 130 | }); 131 | }) 132 | .forEach(input => corpus.put(input)); 133 | 134 | return corpus; 135 | }); 136 | } 137 | 138 | private storeCrasherFile(hash: string, error: string, rawInput: Buffer): Promise { 139 | const contents = JSON.stringify({ 140 | error, 141 | input: { 142 | utf8: rawInput.toString('utf8'), 143 | hex: rawInput.toString('hex'), 144 | base64: rawInput.toString('base64'), 145 | }, 146 | }, null, 2); 147 | 148 | return writeFileAsync(`${this.getCrashersDir()}/${hash}.json`, contents); 149 | } 150 | 151 | private getCrashersDir() { 152 | return `${this.root}/fuzz-output/crashers`; 153 | } 154 | 155 | private getCorpusDir() { 156 | return `${this.root}/fuzz-output/corpus`; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/Stats.ts: -------------------------------------------------------------------------------- 1 | import { IWorkSummary } from './Protocol'; 2 | 3 | export const enum StatType { 4 | FatalError = 'fatalError', 5 | SpinUp = 'spinUp', 6 | WorkersReady = 'workersReady', 7 | ProgramLogLine = 'programLogLine', 8 | ShutdownStart = 'shutdownStart', 9 | ShutdownComplete = 'shutdownComplete', 10 | WorkerTimeout = 'workerTimeout', 11 | WorkerExecuted = 'workerExecuted', 12 | WorkerErrored = 'workerErrored', 13 | CoverageUpdate = 'coverageUpdate', 14 | } 15 | 16 | /** 17 | * Indicates a fatal error occurred that prevents us from proceeding further. 18 | */ 19 | export interface IFatalError { 20 | type: StatType.FatalError; 21 | error?: Error; 22 | details: string; 23 | } 24 | 25 | /** 26 | * Indicates that the cluster is spinning up. 27 | */ 28 | export interface ISpinUp { 29 | type: StatType.SpinUp; 30 | workerCount: number; 31 | } 32 | 33 | /** 34 | * Indicates that all workers are ready for fuzzing. 35 | */ 36 | export interface IWorkersReady { 37 | type: StatType.WorkersReady; 38 | } 39 | 40 | /** 41 | * Indicates that a shutdown signal was received. 42 | */ 43 | export interface IShutdownStart { 44 | type: StatType.ShutdownStart; 45 | signal?: string; 46 | } 47 | 48 | /** 49 | * Indicates that all workers and resources have shut down. 50 | */ 51 | export interface IShutdownComplete { 52 | type: StatType.ShutdownComplete; 53 | } 54 | 55 | /** 56 | * Indicates that a worker timed out while executing. 57 | */ 58 | export interface IWorkerTimeout { 59 | type: StatType.WorkerTimeout; 60 | worker: number; 61 | } 62 | 63 | /** 64 | * Indicates that a worker completed work on a given input. 65 | */ 66 | export interface IWorkerExecuted { 67 | type: StatType.WorkerExecuted; 68 | summary: IWorkSummary; 69 | } 70 | 71 | /** 72 | * Indicates that a worker errored or crashed. This is _not_ expected. 73 | */ 74 | export interface IWorkerUnhandledError { 75 | type: StatType.WorkerErrored; 76 | worker: number; 77 | error: Error; 78 | } 79 | 80 | /** 81 | * Emitted when we update our coverage count 82 | */ 83 | export interface ICoverageUpdate { 84 | type: StatType.CoverageUpdate; 85 | branches: number; 86 | } 87 | 88 | /** 89 | * Emitted when the user program writes a log message. 90 | */ 91 | export interface IProgramLogLine { 92 | type: StatType.ProgramLogLine; 93 | worker: number; 94 | line: string; 95 | } 96 | 97 | /** 98 | * Type emitted from the Cluster when it has information to report. 99 | */ 100 | export type Stat = 101 | | IFatalError 102 | | ISpinUp 103 | | IWorkersReady 104 | | IShutdownStart 105 | | IShutdownComplete 106 | | IProgramLogLine 107 | | ICoverageUpdate 108 | | IWorkerUnhandledError 109 | | IWorkerExecuted 110 | | IWorkerTimeout; 111 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/Worker.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'crypto'; 2 | import { roundUint8ToNextPowerOfTwo } from './Math'; 3 | import { IModule, IPCCall, PacketKind, Protocol, WorkResult } from './Protocol'; 4 | import { injectable, inject } from 'inversify'; 5 | import * as Types from './dependencies'; 6 | import { ConverageInstrumentor } from './instrumentation/coverage-instrumentor'; 7 | 8 | /** 9 | * Evens off the statement count in the coverage into count "buckets" and 10 | * returns the sum of all buckets. 11 | */ 12 | function flattenBuckets(coverage: Buffer): [Buffer, number] { 13 | let total = 0; 14 | for (let i = 0; i < coverage.length; i += 1) { 15 | coverage[i] = roundUint8ToNextPowerOfTwo(coverage[i]); 16 | total += coverage[i]; 17 | } 18 | return [coverage, total]; 19 | } 20 | 21 | /** 22 | * Returns a relative time in microseconds. 23 | */ 24 | function getMicroTime(): number { 25 | const [s, ns] = process.hrtime(); 26 | return s * 10e6 + ns / 10e4; 27 | } 28 | 29 | /** 30 | * The worker is attached to a Cluster and runs the target script when the 31 | * manager asks for it, reporting back code coverage metrics. 32 | */ 33 | @injectable() 34 | export class Worker { 35 | /** 36 | * The module to be put under fuzzing 37 | */ 38 | private target!: IModule; 39 | private lastCoverage!: Buffer; 40 | private proto!: Protocol; 41 | 42 | constructor( 43 | private targetPath: string, 44 | @inject(Types.CoverageInstrumentor) private readonly instrumenter: ConverageInstrumentor) { 45 | } 46 | 47 | public start() { 48 | const proto = (this.proto = new Protocol(require('./fuzz'))); 49 | proto.attach(process.stdin, process.stdout); 50 | this.instrumenter.attach(); 51 | 52 | this.target = require(this.targetPath); // tslint:disable-line 53 | 54 | proto.on('message', (msg: IPCCall) => { 55 | switch (msg.kind) { 56 | case PacketKind.DoWork: 57 | this.doWork(msg.input); 58 | break; 59 | case PacketKind.RequestCoverage: 60 | proto.write({ 61 | kind: PacketKind.WorkCoverage, 62 | coverage: this.lastCoverage, 63 | }); 64 | break; 65 | default: 66 | throw new Error(`Unknown IPC call: ${msg.kind}`); 67 | } 68 | }); 69 | 70 | proto.on('error', (err: Error) => { 71 | console.error(err); 72 | process.exit(1); 73 | }); 74 | 75 | proto.write({ kind: PacketKind.Ready }); 76 | } 77 | 78 | private runFuzz(input: Buffer, callback: (err: any, res: WorkResult) => void): void { 79 | let called = false; 80 | const handler = (err: any, res: WorkResult) => { 81 | if (called) { 82 | return; 83 | } 84 | 85 | called = true; 86 | process.removeListener('uncaughtException', handler); 87 | callback(err, res); 88 | }; 89 | 90 | process.once('uncaughtException' as any, handler); 91 | 92 | try { 93 | this.runFuzzInner(input, handler); 94 | } catch (e) { 95 | handler(e, null as any); 96 | } 97 | } 98 | private runFuzzInner(input: Buffer, callback: (err: any, res: WorkResult) => void): void { 99 | if (this.target.fuzz.length > 1) { 100 | this.target.fuzz(input, callback); 101 | return; 102 | } 103 | 104 | const out = this.target.fuzz(input); 105 | if (out && typeof out.then === 'function') { 106 | out.then(res => callback(null, res)).catch(err => callback(err, null as any)); 107 | return; 108 | } 109 | 110 | callback(null, out); 111 | } 112 | 113 | private doWork(input: Buffer): void { 114 | this.instrumenter.attach(); 115 | 116 | const start = getMicroTime(); 117 | 118 | this.runFuzz(input, (error, result) => { 119 | const runtime = getMicroTime() - start; 120 | const [coverage, size] = flattenBuckets(this.instrumenter.getLastCoverage()); 121 | this.lastCoverage = coverage; 122 | 123 | this.proto.write({ 124 | coverageSize: size, 125 | error: error ? error.stack || error.message || error : undefined, 126 | hash: createHash('md5') 127 | .update(coverage) 128 | .digest('hex'), 129 | inputLength: input.length, 130 | kind: PacketKind.WorkSummary, 131 | result, 132 | runtime, 133 | }); 134 | }); 135 | } 136 | } 137 | 138 | if (require.main === module) { 139 | new Worker(process.argv[2], JSON.parse(process.argv[3])).start(); 140 | } 141 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/artifact-set/disk-artifact-set.ts: -------------------------------------------------------------------------------- 1 | import { IArtifactSet, IArtifact, getArtifactId } from '.'; 2 | import { 3 | readdir, 4 | readFile, 5 | stat, 6 | pathExists, 7 | readJson, 8 | mkdirp, 9 | writeFile, 10 | writeJson, 11 | } from 'fs-extra'; 12 | import { extname, join } from 'path'; 13 | 14 | export class DiskArtifactSet implements IArtifactSet { 15 | /** 16 | * Additional metadata file stored in the artifacts. 17 | */ 18 | private static readonly metadataExtension = '.metadata'; 19 | 20 | private data?: Promise<{ [id: string]: IArtifact }>; 21 | 22 | constructor(private readonly directory: string) {} 23 | 24 | public async add(artifact: IArtifact) { 25 | const id = getArtifactId(artifact); 26 | const todo: Promise[] = [ 27 | this.all().then(contents => (contents[id] = artifact)), 28 | writeFile(join(this.directory, id), artifact.data), 29 | ]; 30 | 31 | if (artifact.metadata) { 32 | todo.push( 33 | writeJson(join(this.directory, id + DiskArtifactSet.metadataExtension), artifact.metadata), 34 | ); 35 | } 36 | 37 | await Promise.all(todo); 38 | } 39 | 40 | public all() { 41 | if (!this.data) { 42 | this.data = this.loadFromDisk(); 43 | } 44 | 45 | return this.data; 46 | } 47 | 48 | private async loadFromDisk() { 49 | const output: { [id: string]: IArtifact } = {}; 50 | 51 | await mkdirp(this.directory); 52 | 53 | for (const filename of await readdir(this.directory)) { 54 | const file = join(this.directory, filename); 55 | if (extname(file) === DiskArtifactSet.metadataExtension) { 56 | continue; 57 | } 58 | 59 | if (!(await stat(file)).isFile()) { 60 | continue; 61 | } 62 | 63 | const data = await readFile(file); 64 | const delimiter = data.indexOf(0); 65 | if (delimiter === -1) { 66 | continue; 67 | } 68 | 69 | const metadata = (await pathExists(file + DiskArtifactSet.metadataExtension)) 70 | ? await readJson(file + DiskArtifactSet.metadataExtension) 71 | : null; 72 | const signature = getArtifactId(data.slice(delimiter + 1)); 73 | 74 | output[signature] = { data, metadata, userGenerated: signature !== filename }; 75 | } 76 | 77 | return output; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/artifact-set/index.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'crypto'; 2 | 3 | export interface IArtifactSet { 4 | /** 5 | * Stores the artifact in the set. 6 | */ 7 | add(artifact: IArtifact): Promise; 8 | 9 | /** 10 | * Gets all inputs from the set. 11 | */ 12 | all(): Promise }>>; 13 | } 14 | 15 | /** 16 | * Artifact stored in the IArtifactSet. 17 | */ 18 | export interface IArtifact { 19 | /** 20 | * Artifact data. 21 | */ 22 | data: Buffer; 23 | 24 | /** 25 | * Metadata specific to the artifact. 26 | */ 27 | metadata: T; 28 | 29 | /** 30 | * Whether the file is user-generated. 31 | */ 32 | userGenerated: boolean; 33 | } 34 | 35 | /** 36 | * Returns the ID of the given artifact or buffer. 37 | */ 38 | export const getArtifactId = (data: IArtifact | Buffer) => { 39 | const hash = createHash('sha1'); 40 | if (data instanceof Buffer) { 41 | hash.update(data); 42 | } else { 43 | hash.update(data.data); 44 | } 45 | 46 | return hash.digest('hex'); 47 | }; 48 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/artifact-set/memory-artifact-set.ts: -------------------------------------------------------------------------------- 1 | import { IArtifactSet, IArtifact, getArtifactId } from '.'; 2 | 3 | export class MemoryArtifactSet implements IArtifactSet { 4 | private readonly data: { [id: string]: IArtifact } = {}; 5 | 6 | public add(artifact: IArtifact) { 7 | this.data[getArtifactId(artifact)] = artifact; 8 | return Promise.resolve(); 9 | } 10 | 11 | public all() { 12 | return Promise.resolve(this.data); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/cluster-factory.ts: -------------------------------------------------------------------------------- 1 | import { FileSerializer } from "./Serializer"; 2 | import { IClusterOptions, Cluster } from "./Cluster"; 3 | import { injectable, inject } from "inversify"; 4 | import * as Types from "./dependencies"; 5 | import { LiteralExtractor } from "./instrumentation/literal-extractor"; 6 | 7 | /** 8 | * The Cluster coordinates multiple child 9 | */ 10 | @injectable() 11 | export class ClusterFactory { 12 | private serializer = new FileSerializer(); 13 | 14 | constructor( 15 | private readonly options: IClusterOptions, 16 | @inject(Types.LiteralExtractor) 17 | private readonly literalExtractor: LiteralExtractor 18 | ) {} 19 | 20 | /** 21 | * Boots the cluster. 22 | */ 23 | public start() { 24 | this.seedAndValidateModules(); 25 | const cluster = new Cluster(this.options, this.serializer); 26 | setTimeout(() => cluster.start(), 0); 27 | return cluster; 28 | } 29 | 30 | /** 31 | * Does a quick smoke test to see if the module looks like it's set up 32 | * correctly to fuzz, throwing an error if it isn't. 33 | */ 34 | private async seedAndValidateModules() { 35 | const literals = await this.literalExtractor.detectAll(async () => { 36 | const target = require(this.options.target); // tslint:disable-line 37 | if (!target || typeof target.fuzz !== 'function') { 38 | throw new Error("Expected the file to export a fuzz() function, but it didn't!"); 39 | } 40 | 41 | try { 42 | await target(Buffer.alloc(0)); 43 | } catch { 44 | // ignore any errors, we'll catch them in fuzzing, we just want to make 45 | // sure that any dynamic imports run. 46 | } 47 | }); 48 | 49 | 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/dependencies.ts: -------------------------------------------------------------------------------- 1 | import { Container } from 'inversify'; 2 | 3 | export const FuzzOptions = Symbol('FuzzOptions'); 4 | export const ClusterFactory = Symbol('ClusterFactory'); 5 | 6 | export const HookManager = Symbol('HookManager'); 7 | export const LiteralExtractor = Symbol('LiteralExtractor'); 8 | export const CoverageInstrumentor = Symbol('CoverageInstrumentor'); 9 | 10 | export const MutationAlgorithms = Symbol('MutationAlgorithms'); 11 | export const Mutator = Symbol('Mutator'); 12 | 13 | export const CoverageHashService = Symbol('CoverageHashService'); 14 | export const RuntimeServiceCollection = Symbol('RuntimeServiceCollection'); 15 | export const Runtime = Symbol('Runtime'); 16 | 17 | let singleton: Container; 18 | export const getContainerInstance = () => { 19 | if (!singleton) { 20 | singleton = createContainer(); 21 | } 22 | 23 | return singleton.createChild(); 24 | }; 25 | 26 | export const createContainer = () => { 27 | const container = new Container(); 28 | 29 | // Things are dynamically required() so that we can boot up quickly and not 30 | // require things we don't need in worker processes. 31 | 32 | container 33 | .bind(HookManager) 34 | .toDynamicValue(ctx => 35 | ctx.container.resolve(require('./instrumentation/hook-manager').HookManager), 36 | ) 37 | .inSingletonScope(); 38 | 39 | container 40 | .bind(CoverageInstrumentor) 41 | .toDynamicValue(ctx => 42 | ctx.container.resolve( 43 | require('./instrumentation/coverage-instrumentor').ConverageInstrumentor, 44 | ), 45 | ) 46 | .inSingletonScope(); 47 | 48 | container 49 | .bind(LiteralExtractor) 50 | .toDynamicValue(ctx => 51 | ctx.container.resolve(require('./instrumentation/literal-extractor').LiteralExtractor), 52 | ) 53 | .inSingletonScope(); 54 | 55 | container 56 | .bind(MutationAlgorithms) 57 | .toDynamicValue(ctx => ctx.container.resolve(require('./mutations/algorithms').mutators)) 58 | .inSingletonScope(); 59 | 60 | container 61 | .bind(Mutator) 62 | .toDynamicValue(ctx => ctx.container.resolve(require('./mutations/mutator').Mutator)) 63 | .inSingletonScope(); 64 | 65 | container 66 | .bind(ClusterFactory) 67 | .toDynamicValue(ctx => ctx.container.resolve(require('./cluster-factory').ClusterFactory)) 68 | .inSingletonScope(); 69 | 70 | container 71 | .bind(CoverageHashService) 72 | .toDynamicValue(ctx => ctx.container.resolve(require('./runtime/coverage-hash').CoverageHash)) 73 | .inSingletonScope(); 74 | 75 | container 76 | .bind(RuntimeServiceCollection) 77 | .toDynamicValue(ctx => 78 | ctx.container.resolve( 79 | require('./runtime/runtime-service-collection').RuntimeServiceCollection, 80 | ), 81 | ) 82 | .inSingletonScope(); 83 | 84 | container 85 | .bind(Runtime) 86 | .toDynamicValue(ctx => ctx.container.resolve(require('./runtime/runtime').Runtime)) 87 | .inSingletonScope(); 88 | 89 | return container; 90 | }; 91 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/input/corpus.ts: -------------------------------------------------------------------------------- 1 | import { Input } from "./input"; 2 | import { injectable } from "inversify"; 3 | 4 | /** 5 | * The hash store saves a bunch of Buffers and provides methods to efficiently 6 | * check to see if a given buffer is in the store yet. 7 | */ 8 | @injectable() 9 | export class Corpus { 10 | private store: { 11 | [hash: string]: { 12 | input: Input, 13 | indexInRunning: number, 14 | }, 15 | } = Object.create(null); 16 | 17 | private mutator = new Mutator(); 18 | private runningScore: { runningScore: number, input: Input }[] = []; 19 | private totalExecutionTime = 0; 20 | private totalBranchCoverage = 0; 21 | private literals = new Set(); 22 | 23 | public foundLiterals(literals: ReadonlyArray) { 24 | literals = literals.filter(l => !this.literals.has(l)); 25 | if (!literals.length) { 26 | return; 27 | } 28 | 29 | this.mutator.addLiterals(literals); 30 | for (const literal of literals) { 31 | this.literals.add(literal); 32 | } 33 | } 34 | 35 | /** 36 | * Returns if we're interested in getting a full summary report for the 37 | * the given hash. 38 | */ 39 | public isInterestedIn(input: Input) { 40 | return !this.store[input.summary.hash]; 41 | } 42 | 43 | /** 44 | * Returns an input, weighting by its score. 45 | */ 46 | public pickWeighted(): Input { 47 | if (this.runningScore.length === 0) { 48 | return zeroInput; 49 | } 50 | 51 | const running = this.runningScore; 52 | const targetScore = Math.random() * running[running.length - 1].runningScore; 53 | 54 | let i = 0; 55 | while (running[i].runningScore < targetScore) { 56 | i += 1; 57 | } 58 | 59 | return running[i].input; 60 | } 61 | 62 | /** 63 | * Returns all inputs stored in the corpus. 64 | */ 65 | public getAllInputs(): Input[] { 66 | return this.runningScore.map(r => r.input); 67 | } 68 | 69 | /** 70 | * Stores the work summary and the coverage file. 71 | */ 72 | public put(input: Input) { 73 | const existing = this.store[input.summary.hash]; 74 | if (existing) { 75 | return; 76 | } 77 | 78 | 79 | const running = this.runningScore; 80 | const index = running.length; 81 | const score = this.scoreInput(input); 82 | 83 | this.totalBranchCoverage += input.summary.coverageSize; 84 | this.totalExecutionTime += input.summary.runtime; 85 | 86 | running[index] = { 87 | runningScore: index === 0 ? score : running[index - 1].runningScore + score, 88 | input, 89 | }; 90 | 91 | this.store[input.summary.hash] = { 92 | indexInRunning: index, 93 | input, 94 | }; 95 | } 96 | 97 | /** 98 | * Returns the number of items in our store. 99 | */ 100 | public size() { 101 | return this.runningScore.length; 102 | } 103 | 104 | public coverage() { 105 | return this.runningScore.reduce((acc, r) => acc + r.input.summary.coverageSize, 0); 106 | } 107 | 108 | private scoreInput(input: Input): number { 109 | const avgTime = this.totalExecutionTime / Math.min(1, this.runningScore.length); 110 | const avgCoverage = this.totalBranchCoverage / Math.min(1, this.runningScore.length); 111 | return input.getScore(avgTime, avgCoverage); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/input/input-generator.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connor4312/js-fuzz/cb19be3062274dfc62d0f81e18c16b0b6f3f0006/packages/js-fuzz-core/src/input/input-generator.ts -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/input/input.ts: -------------------------------------------------------------------------------- 1 | import { PacketKind, WorkResult, IWorkSummary } from "../protocol/types"; 2 | 3 | /** 4 | * The Input is a record for a particular job. It can contain a Coverage hash. 5 | */ 6 | export class Input { 7 | public static zero = new Input( 8 | Buffer.from([]), 9 | 0, 10 | { 11 | kind: PacketKind.WorkSummary, 12 | result: WorkResult.Allow, 13 | coverageSize: 0, 14 | inputLength: 0, 15 | hash: '', 16 | runtime: Infinity, 17 | }, 18 | ); 19 | 20 | private static initialScore = 10; 21 | private static minScore = 1; 22 | private static maxScore = 100; 23 | 24 | constructor( 25 | public readonly input: Buffer, 26 | public readonly depth: number, 27 | public readonly summary: IWorkSummary, 28 | ) {} 29 | 30 | /** 31 | * Serializes an input into a string. 32 | */ 33 | public serialize(): string { 34 | return JSON.stringify({ 35 | input: this.input.toString('hex'), 36 | depth: this.depth, 37 | summary: this.summary, 38 | $isJsFuzz: true, 39 | }); 40 | } 41 | 42 | /** 43 | * Returns whether this input resulted in more interesting behavior 44 | * that the other one. This is mostly ported from go-fuzz 45 | * @see https://git.io/vMwjF 46 | */ 47 | public getScore(averageExecutionTime: number, averageCoverSize: number): number { 48 | let score = Input.initialScore; 49 | // Execution time multiplier 0.1-3x. 50 | // Fuzzing faster inputs increases efficiency. 51 | 52 | const timeRatio = this.summary.runtime / averageExecutionTime; 53 | if (timeRatio > 10) { 54 | score /= 10; 55 | } else if (timeRatio > 4) { 56 | score /= 4; 57 | } else if (timeRatio > 2) { 58 | score /= 2; 59 | } else if (timeRatio < 0.25) { 60 | score *= 3; 61 | } else if (timeRatio < 0.33) { 62 | score *= 2; 63 | } else if (timeRatio < 0.5) { 64 | score *= 1.5; 65 | } 66 | 67 | // Coverage size multiplier 0.25-3x. 68 | // Inputs with larger coverage are more interesting. 69 | const coverSize = this.summary.coverageSize / averageCoverSize; 70 | if (coverSize > 3) { 71 | score *= 3; 72 | } else if (coverSize > 2) { 73 | score *= 2; 74 | } else if (coverSize > 1.5) { 75 | score *= 1.5; 76 | } else if (coverSize < 0.3) { 77 | score /= 4; 78 | } else if (coverSize < 0.5) { 79 | score /= 2; 80 | } else if (coverSize < 0.75) { 81 | score /= 1.5; 82 | } 83 | 84 | // Input depth multiplier 1-5x. 85 | // Deeper inputs have higher chances of digging deeper into code. 86 | if (this.depth < 10) { 87 | // no boost for you 88 | } else if (this.depth < 20) { 89 | score *= 2; 90 | } else if (this.depth < 40) { 91 | score *= 3; 92 | } else if (this.depth < 80) { 93 | score *= 4; 94 | } else { 95 | score *= 5; 96 | } 97 | 98 | // User boost (Fuzz function return value) multiplier 1-2x. 99 | // We don't know what it is, but user said so. 100 | if (this.summary.result === WorkResult.Reinforce) { 101 | // Assuming this is a correct input (e.g. deserialized successfully). 102 | score *= 2; 103 | } 104 | 105 | return Math.min(Input.maxScore, Math.max(Input.minScore, score)); 106 | } 107 | 108 | /** 109 | * Deserializes the input string to a qualified Input object 110 | */ 111 | public static Deserialize(input: string): Input { 112 | const parsed = JSON.parse(input); 113 | if (!parsed.$isJsFuzz) { 114 | throw new SyntaxError('The provided packet is not a js-fuzz input'); 115 | } 116 | 117 | return new Input( 118 | Buffer.from(parsed.input, 'hex'), 119 | parsed.depth, 120 | parsed.summary, 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/instrumentation/coverage-instrumentor.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { ConverageInstrumentor } from './coverage-instrumentor'; 3 | import { readFileSync, readdirSync } from 'fs'; 4 | import * as Types from '../dependencies'; 5 | 6 | const loadInstrumentationFixtures = () => { 7 | const base = `${__dirname}/../../test/fixture/instrument`; 8 | const files = readdirSync(base); 9 | const output: { name: string; before: string; after: string }[] = []; 10 | 11 | files.forEach(name => { 12 | const match = /^(.+)\.before\.txt$/.exec(name); 13 | if (!match || !files.includes(`${match[1]}.after.txt`)) { 14 | return; 15 | } 16 | 17 | const tcase = match[1]; 18 | output.push({ 19 | name, 20 | before: readFileSync(`${base}/${tcase}.before.txt`, 'utf8').trim(), 21 | after: readFileSync(`${base}/${tcase}.after.txt`, 'utf8').trim(), 22 | }); 23 | }); 24 | 25 | return output; 26 | }; 27 | 28 | describe('coverage-instrumenter', () => { 29 | let inst: ConverageInstrumentor; 30 | 31 | before(() => { 32 | const container = Types.getContainerInstance(); 33 | container.bind(Types.FuzzOptions).toConstantValue({ exclude: [] }); 34 | inst = container.get(Types.CoverageInstrumentor); 35 | }); 36 | 37 | after(() => { 38 | inst.detach(); 39 | }); 40 | 41 | describe('fixtures', () => { 42 | loadInstrumentationFixtures().forEach(tcase => { 43 | it(`instruments ${tcase.name}`, () => { 44 | const instrumented = inst.instrument(tcase.before); 45 | expect(instrumented).to.equal(tcase.after); 46 | }); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/instrumentation/coverage-instrumentor.ts: -------------------------------------------------------------------------------- 1 | import { generate } from 'escodegen'; 2 | import { parseScript } from 'esprima'; 3 | import * as ESTrasverse from 'estraverse'; 4 | import { 5 | BlockStatement, 6 | Expression, 7 | ExpressionStatement, 8 | Node, 9 | SequenceExpression, 10 | Statement, 11 | } from 'estree'; 12 | import { injectable, inject } from 'inversify'; 13 | import * as Types from '../dependencies'; 14 | import { HookManager } from './hook-manager'; 15 | import { Runtime } from '../runtime/runtime'; 16 | import { createCoverageId } from '../runtime/coverage-hash'; 17 | 18 | /** 19 | * Wraps the node as a block statement if it isn't already on. 20 | */ 21 | function ensureBlock(stmt: Statement): BlockStatement { 22 | if (!stmt) { 23 | return { type: 'BlockStatement', body: [] }; 24 | } 25 | 26 | if (stmt.type !== 'BlockStatement') { 27 | return { type: 'BlockStatement', body: [stmt] }; 28 | } 29 | 30 | return stmt; 31 | } 32 | 33 | /** 34 | * Prepends the list of statements to the provided block and returns it. If 35 | * the block to be prepended to is already a BlockExpression, we'll just 36 | * modify its body, otherwise we'll wrap it using the comma operator. 37 | */ 38 | function prependBlock(block: Statement, expr: Expression): BlockStatement | ExpressionStatement { 39 | switch (block.type) { 40 | case 'BlockStatement': 41 | block.body = [{ type: 'ExpressionStatement', expression: expr }, ...block.body]; 42 | return block; 43 | case 'ExpressionStatement': 44 | block.expression = prependExpression(block.expression, expr); 45 | return block; 46 | default: 47 | throw new Error(`Unsupported wrap type ${block.type}`); 48 | } 49 | } 50 | 51 | /** 52 | * Prepends the list of statements to the provided expression and returns it. 53 | */ 54 | function prependExpression(block: Expression, expr: Expression): SequenceExpression { 55 | return { 56 | type: 'SequenceExpression', 57 | expressions: [expr, block], 58 | }; 59 | } 60 | 61 | /** 62 | * The Instrumenter transforms JavaScript code adding coverage measurements 63 | * as described in AFL's whitepaper here: 64 | * http://lcamtuf.coredump.cx/afl/technical_details.txt 65 | */ 66 | @injectable() 67 | export class ConverageInstrumentor { 68 | private idCounter = 0; 69 | private detachFn?: () => void; 70 | 71 | constructor( 72 | @inject(Types.HookManager) private readonly hooks: HookManager, 73 | @inject(Types.Runtime) private readonly runtime: Runtime, 74 | ) {} 75 | 76 | /** 77 | * Hooks the instrumenter into the require() statement. 78 | */ 79 | public attach() { 80 | this.runtime.install(); 81 | this.detachFn = this.hooks.hookRequire(code => this.instrument(code)); 82 | } 83 | 84 | /** 85 | * Detaches the instrumenter from intercepting new code. 86 | */ 87 | public detach() { 88 | if (this.detachFn) { 89 | this.detachFn(); 90 | this.detachFn = undefined; 91 | } 92 | } 93 | 94 | /** 95 | * Instruments the provided code with branch analysis instructions. 96 | */ 97 | public instrument(code: string) { 98 | return generate( 99 | ESTrasverse.replace(parseScript(code), { 100 | enter: stmt => this.instrumentBranches(stmt), 101 | }), 102 | ); 103 | } 104 | 105 | /** 106 | * Walks and instruments the AST tree. 107 | */ 108 | private instrumentBranches(stmt: Node) { 109 | switch (stmt.type) { 110 | case 'IfStatement': 111 | stmt.consequent = prependBlock(ensureBlock(stmt.consequent), this.branch()); 112 | stmt.alternate = stmt.alternate && prependBlock(ensureBlock(stmt.alternate), this.branch()); 113 | return stmt; 114 | case 'ConditionalExpression': 115 | stmt.consequent = prependExpression(stmt.consequent, this.branch()); 116 | stmt.alternate = prependExpression(stmt.alternate, this.branch()); 117 | return stmt; 118 | case 'LogicalExpression': 119 | stmt.left = prependExpression(stmt.left, this.branch()); 120 | stmt.right = prependExpression(stmt.right, this.branch()); 121 | return stmt; 122 | case 'SwitchCase': 123 | stmt.consequent = [ 124 | { type: 'ExpressionStatement', expression: this.branch() }, 125 | ...stmt.consequent, 126 | ]; 127 | return stmt; 128 | default: 129 | return stmt; 130 | } 131 | } 132 | 133 | private branch() { 134 | const id = createCoverageId(this.idCounter++); 135 | return this.runtime.call('coverage', 'increment', { 136 | type: 'Literal', 137 | value: id, 138 | raw: String(id), 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/instrumentation/hook-manager.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { inject, injectable } from 'inversify'; 3 | import { IFuzzOptions } from '../options'; 4 | import * as Types from '../dependencies'; 5 | import * as minimatch from 'minimatch'; 6 | 7 | /** 8 | * Handles hooking into Node's require. 9 | */ 10 | @injectable() 11 | export class HookManager { 12 | constructor(@inject(Types.FuzzOptions) private readonly options: Pick) {} 13 | 14 | /** 15 | * Hooks the transformation function into Node's require(). Returns a 16 | * function that can be used to unhook it. 17 | */ 18 | public hookRequire(transform: (input: string) => string, extension = '.js') { 19 | const previous = require.extensions[extension]; 20 | require.extensions[extension] = (m: any, fname: string) => { 21 | const contents = readFileSync(fname, 'utf8'); 22 | if (!this.isFileExcluded(fname)) { 23 | m._compile(transform(contents), fname); 24 | } else { 25 | m._compile(contents, fname); 26 | } 27 | }; 28 | 29 | return () => { 30 | require.extensions[extension] = previous; 31 | }; 32 | } 33 | 34 | private isFileExcluded(name: string) { 35 | for (const pattern of this.options.exclude) { 36 | if (minimatch(name, pattern)) { 37 | return true; 38 | } 39 | } 40 | 41 | return false; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/instrumentation/literal-extractor.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { readFileSync, readdirSync } from 'fs'; 3 | import { HookManager } from './hook-manager'; 4 | import { LiteralExtractor } from './literal-extractor'; 5 | import { 6 | setRandomNumberGenerator, 7 | createRandomNumberGenerator, 8 | resetRandomNumberGenerator, 9 | } from '../Math'; 10 | 11 | const loadInstrumentationFixtures = () => { 12 | const base = `${__dirname}/../../test/fixture/instrument`; 13 | const files = readdirSync(base); 14 | const output: { name: string; contents: string; literals: string[] }[] = []; 15 | 16 | files.forEach(name => { 17 | const match = /^(.+)\.before\.txt$/.exec(name); 18 | if (!match) { 19 | return; 20 | } 21 | 22 | const tcase = match[1]; 23 | output.push({ 24 | name, 25 | contents: readFileSync(`${base}/${tcase}.before.txt`, 'utf8').trim(), 26 | literals: JSON.parse(readFileSync(`${base}/${tcase}.literals.json`, 'ucs-2')), 27 | }); 28 | }); 29 | 30 | return output; 31 | }; 32 | 33 | describe('literal-extractor', () => { 34 | let inst: LiteralExtractor; 35 | 36 | beforeEach(() => { 37 | setRandomNumberGenerator(createRandomNumberGenerator(42)); 38 | inst = new LiteralExtractor(new HookManager({ exclude: [] })); 39 | }); 40 | 41 | afterEach(() => { 42 | resetRandomNumberGenerator(); 43 | }); 44 | 45 | describe('fixtures', () => { 46 | loadInstrumentationFixtures().forEach(tcase => { 47 | it(`instruments ${tcase.name}`, () => { 48 | expect([...inst.detect(tcase.contents)]).to.deep.equal(tcase.literals); 49 | }); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/instrumentation/literal-extractor.ts: -------------------------------------------------------------------------------- 1 | import { parseScript } from 'esprima'; 2 | import * as ESTrasverse from 'estraverse'; 3 | import { injectable, inject } from 'inversify'; 4 | import * as Types from '../dependencies'; 5 | import { HookManager } from './hook-manager'; 6 | import * as RandExp from 'randexp'; 7 | import { randn } from '../Math'; 8 | import { RegExpLiteral } from 'estree'; 9 | 10 | /** 11 | * The LiteralExtractor reads all literals from required javascript modules. 12 | * This is used to seed the corpus with interesting data. 13 | */ 14 | @injectable() 15 | export class LiteralExtractor { 16 | constructor(@inject(Types.HookManager) private readonly hooks: HookManager) {} 17 | 18 | /** 19 | * Detects and returns all literals in files required synchronously within 20 | * the given function. 21 | */ 22 | public async detectAll(fn: () => Promise): Promise> { 23 | const literals = new Set(); 24 | const unhook = this.hooks.hookRequire(src => { 25 | this.addToSet(src, literals); 26 | return src; 27 | }); 28 | 29 | try { 30 | await fn(); 31 | } finally { 32 | unhook(); 33 | } 34 | 35 | return literals; 36 | } 37 | 38 | public detect(code: string): Set { 39 | const literals = new Set(); 40 | this.addToSet(code, literals); 41 | return literals; 42 | } 43 | 44 | private addToSet(code: string, literals: Set) { 45 | ESTrasverse.traverse(parseScript(code), { 46 | enter: stmt => { 47 | if (stmt.type !== 'Literal') { 48 | return; 49 | } 50 | 51 | if ('regex' in stmt) { 52 | this.addRegex(stmt, literals); 53 | } else { 54 | literals.add(String(stmt.value)); 55 | } 56 | }, 57 | }); 58 | } 59 | 60 | private addRegex(stmt: RegExpLiteral, literals: Set) { 61 | const randexp = new RandExp(stmt.regex.pattern, stmt.regex.flags); 62 | randexp.max = 1; // mutators will take care of duplicating out, no need to add noise 63 | randexp.randInt = randn; 64 | 65 | // Add in a few variations for good measure 66 | for (let i = 0; i < 3; i++) { 67 | literals.add(randexp.gen()); 68 | } 69 | 70 | // And high-matching sequences 71 | randexp.defaultRange.subtract(32, 126); 72 | randexp.defaultRange.add(0, 65535); 73 | for (let i = 0; i < 3; i++) { 74 | literals.add(randexp.gen()); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/mutations/algorithms/algorithms.test.ts: -------------------------------------------------------------------------------- 1 | import { IMutationAlgorithm, createAll } from '../algorithms'; 2 | import { expect } from 'chai'; 3 | import { 4 | setRandomNumberGenerator, 5 | createRandomNumberGenerator, 6 | resetRandomNumberGenerator, 7 | } from '../../Math'; 8 | import { AsciiDigitReplaceMutator } from './ascii-digit-replace-mutator'; 9 | import { AsciiNumberReplaceMutator } from './ascii-number-replace-mutator'; 10 | import { ReplaceInteresting8Mutator } from './replace-interesting-8-mutator'; 11 | import { ReplaceInteresting16Mutator } from './replace-interesting-16-mutator'; 12 | import { ReplaceInteresting32Mutator } from './replace-interesting-32-mutator'; 13 | import { Container } from 'inversify'; 14 | 15 | interface ITestCase { 16 | alg: IMutationAlgorithm; 17 | input: string | Buffer; 18 | output: string | Buffer | null; 19 | } 20 | 21 | describe('mutation algorithms', () => { 22 | const seed = 26220219121059154247048; 23 | beforeEach(() => { 24 | setRandomNumberGenerator(createRandomNumberGenerator(seed)); 25 | }); 26 | 27 | describe('sanity check all mutators', () => { 28 | for (const alg of createAll(new Container())) { 29 | it(alg.constructor.name, () => { 30 | for (let length = 0; length < 10; length++) { 31 | const buffer = Buffer.alloc(length); 32 | for (let i = 0; i < length; i++) { 33 | buffer[i] = i; 34 | } 35 | 36 | alg.mutate(Buffer.alloc(length)); 37 | } 38 | }); 39 | } 40 | }); 41 | 42 | afterEach(() => resetRandomNumberGenerator()); 43 | 44 | const tcases: ITestCase[] = [ 45 | { 46 | alg: new AsciiDigitReplaceMutator(), 47 | input: 'lorem ipsum123 dolor', 48 | output: 'lorem ipsum113 dolor', 49 | }, 50 | { 51 | alg: new AsciiDigitReplaceMutator(), 52 | input: 'lorem ipsum dolor', 53 | output: null, 54 | }, 55 | { 56 | alg: new AsciiNumberReplaceMutator(), 57 | input: 'lorem 42 ipsum123 dolor', 58 | output: 'lorem 42 ipsum112642223543156740 dolor', 59 | }, 60 | { 61 | alg: new AsciiNumberReplaceMutator(), 62 | input: 'lorem ipsum123', 63 | output: 'lorem ipsum112642223543156740', 64 | }, 65 | { 66 | alg: new AsciiNumberReplaceMutator(), 67 | input: 'lorem ipsum -123', 68 | output: 'lorem ipsum -112642223543156740', 69 | }, 70 | { 71 | alg: new AsciiNumberReplaceMutator(), 72 | input: 'lorem ipsum', 73 | output: null, 74 | }, 75 | { 76 | alg: new ReplaceInteresting8Mutator(), 77 | input: Buffer.from([0, 1, 2, 3, 4, 5, 6, 7]), 78 | output: Buffer.from([0, 1, 2, 3, 255, 5, 6, 7]), 79 | }, 80 | { 81 | alg: new ReplaceInteresting8Mutator(), 82 | input: Buffer.from([]), 83 | output: null, 84 | }, 85 | { 86 | alg: new ReplaceInteresting16Mutator(), 87 | input: Buffer.from([3, 3, 2, 3, 4, 5, 6, 7]), 88 | output: Buffer.from([3, 1, 0, 3, 4, 5, 6, 7]), 89 | }, 90 | { 91 | alg: new ReplaceInteresting16Mutator(), 92 | input: Buffer.from([0]), 93 | output: null, 94 | }, 95 | { 96 | alg: new ReplaceInteresting32Mutator(), 97 | input: Buffer.from([0, 1, 2, 3, 4, 5, 6, 7]), 98 | output: Buffer.from([255, 255, 255, 128, 4, 5, 6, 7]), 99 | }, 100 | { 101 | alg: new ReplaceInteresting32Mutator(), 102 | input: Buffer.from([0, 1, 2]), 103 | output: null, 104 | }, 105 | ]; 106 | 107 | for (const tcase of tcases) { 108 | it(`${tcase.alg.constructor.name}: ${tcase.input.toString('base64')}`, () => { 109 | let actual = tcase.alg.mutate( 110 | tcase.input instanceof Buffer ? tcase.input : Buffer.from(tcase.input), 111 | { literals: ['a', 'b', 'c'] }, 112 | ); 113 | if (typeof tcase.output === 'string' && actual) { 114 | expect(actual.toString()).to.equal(tcase.output); 115 | } else { 116 | expect(actual).to.deep.equal(tcase.output); 117 | } 118 | }); 119 | } 120 | }); 121 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/mutations/algorithms/ascii-digit-replace-mutator.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "inversify"; 2 | import { IMutationAlgorithm } from "."; 3 | import { randn, pickOne } from "../../Math"; 4 | import { dupe, CharCodes } from "./util"; 5 | 6 | @injectable() 7 | export class AsciiDigitReplaceMutator implements IMutationAlgorithm { 8 | public mutate(buffer: Buffer) { 9 | const digitPositions: number[] = []; 10 | for (let i = 0; i < buffer.length; i += 1) { 11 | if (buffer[i] >= CharCodes.Zero && buffer[i] <= CharCodes.Nine) { 12 | digitPositions.push(i); 13 | } 14 | } 15 | 16 | if (digitPositions.length === 0) { 17 | return null; 18 | } 19 | 20 | buffer = dupe(buffer); 21 | buffer[pickOne(digitPositions)] = randn(10) + CharCodes.Zero; 22 | return buffer; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/mutations/algorithms/ascii-number-replace-mutator.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "inversify"; 2 | import { IMutationAlgorithm } from "."; 3 | import { randn, pickOne, maxInt32 } from "../../Math"; 4 | import { CharCodes } from "./util"; 5 | 6 | @injectable() 7 | export class AsciiNumberReplaceMutator implements IMutationAlgorithm { 8 | public mutate(buffer: Buffer) { 9 | const numberPositions: { start: number; end: number }[] = []; 10 | let start = -1; 11 | for (let i = 0; i < buffer.length; i += 1) { 12 | if ( 13 | (buffer[i] >= CharCodes.Zero && buffer[i] <= CharCodes.Nine) || 14 | (start === -1 && buffer[i] === CharCodes.Dash) 15 | ) { 16 | if (start === -1) { 17 | start = i; 18 | } 19 | } else if (start !== -1 && i - start > 1) { 20 | numberPositions.push({ start, end: i }); 21 | start = -1; 22 | } 23 | } 24 | 25 | if (start > 0 && start < buffer.length - 1) { 26 | numberPositions.push({ start, end: buffer.length }); 27 | } 28 | 29 | if (numberPositions.length === 0) { 30 | return null; 31 | } 32 | 33 | let value: number; 34 | switch (randn(4)) { 35 | case 0: 36 | value = randn(1000); 37 | break; 38 | case 1: 39 | value = randn(maxInt32); 40 | break; 41 | case 2: 42 | value = randn(maxInt32) ** 2; 43 | break; 44 | case 3: 45 | value = -randn(maxInt32); 46 | break; 47 | default: 48 | throw new Error('unreachable'); 49 | } 50 | 51 | const toReplace = pickOne(numberPositions); 52 | if (buffer[toReplace.start] === CharCodes.Dash) { 53 | value *= -1; 54 | } 55 | 56 | return Buffer.concat([ 57 | buffer.slice(0, toReplace.start), 58 | Buffer.from(value.toString()), 59 | buffer.slice(toReplace.end), 60 | ]); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/mutations/algorithms/copy-range-mutator.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "inversify"; 2 | import { IMutationAlgorithm } from "."; 3 | import { randn } from "../../Math"; 4 | import { chooseLength, dupe } from "./util"; 5 | 6 | @injectable() 7 | export class CopyRangeMutator implements IMutationAlgorithm { 8 | public mutate(buffer: Buffer) { 9 | if (buffer.length < 2) { 10 | return null; 11 | } 12 | 13 | const src = randn(buffer.length); 14 | const len = chooseLength(buffer.length - src); 15 | let dst = randn(buffer.length); 16 | while (dst === src) { 17 | dst = randn(buffer.length); 18 | } 19 | 20 | buffer = dupe(buffer); 21 | buffer.copy(buffer, dst, src, len); 22 | return buffer; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/mutations/algorithms/duplicate-range-mutator.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "inversify"; 2 | import { IMutationAlgorithm } from "."; 3 | import { randn } from "../../Math"; 4 | import { chooseLength, dupe } from "./util"; 5 | 6 | @injectable() 7 | export class DuplicateRangeMutator implements IMutationAlgorithm { 8 | public mutate(buffer: Buffer) { 9 | if (buffer.length < 2) { 10 | return null; 11 | } 12 | 13 | buffer = dupe(buffer); 14 | const src = randn(buffer.length); 15 | const len = chooseLength(buffer.length - src); 16 | let dst = randn(buffer.length); 17 | while (dst === src) { 18 | dst = randn(buffer.length); 19 | } 20 | 21 | return Buffer.concat([buffer.slice(0, src + len), buffer.slice(src, len), buffer.slice(dst)]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/mutations/algorithms/flip-bit-mutator.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "inversify"; 2 | import { IMutationAlgorithm } from "."; 3 | import { randn } from "../../Math"; 4 | import { dupe } from "./util"; 5 | 6 | @injectable() 7 | export class FlipBitMutator implements IMutationAlgorithm { 8 | public mutate(buffer: Buffer) { 9 | if (buffer.length < 1) { 10 | return null; 11 | } 12 | 13 | buffer = dupe(buffer); 14 | buffer[randn(buffer.length)] ^= 1 << randn(8); 15 | return buffer; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/mutations/algorithms/index.ts: -------------------------------------------------------------------------------- 1 | import { Container } from 'inversify'; 2 | import { AsciiDigitReplaceMutator } from './ascii-digit-replace-mutator'; 3 | import { AsciiNumberReplaceMutator } from './ascii-number-replace-mutator'; 4 | import { CopyRangeMutator } from './copy-range-mutator'; 5 | import { DuplicateRangeMutator } from './duplicate-range-mutator'; 6 | import { FlipBitMutator } from './flip-bit-mutator'; 7 | import { InsertRangeMutator } from './insert-range-mutator'; 8 | import { RandomByteMutator } from './random-byte-mutator'; 9 | import { RemoveRangeMutator } from './remove-range-mutator'; 10 | import { ReplaceInteresting8Mutator } from './replace-interesting-8-mutator'; 11 | import { ReplaceInteresting16Mutator } from './replace-interesting-16-mutator'; 12 | import { ReplaceInteresting32Mutator } from './replace-interesting-32-mutator'; 13 | import { SwapByteMutator } from './swap-byte-mutator'; 14 | import { Uint8IncrementMutator } from './uint8-increment-mutator'; 15 | import { Uint32IncrementMutator } from './uint32-increment-mutator'; 16 | import { Uint16IncrementMutator } from './uint16-increment-mutator'; 17 | 18 | /** 19 | * Context given to the mutation algorithm. 20 | */ 21 | export interface IMutationContext { 22 | /** 23 | * Known string literals that can be mixed into the output. 24 | */ 25 | literals: ReadonlyArray; 26 | } 27 | 28 | export interface IMutationAlgorithm { 29 | /** 30 | * Runs the mutation on the buffer, returning a new buffer. If the 31 | * mutation can't be run right now, this will return null. 32 | */ 33 | mutate(input: Buffer, mutator: IMutationContext): Buffer | null; 34 | } 35 | 36 | /** 37 | * Creates all mutation algorithms from the container. 38 | */ 39 | export const createAll = (container: Container) => [ 40 | container.resolve(AsciiDigitReplaceMutator), 41 | container.resolve(AsciiNumberReplaceMutator), 42 | container.resolve(CopyRangeMutator), 43 | container.resolve(DuplicateRangeMutator), 44 | container.resolve(FlipBitMutator), 45 | container.resolve(InsertRangeMutator), 46 | container.resolve(RandomByteMutator), 47 | container.resolve(RemoveRangeMutator), 48 | container.resolve(ReplaceInteresting8Mutator), 49 | container.resolve(ReplaceInteresting16Mutator), 50 | container.resolve(ReplaceInteresting32Mutator), 51 | container.resolve(SwapByteMutator), 52 | container.resolve(Uint8IncrementMutator), 53 | container.resolve(Uint16IncrementMutator), 54 | container.resolve(Uint32IncrementMutator), 55 | ]; 56 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/mutations/algorithms/insert-range-mutator.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "inversify"; 2 | import { IMutationAlgorithm } from "."; 3 | import { randn } from "../../Math"; 4 | import { chooseLength } from "./util"; 5 | import { randomBytes } from "crypto"; 6 | 7 | @injectable() 8 | export class InsertRangeMutator implements IMutationAlgorithm { 9 | public mutate(buffer: Buffer) { 10 | const start = randn(buffer.length + 1); 11 | 12 | return Buffer.concat([ 13 | buffer.slice(0, start), 14 | randomBytes(chooseLength(10)), 15 | buffer.slice(start), 16 | ]); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/mutations/algorithms/random-byte-mutator.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "inversify"; 2 | import { IMutationAlgorithm } from "."; 3 | import { randn } from "../../Math"; 4 | import { dupe } from "./util"; 5 | 6 | @injectable() 7 | export class RandomByteMutator implements IMutationAlgorithm { 8 | public mutate(buffer: Buffer) { 9 | if (buffer.length < 1) { 10 | return null; 11 | } 12 | 13 | buffer = dupe(buffer); 14 | buffer[randn(buffer.length)] ^= randn(255) + 1; 15 | return buffer; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/mutations/algorithms/remove-range-mutator.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "inversify"; 2 | import { IMutationAlgorithm } from "."; 3 | import { randn } from "../../Math"; 4 | import { chooseLength } from "./util"; 5 | 6 | @injectable() 7 | export class RemoveRangeMutator implements IMutationAlgorithm { 8 | public mutate(buffer: Buffer) { 9 | if (buffer.length < 1) { 10 | return null; 11 | } 12 | 13 | const start = randn(buffer.length); 14 | const end = start + chooseLength(buffer.length - start); 15 | return Buffer.concat([buffer.slice(0, start), buffer.slice(end)]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/mutations/algorithms/replace-interesting-16-mutator.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "inversify"; 2 | import { IMutationAlgorithm } from "."; 3 | import { randn, pickOne } from "../../Math"; 4 | import { dupe } from "./util"; 5 | import { interesting16Bits } from "../interesting-bits"; 6 | 7 | @injectable() 8 | export class ReplaceInteresting16Mutator implements IMutationAlgorithm { 9 | public mutate(buffer: Buffer) { 10 | if (buffer.length < 2) { 11 | return null; 12 | } 13 | 14 | buffer = dupe(buffer); 15 | pickOne(interesting16Bits).copy(buffer, randn(buffer.length - 1), 0, 2); 16 | return buffer; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/mutations/algorithms/replace-interesting-32-mutator.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "inversify"; 2 | import { IMutationAlgorithm } from "."; 3 | import { randn, pickOne } from "../../Math"; 4 | import { dupe } from "./util"; 5 | import { interesting32Bits } from "../interesting-bits"; 6 | 7 | @injectable() 8 | export class ReplaceInteresting32Mutator implements IMutationAlgorithm { 9 | public mutate(buffer: Buffer) { 10 | if (buffer.length < 4) { 11 | return null; 12 | } 13 | 14 | buffer = dupe(buffer); 15 | pickOne(interesting32Bits).copy(buffer, randn(buffer.length - 3)); 16 | return buffer; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/mutations/algorithms/replace-interesting-8-mutator.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "inversify"; 2 | import { IMutationAlgorithm } from "."; 3 | import { randn, pickOne } from "../../Math"; 4 | import { dupe } from "./util"; 5 | import { interesting8Bits } from "../interesting-bits"; 6 | 7 | @injectable() 8 | export class ReplaceInteresting8Mutator implements IMutationAlgorithm { 9 | public mutate(buffer: Buffer) { 10 | if (buffer.length === 0) { 11 | return null; 12 | } 13 | 14 | buffer = dupe(buffer); 15 | buffer[randn(buffer.length)] = pickOne(interesting8Bits)[0]; 16 | return buffer; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/mutations/algorithms/swap-byte-mutator.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "inversify"; 2 | import { IMutationAlgorithm } from "."; 3 | import { randn } from "../../Math"; 4 | import { dupe } from "./util"; 5 | 6 | @injectable() 7 | export class SwapByteMutator implements IMutationAlgorithm { 8 | public mutate(buffer: Buffer) { 9 | if (buffer.length < 2) { 10 | return null; 11 | } 12 | 13 | const src = randn(buffer.length); 14 | let dst = randn(buffer.length); 15 | while (dst === src) { 16 | dst = randn(buffer.length); 17 | } 18 | 19 | buffer = dupe(buffer); 20 | [buffer[src], buffer[dst]] = [buffer[dst], buffer[src]]; 21 | return buffer; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/mutations/algorithms/uint16-increment-mutator.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "inversify"; 2 | import { IMutationAlgorithm } from "."; 3 | import { randn } from "../../Math"; 4 | import { dupe, roughWrap } from "./util"; 5 | 6 | @injectable() 7 | export class Uint16IncrementMutator implements IMutationAlgorithm { 8 | public mutate(buffer: Buffer) { 9 | if (buffer.length < 2) { 10 | return null; 11 | } 12 | 13 | const index = randn(buffer.length - 2); 14 | const amount = Math.random() > 0.5 ? 1 : -1; 15 | buffer = dupe(buffer); 16 | if (Math.random() > 0.5) { 17 | buffer.writeUInt16BE(roughWrap(buffer.readUInt16BE(index) + amount, 0xffff), index); 18 | } else { 19 | buffer.writeUInt16LE(roughWrap(buffer.readUInt16LE(index) + amount, 0xffff), index); 20 | } 21 | 22 | return buffer; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/mutations/algorithms/uint32-increment-mutator.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "inversify"; 2 | import { IMutationAlgorithm } from "."; 3 | import { randn } from "../../Math"; 4 | import { dupe, roughWrap } from "./util"; 5 | 6 | @injectable() 7 | export class Uint32IncrementMutator implements IMutationAlgorithm { 8 | public mutate(buffer: Buffer) { 9 | if (buffer.length < 4) { 10 | return null; 11 | } 12 | 13 | const index = randn(buffer.length - 4); 14 | const amount = Math.random() > 0.5 ? 1 : -1; 15 | buffer = dupe(buffer); 16 | if (Math.random() > 0.5) { 17 | buffer.writeUInt32BE(roughWrap(buffer.readUInt32BE(index) + amount, 0xffffffff), index); 18 | } else { 19 | buffer.writeUInt32LE(roughWrap(buffer.readUInt32LE(index) + amount, 0xffffffff), index); 20 | } 21 | 22 | return buffer; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/mutations/algorithms/uint8-increment-mutator.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "inversify"; 2 | import { IMutationAlgorithm } from "."; 3 | import { randn } from "../../Math"; 4 | import { dupe } from "./util"; 5 | 6 | @injectable() 7 | export class Uint8IncrementMutator implements IMutationAlgorithm { 8 | public mutate(buffer: Buffer) { 9 | if (buffer.length < 1) { 10 | return null; 11 | } 12 | 13 | buffer = dupe(buffer); 14 | buffer[randn(buffer.length)] = Math.random() > 0.5 ? 1 : -1; 15 | return buffer; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/mutations/algorithms/util.ts: -------------------------------------------------------------------------------- 1 | import { randn } from "../../Math"; 2 | 3 | /** 4 | * Chooses a random length, favoring small numbers. 5 | */ 6 | export function chooseLength(max: number): number { 7 | const x = randn(100); 8 | if (x < 90) { 9 | return randn(Math.min(8, max)) + 1; 10 | } 11 | if (x < 99) { 12 | return randn(Math.min(32, max)) + 1; 13 | } 14 | 15 | return randn(max) + 1; 16 | } 17 | 18 | /** 19 | * Decorator that dupes a buffer. 20 | */ 21 | export function dupe(buf: Buffer): Buffer { 22 | const output = Buffer.allocUnsafe(buf.length); 23 | buf.copy(output); 24 | return output; 25 | } 26 | 27 | /** 28 | * Rough "wrapping" function that ensure a number is in the range [0, threshold) 29 | */ 30 | export function roughWrap(input: number, threshold: number) { 31 | if (input >= threshold) { 32 | return 0; 33 | } 34 | if (input < 0) { 35 | return threshold - 1; 36 | } 37 | 38 | return input; 39 | } 40 | 41 | export const enum CharCodes { 42 | Zero = 48, 43 | Nine = 57, 44 | Dash = 45, 45 | } 46 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/mutations/interesting-bits.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Defined 'interesting' integeners. From go-fuzz. 3 | * @see https://github.com/dvyukov/go-fuzz/blob/5cc3605ccbb6e38722b388400463114cc3ef29f5/go-fuzz/mutator.go#L417-L430 4 | */ 5 | 6 | const interesting8BitNumbers = [-128, -1, 0, 1, 16, 32, 64, 100, 127]; 7 | 8 | export const interesting8Bits = interesting8BitNumbers.map(n => Buffer.from([n])); 9 | 10 | const interesting16BitNumbers = [ 11 | ...interesting8BitNumbers, 12 | -32768, 13 | -129, 14 | 128, 15 | 255, 16 | 256, 17 | 512, 18 | 1000, 19 | 1024, 20 | 4096, 21 | 32767, 22 | ]; 23 | 24 | export const interesting16Bits = interesting16BitNumbers 25 | .map(n => { 26 | const buf = Buffer.allocUnsafe(2); 27 | buf.writeInt16BE(n, 0); 28 | return buf; 29 | }) 30 | .concat( 31 | interesting16BitNumbers.map(n => { 32 | const buf = Buffer.allocUnsafe(2); 33 | buf.writeInt16LE(n, 0); 34 | return buf; 35 | }), 36 | ); 37 | 38 | const interesting32BitNumbers = [ 39 | ...interesting16BitNumbers, 40 | -2147483648, 41 | -100663046, 42 | -32769, 43 | 32768, 44 | 65535, 45 | 65536, 46 | 100663045, 47 | 2147483647, 48 | ]; 49 | 50 | export const interesting32Bits = interesting32BitNumbers 51 | .map(n => { 52 | const buf = Buffer.allocUnsafe(4); 53 | buf.writeInt32BE(n, 0); 54 | return buf; 55 | }) 56 | .concat( 57 | interesting16BitNumbers.map(n => { 58 | const buf = Buffer.allocUnsafe(4); 59 | buf.writeInt32BE(n, 0); 60 | return buf; 61 | }), 62 | ); 63 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/mutations/mutator.ts: -------------------------------------------------------------------------------- 1 | import { IMutationAlgorithm, IMutationContext } from './algorithms'; 2 | import { pickOne } from '../Math'; 3 | import { injectable, inject } from 'inversify'; 4 | import { MutationAlgorithms } from '../dependencies'; 5 | 6 | /** 7 | * Mutation manager. 8 | */ 9 | @injectable() 10 | export class Mutator { 11 | private context: IMutationContext = { literals: [] }; 12 | 13 | constructor( 14 | @inject(MutationAlgorithms) 15 | private readonly algorithms: ReadonlyArray, 16 | ) {} 17 | 18 | /** 19 | * Adds a new literal that can be used for mutations. 20 | */ 21 | public addLiterals(literals: ReadonlyArray) { 22 | this.context = { 23 | ...this.context, 24 | literals: [...this.context.literals, ...literals], 25 | }; 26 | } 27 | 28 | /** 29 | * Mutates the input buffer. 30 | */ 31 | public mutate(input: Buffer) { 32 | let mutations = 1; 33 | while (Math.random() < 0.5) { 34 | mutations += 1; 35 | } 36 | 37 | for (let i = 0; i < mutations; i += 1) { 38 | const next = pickOne(this.algorithms).mutate(input, this.context); 39 | if (next !== null) { 40 | input = next; 41 | } else { 42 | i -= 1; 43 | } 44 | } 45 | 46 | return input; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/options.ts: -------------------------------------------------------------------------------- 1 | import { IInstrumenterOptions } from "./instrumentation/coverage-instrumentor"; 2 | 3 | /** 4 | * Options passed in to instantiate the cluster. 5 | */ 6 | export interface IFuzzOptions { 7 | /** 8 | * The number of worker processes to create. 9 | */ 10 | workers: number; 11 | 12 | /** 13 | * Absolute path to the target script to generate coverage on. 14 | */ 15 | target: string; 16 | 17 | /** 18 | * Patterns for files or modules which should be excluded from coverage. 19 | */ 20 | exclude: string[]; 21 | 22 | /** 23 | * Length of time we kill worker processes after, given in milliseconds, if 24 | * they don't produce results. 25 | */ 26 | timeout: number; 27 | 28 | /** 29 | * Instrumentation options. 30 | */ 31 | instrumentor?: Partial; 32 | } 33 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/protocol/fuzz.proto: -------------------------------------------------------------------------------- 1 | package fuzz; 2 | syntax = "proto3"; 3 | 4 | message Ready {} 5 | 6 | message RequestCoverage {} 7 | 8 | message WorkSummary { 9 | uint32 result = 1; 10 | string hash = 2; 11 | uint32 runtime = 3; 12 | string error = 4; 13 | } 14 | 15 | message WorkCoverage { 16 | bytes coverage = 1; 17 | } 18 | 19 | message DoWork { 20 | bytes input = 1; 21 | } 22 | 23 | message FoundLiterals { 24 | repeated string literals = 1; 25 | } 26 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/protocol/protocol.ts: -------------------------------------------------------------------------------- 1 | import * as protobufjs from 'protobufjs'; 2 | import { Observable, Subject } from 'rxjs'; 3 | import { IPCCall, PacketKind } from './types'; 4 | 5 | /** 6 | * Protocol implements a super simple protobuf-based encoding on top of 7 | * readable and writable streams. Each message is length-prefix with a varint. 8 | */ 9 | export class Protocol { 10 | private output?: NodeJS.WritableStream; 11 | private messages: { [name: string]: protobufjs.Type } = require('./fuzz').fuzz; 12 | private inputBuffer = new RWBuffer(); 13 | 14 | /** 15 | * Attaches an ipc reader/writer to the streams. 16 | */ 17 | public attach(input: NodeJS.ReadableStream, output: NodeJS.WritableStream): Observable { 18 | this.output = output; 19 | 20 | const out = new Subject(); 21 | input.on('data', (data: Buffer) => { 22 | this.inputBuffer.write(data); 23 | while (true) { 24 | const parsed = this.decodeInputs(); 25 | if (!parsed) { 26 | return; 27 | } 28 | 29 | try { 30 | out.next(parsed); 31 | } catch (err) { 32 | out.error(err); 33 | return; 34 | } 35 | } 36 | }); 37 | 38 | input.on('error', err => out.error(err)); 39 | input.on('end', () => out.complete()); 40 | 41 | return out; 42 | } 43 | 44 | /** 45 | * Writes the ipc call to the output stream. 46 | */ 47 | public write(message: IPCCall) { 48 | if (!this.output) { 49 | throw new Error('Cannot write() without first calling attach()'); 50 | } 51 | 52 | this.output.write(new Buffer([message.kind])); 53 | this.output.write( 54 | Buffer.from(this.messages[PacketKind[message.kind]].encodeDelimited(message).finish()), 55 | ); 56 | } 57 | 58 | /** 59 | * Ends the underlying output stream. 60 | */ 61 | public end() { 62 | if (this.output) { 63 | this.output.end(); 64 | } 65 | } 66 | 67 | private decodeInputs() { 68 | const kindId = this.inputBuffer.peek(); 69 | const unread = this.inputBuffer.getUnread(); 70 | return this.decodeMessage(kindId, unread); 71 | } 72 | 73 | private decodeMessage(kindId: number, input: Buffer): IPCCall | null { 74 | const kind = this.messages[(PacketKind)[String(kindId)]]; 75 | if (!kind) { 76 | throw new Error(`Corrupt data stream: unknown message kind ${kindId}`); 77 | } 78 | 79 | const reader = new protobufjs.BufferReader(input); 80 | let output: IPCCall; 81 | try { 82 | output = kind.decodeDelimited(reader); 83 | } catch (e) { 84 | if (!(e instanceof RangeError)) { 85 | throw new Error(`Error parsing data stream: ${e.message}`); 86 | } 87 | 88 | this.inputBuffer.advanceRead(-1); 89 | return null; // rather naive to buffer infinitely, 90 | } 91 | 92 | this.inputBuffer.advanceRead(reader.pos); 93 | output.kind = kindId; 94 | return output; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/protocol/rw-buffer.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { RWBuffer } from './rw-buffer'; 3 | 4 | describe('RWBuffer', () => { 5 | it('sets up correctly', () => { 6 | const buffer = new RWBuffer(4); 7 | expect(buffer.underlyingSize()).to.equal(4); 8 | expect(buffer.length()).to.equal(0); 9 | expect(buffer.getUnread()).to.deep.equal(Buffer.from([])); 10 | }); 11 | 12 | it('writes small data', () => { 13 | const buffer = new RWBuffer(4); 14 | buffer.write(Buffer.from('hi')); 15 | expect(buffer.underlyingSize()).to.equal(4); 16 | expect(buffer.length()).to.equal(2); 17 | expect(buffer.getUnread()).to.deep.equal(Buffer.from('hi')); 18 | }); 19 | 20 | it('advances pointer', () => { 21 | const buffer = new RWBuffer(4); 22 | buffer.write(Buffer.from('hi')); 23 | buffer.advanceRead(2); 24 | 25 | expect(buffer.underlyingSize()).to.equal(4); 26 | expect(buffer.length()).to.equal(0); 27 | expect(buffer.getUnread()).to.deep.equal(Buffer.from([])); 28 | }); 29 | 30 | it('shifts data without reallocating', () => { 31 | const buffer = new RWBuffer(4); 32 | buffer.write(Buffer.from('hi')); 33 | buffer.advanceRead(2); 34 | buffer.write(Buffer.from('hiya')); 35 | 36 | expect(buffer.underlyingSize()).to.equal(4); 37 | expect(buffer.length()).to.equal(4); 38 | expect(buffer.getUnread()).to.deep.equal(Buffer.from('hiya')); 39 | }); 40 | 41 | it('grows on large data', () => { 42 | const buffer = new RWBuffer(4); 43 | buffer.write(Buffer.from('hello world')); 44 | 45 | expect(buffer.underlyingSize()).to.equal(16); 46 | expect(buffer.length()).to.equal(11); 47 | expect(buffer.getUnread()).to.deep.equal(Buffer.from('hello world')); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/protocol/rw-buffer.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * RWBuffer is a simple implementation of a byte buffer with read and write pointers. 4 | */ 5 | export class RWBuffer { 6 | private underlying: Buffer; 7 | private writePtr = 0; 8 | private readPtr = 0; 9 | 10 | constructor(size: number = 10 * 1024 * 1024) { 11 | this.underlying = Buffer.allocUnsafe(size); 12 | } 13 | 14 | /** 15 | * Copies the input buffer to the underlying storage. 16 | */ 17 | public write(input: Buffer) { 18 | this.grow(input.length); 19 | input.copy(this.underlying, this.writePtr); 20 | this.writePtr += input.length; 21 | } 22 | 23 | /** 24 | * Returns the slice of the buffer that has been written but not read yet. 25 | * This will share memory with the underlying buffer and is NOT safe to 26 | * mutate or use after write() is called. 27 | */ 28 | public getUnread(): Buffer { 29 | return this.underlying.slice(this.readPtr, this.writePtr); 30 | } 31 | 32 | /** 33 | * Advances the read pointer by the given amount. 34 | */ 35 | public advanceRead(amount: number) { 36 | this.readPtr += amount; 37 | } 38 | 39 | /** 40 | * Returns the number of unread bytes in the buffer. 41 | */ 42 | public length() { 43 | return this.writePtr - this.readPtr; 44 | } 45 | 46 | /** 47 | * Gets the underlying buffer size. 48 | */ 49 | public underlyingSize() { 50 | return this.underlying.length; 51 | } 52 | 53 | /** 54 | * Peeks at the next byte in the buffer, advancing the read pointer. 55 | */ 56 | public peek(): number { 57 | if (this.readPtr === this.writePtr) { 58 | throw new RangeError('out of bound'); 59 | } 60 | 61 | const byte = this.underlying.readUInt8(this.readPtr); 62 | this.readPtr += 1; 63 | return byte; 64 | } 65 | 66 | /** 67 | * Grows the underlying buffer to ensure there's space to write the 68 | * provided message. 69 | */ 70 | private grow(size: number) { 71 | // Grow if the message is too large to fit in our buffer at all. 72 | for (let ulen = this.underlying.length; size > ulen; ulen *= 2) { 73 | const next = Buffer.allocUnsafe(ulen * 2); 74 | this.underlying.copy(next, 0, this.readPtr, this.writePtr); 75 | this.writePtr -= this.readPtr; 76 | this.readPtr = 0; 77 | this.underlying = next; 78 | } 79 | 80 | // Reset the pointers and positioning if writing the message would go 81 | // past the end of the buffer. 82 | if (this.writePtr + size >= this.underlying.length) { 83 | this.underlying.copy(this.underlying, 0, this.readPtr, this.writePtr); 84 | this.writePtr -= this.readPtr; 85 | this.readPtr = 0; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/protocol/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type delimiter for IPCCalls. 3 | */ 4 | export enum PacketKind { 5 | Ready, 6 | WorkSummary, 7 | RequestCoverage, 8 | WorkCoverage, 9 | DoWork, 10 | FoundLiterals, 11 | } 12 | 13 | /** 14 | * An IReady message is sent from workers when their code is loaded and ready to go. 15 | */ 16 | export interface IReadyCall { 17 | kind: PacketKind.Ready; 18 | } 19 | 20 | export enum WorkResult { 21 | Ignore, 22 | Allow, 23 | Reinforce, 24 | Error, 25 | } 26 | 27 | /** 28 | * An IRequestCoverage is sent from the master to the slave if the work 29 | * resulted in something that looks interesting. 30 | */ 31 | export interface IRequestCoverage { 32 | kind: PacketKind.RequestCoverage; 33 | } 34 | 35 | /** 36 | * A WorkSummary is sent from the slave to the master when work is completed. 37 | */ 38 | export interface IWorkSummary { 39 | kind: PacketKind.WorkSummary; 40 | result: WorkResult; 41 | coverageSize: number; 42 | inputLength: number; 43 | hash: string; 44 | runtime: number; // given in microseconds 45 | error?: string; 46 | } 47 | 48 | /** 49 | * An IWorkCoverage is sent in response to an IRequestCoverage message. 50 | */ 51 | export interface IWorkCoverage { 52 | kind: PacketKind.WorkCoverage; 53 | coverage: Buffer; 54 | } 55 | 56 | /** 57 | * IDoWork is sent to signal a slave that we want to fuzz the given input. 58 | */ 59 | export interface IDoWork { 60 | kind: PacketKind.DoWork; 61 | input: Buffer; 62 | } 63 | 64 | export type IPCCall = 65 | | IWorkSummary 66 | | IDoWork 67 | | IReadyCall 68 | | IWorkCoverage 69 | | IRequestCoverage; 70 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/runtime/coverage-hash.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "inversify"; 2 | import { createHash } from "crypto"; 3 | import { Inliner } from "./inliner"; 4 | 5 | /** 6 | * Hash bit size. A constant in go-fuzz. 64KB. 7 | */ 8 | const hashBits = 64 << 10; 9 | 10 | const idBuffer = Buffer.alloc(4); 11 | 12 | /** 13 | * Creates a new ID for the given coverage block. 14 | */ 15 | export function createCoverageId(counter: number) { 16 | idBuffer.writeInt32BE(counter | 0, 0) // bitwise force to an int32 17 | return createHash('sha1').update(idBuffer).digest().readUInt32BE(0); 18 | } 19 | 20 | /** 21 | * A service passed into the Runtime which provides a hash table that 22 | * records coverage. 23 | */ 24 | export interface ICoverageHash { 25 | increment: Inliner; 26 | reset(): void; 27 | } 28 | 29 | /** 30 | * A service passed into the Runtime which provides a hash table that 31 | * records coverage. 32 | */ 33 | @injectable() 34 | export class CoverageHash implements ICoverageHash { 35 | private hash = Buffer.alloc(hashBits); 36 | 37 | public readonly increment = new Inliner((ctx, id) => ({ 38 | type: 'UpdateExpression', 39 | operator: '++', 40 | prefix: false, 41 | argument: { 42 | type: 'MemberExpression', 43 | object: ctx.accessOwnProperty('hash'), 44 | property: id, 45 | computed: true, 46 | }, 47 | })) 48 | 49 | public reset() { 50 | this.hash.fill(0); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/runtime/inliner.ts: -------------------------------------------------------------------------------- 1 | import { Expression, MemberExpression } from 'estree'; 2 | 3 | export interface IInliningContext { 4 | accessOwnProperty(property: string): MemberExpression; 5 | } 6 | 7 | /** 8 | * Type that can be exposed in the Runtime services which, when inserted in 9 | * generated code, will inline the generated expression instead of creating a 10 | * method call. Used for hot-path optimization. 11 | */ 12 | export class Inliner { 13 | constructor( 14 | public readonly generator: (context: IInliningContext, ...args: Expression[]) => Expression, 15 | ) {} 16 | } 17 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/runtime/runtime-service-collection.ts: -------------------------------------------------------------------------------- 1 | import { CoverageHash, ICoverageHash } from "./coverage-hash"; 2 | import { injectable, inject } from "inversify"; 3 | import * as Types from "../dependencies"; 4 | 5 | export interface IRuntimeServices { 6 | coverage: ICoverageHash; 7 | } 8 | 9 | /** 10 | * Collection of invokable runtime services. 11 | */ 12 | @injectable() 13 | export class RuntimeServiceCollection implements IRuntimeServices { 14 | constructor( 15 | @inject(Types.CoverageHashService) 16 | public readonly coverage: CoverageHash 17 | ) {} 18 | 19 | public reset() { 20 | this.coverage.reset(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/src/runtime/runtime.ts: -------------------------------------------------------------------------------- 1 | import { Expression, MemberExpression } from 'estree'; 2 | import { IRuntimeServices, RuntimeServiceCollection } from './runtime-service-collection'; 3 | import { inject, injectable } from 'inversify'; 4 | import * as Types from '../dependencies'; 5 | import { Inliner } from './inliner'; 6 | 7 | /** 8 | * Global name for the runtime variable the Runtime instance is stored in. 9 | */ 10 | const globalName = '__js_fuzz_'; 11 | 12 | declare const global: { [key: string]: any }; 13 | 14 | @injectable() 15 | export class Runtime { 16 | constructor( 17 | @inject(Types.RuntimeServiceCollection) 18 | public readonly services: RuntimeServiceCollection, 19 | ) {} 20 | 21 | /** 22 | * Installs the runtime global. 23 | */ 24 | public install() { 25 | global[globalName] = this; 26 | } 27 | 28 | /** 29 | * Resets all services. 30 | */ 31 | public reset() { 32 | this.services.reset(); 33 | } 34 | 35 | /** 36 | * Creates an ES node that calls an expression on the runtime. 37 | */ 38 | public call( 39 | service: K, 40 | method: keyof { 41 | [J in keyof IRuntimeServices[K]]: IRuntimeServices[K][J] extends 42 | | ((...args: Expression[]) => Expression) 43 | | Inliner 44 | ? true 45 | : never 46 | }, 47 | ...args: Expression[] 48 | ): Expression { 49 | const implementation = this.services[service][method]; 50 | if (implementation instanceof Inliner) { 51 | return implementation.generator( 52 | { accessOwnProperty: prop => this.createPropertyAccess(service, prop as any) }, 53 | ...args, 54 | ); 55 | } 56 | 57 | return { 58 | type: 'CallExpression', 59 | callee: this.createPropertyAccess(service, method), 60 | arguments: args, 61 | }; 62 | } 63 | 64 | private createPropertyAccess( 65 | service: K, 66 | method: keyof { 67 | [J in keyof IRuntimeServices[K]]: IRuntimeServices[K][J] extends 68 | | ((...args: Expression[]) => Expression) 69 | | Inliner 70 | ? true 71 | : never 72 | }, 73 | ): MemberExpression { 74 | return { 75 | type: 'MemberExpression', 76 | computed: false, 77 | object: { 78 | type: 'MemberExpression', 79 | computed: false, 80 | object: { 81 | type: 'Identifier', 82 | name: globalName, 83 | }, 84 | property: { 85 | type: 'Identifier', 86 | name: service, 87 | }, 88 | }, 89 | property: { 90 | type: 'Identifier', 91 | name: method as string, 92 | }, 93 | }; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/test/bench/hashStore.bench.js: -------------------------------------------------------------------------------- 1 | const HashStore = require('../../lib/src/HashStore').HashStore 2 | const store = new HashStore() 3 | const crypto = require('crypto') 4 | const buf = crypto.randomBytes(1024 * 64) 5 | 6 | let a = true 7 | bench('exists', () => a = a && store.exists(store.getHashFor(buf), buf)) 8 | 9 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/test/bench/instrumentation.bench.js: -------------------------------------------------------------------------------- 1 | // let us bench against Istanbul's coverage, just for kicks. 2 | const testIstanbul = process.argv.indexOf('--istanbul') > -1 3 | let istInstrumenter 4 | try { istInstrumenter = new (require('istanbul').Instrumenter)() } catch (e) {} 5 | 6 | const cases = require('../util').loadInstrumentationFixtures() 7 | 8 | let incr = 0 9 | global.__coverage__ = [] 10 | global.__coverage___prevState = 0 11 | global.foo = global.bin = global.baz = global.bar = () => { 12 | incr++ 13 | return true 14 | } 15 | 16 | cases.forEach(tcase => { 17 | suite(tcase.name, () => { 18 | const before = new Function(tcase.before) 19 | const after = new Function(tcase.after) 20 | set('mintime', 1000) 21 | bench('before', () => before()) 22 | bench('after', () => after()) 23 | 24 | if (testIstanbul) { 25 | const afterInstanbul = new Function(istInstrumenter.instrumentSync(tcase.before)) 26 | bench('after (istanbul)', () => afterInstanbul()) 27 | } 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/test/fixture/instrument/if-statements.after.txt: -------------------------------------------------------------------------------- 1 | if (foo()) { 2 | __js_fuzz_.coverage.hash[2422852216]++; 3 | bar(); 4 | } 5 | if (foo()) { 6 | __js_fuzz_.coverage.hash[1201538291]++; 7 | bar(); 8 | } 9 | if (foo()) { 10 | __js_fuzz_.coverage.hash[4283873684]++; 11 | bar(); 12 | } else { 13 | __js_fuzz_.coverage.hash[2752366794]++; 14 | baz(); 15 | } 16 | if ((__js_fuzz_.coverage.hash[3178677100]++, foo()) || (__js_fuzz_.coverage.hash[1101346672]++, bin())) { 17 | __js_fuzz_.coverage.hash[4039726864]++; 18 | bar(); 19 | } else { 20 | __js_fuzz_.coverage.hash[14765166]++; 21 | baz(); 22 | } -------------------------------------------------------------------------------- /packages/js-fuzz-core/test/fixture/instrument/if-statements.before.txt: -------------------------------------------------------------------------------- 1 | if (foo()) { 2 | bar(); 3 | } 4 | 5 | if (foo()) 6 | bar(); 7 | 8 | if (foo()) 9 | bar(); 10 | else 11 | baz(); 12 | 13 | if (foo() || bin()) { 14 | bar(); 15 | } else { 16 | baz(); 17 | } 18 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/test/fixture/instrument/if-statements.literals.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /packages/js-fuzz-core/test/fixture/instrument/json2.after.txt: -------------------------------------------------------------------------------- 1 | const JSON2 = function () { 2 | 'use strict'; 3 | var rx_one = /^[\],:{}\s]*$/; 4 | var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g; 5 | var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g; 6 | var rx_four = /(?:^|:|,)(?:\s*\[)+/g; 7 | var rx_escapable = /[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; 8 | var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; 9 | function f(n) { 10 | return n < 10 ? (__js_fuzz_.coverage.hash[2738366220]++, '0' + n) : (__js_fuzz_.coverage.hash[4273908027]++, n); 11 | } 12 | function this_value() { 13 | return this.valueOf(); 14 | } 15 | if (typeof Date.prototype.toJSON !== 'function') { 16 | __js_fuzz_.coverage.hash[3898928360]++; 17 | Date.prototype.toJSON = function () { 18 | return isFinite(this.valueOf()) ? (__js_fuzz_.coverage.hash[2559261518]++, this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z') : (__js_fuzz_.coverage.hash[44248990]++, null); 19 | }; 20 | Boolean.prototype.toJSON = this_value; 21 | Number.prototype.toJSON = this_value; 22 | String.prototype.toJSON = this_value; 23 | } 24 | var gap; 25 | var indent; 26 | var meta; 27 | var rep; 28 | function quote(string) { 29 | rx_escapable.lastIndex = 0; 30 | return rx_escapable.test(string) ? (__js_fuzz_.coverage.hash[895566665]++, '"' + string.replace(rx_escapable, function (a) { 31 | var c = meta[a]; 32 | return typeof c === 'string' ? (__js_fuzz_.coverage.hash[4066184513]++, c) : (__js_fuzz_.coverage.hash[1806463981]++, '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4)); 33 | }) + '"') : (__js_fuzz_.coverage.hash[3441304185]++, '"' + string + '"'); 34 | } 35 | function str(key, holder) { 36 | var i; 37 | var k; 38 | var v; 39 | var length; 40 | var mind = gap; 41 | var partial; 42 | var value = holder[key]; 43 | if ((__js_fuzz_.coverage.hash[1278105095]++, (__js_fuzz_.coverage.hash[3821930904]++, value) && (__js_fuzz_.coverage.hash[1595934395]++, typeof value === 'object')) && (__js_fuzz_.coverage.hash[1474996517]++, typeof value.toJSON === 'function')) { 44 | __js_fuzz_.coverage.hash[1357922824]++; 45 | value = value.toJSON(key); 46 | } 47 | if (typeof rep === 'function') { 48 | __js_fuzz_.coverage.hash[3227152405]++; 49 | value = rep.call(holder, key, value); 50 | } 51 | switch (typeof value) { 52 | case 'string': 53 | __js_fuzz_.coverage.hash[1230373540]++; 54 | return quote(value); 55 | case 'number': 56 | __js_fuzz_.coverage.hash[3448365116]++; 57 | return isFinite(value) ? (__js_fuzz_.coverage.hash[1533432938]++, String(value)) : (__js_fuzz_.coverage.hash[3654804381]++, 'null'); 58 | case 'boolean': 59 | __js_fuzz_.coverage.hash[3854834486]++; 60 | case 'null': 61 | __js_fuzz_.coverage.hash[90780359]++; 62 | return String(value); 63 | case 'object': 64 | __js_fuzz_.coverage.hash[2459674944]++; 65 | if (!value) { 66 | __js_fuzz_.coverage.hash[3269082493]++; 67 | return 'null'; 68 | } 69 | gap += indent; 70 | partial = []; 71 | if (Object.prototype.toString.apply(value) === '[object Array]') { 72 | __js_fuzz_.coverage.hash[4153537217]++; 73 | length = value.length; 74 | for (i = 0; i < length; i += 1) { 75 | partial[i] = (__js_fuzz_.coverage.hash[1969033073]++, str(i, value)) || (__js_fuzz_.coverage.hash[2487027726]++, 'null'); 76 | } 77 | v = partial.length === 0 ? (__js_fuzz_.coverage.hash[2499520749]++, '[]') : (__js_fuzz_.coverage.hash[3047903615]++, gap ? (__js_fuzz_.coverage.hash[2019945121]++, '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']') : (__js_fuzz_.coverage.hash[3342301869]++, '[' + partial.join(',') + ']')); 78 | gap = mind; 79 | return v; 80 | } 81 | if ((__js_fuzz_.coverage.hash[3307490204]++, rep) && (__js_fuzz_.coverage.hash[3791378227]++, typeof rep === 'object')) { 82 | __js_fuzz_.coverage.hash[4234007692]++; 83 | length = rep.length; 84 | for (i = 0; i < length; i += 1) { 85 | if (typeof rep[i] === 'string') { 86 | __js_fuzz_.coverage.hash[636536630]++; 87 | k = rep[i]; 88 | v = str(k, value); 89 | if (v) { 90 | __js_fuzz_.coverage.hash[93738981]++; 91 | partial.push(quote(k) + (gap ? (__js_fuzz_.coverage.hash[1842654232]++, ': ') : (__js_fuzz_.coverage.hash[968243656]++, ':')) + v); 92 | } 93 | } 94 | } 95 | } else { 96 | __js_fuzz_.coverage.hash[2414534340]++; 97 | for (k in value) { 98 | if (Object.prototype.hasOwnProperty.call(value, k)) { 99 | __js_fuzz_.coverage.hash[659409714]++; 100 | v = str(k, value); 101 | if (v) { 102 | __js_fuzz_.coverage.hash[3942752827]++; 103 | partial.push(quote(k) + (gap ? (__js_fuzz_.coverage.hash[3849098768]++, ': ') : (__js_fuzz_.coverage.hash[2781344851]++, ':')) + v); 104 | } 105 | } 106 | } 107 | } 108 | v = partial.length === 0 ? (__js_fuzz_.coverage.hash[935272587]++, '{}') : (__js_fuzz_.coverage.hash[3585905550]++, gap ? (__js_fuzz_.coverage.hash[1793822355]++, '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}') : (__js_fuzz_.coverage.hash[1595738845]++, '{' + partial.join(',') + '}')); 109 | gap = mind; 110 | return v; 111 | } 112 | } 113 | const JSON = {}; 114 | if (typeof JSON.stringify !== 'function') { 115 | __js_fuzz_.coverage.hash[2748906447]++; 116 | meta = { 117 | '\b': '\\b', 118 | '\t': '\\t', 119 | '\n': '\\n', 120 | '\f': '\\f', 121 | '\r': '\\r', 122 | '"': '\\"', 123 | '\\': '\\\\' 124 | }; 125 | JSON.stringify = function (value, replacer, space) { 126 | var i; 127 | gap = ''; 128 | indent = ''; 129 | if (typeof space === 'number') { 130 | __js_fuzz_.coverage.hash[704365311]++; 131 | for (i = 0; i < space; i += 1) { 132 | indent += ' '; 133 | } 134 | } else { 135 | __js_fuzz_.coverage.hash[1046349797]++; 136 | if (typeof space === 'string') { 137 | __js_fuzz_.coverage.hash[1288345544]++; 138 | indent = space; 139 | } 140 | } 141 | rep = replacer; 142 | if ((__js_fuzz_.coverage.hash[961578865]++, (__js_fuzz_.coverage.hash[2852412675]++, replacer) && (__js_fuzz_.coverage.hash[2452751470]++, typeof replacer !== 'function')) && (__js_fuzz_.coverage.hash[4262135822]++, (__js_fuzz_.coverage.hash[4246658171]++, typeof replacer !== 'object') || (__js_fuzz_.coverage.hash[1319220212]++, typeof replacer.length !== 'number'))) { 143 | __js_fuzz_.coverage.hash[2621467693]++; 144 | throw new Error('JSON.stringify'); 145 | } 146 | return str('', { '': value }); 147 | }; 148 | } 149 | if (typeof JSON.parse !== 'function') { 150 | __js_fuzz_.coverage.hash[2130980127]++; 151 | JSON.parse = function (text, reviver) { 152 | var j; 153 | function walk(holder, key) { 154 | var k; 155 | var v; 156 | var value = holder[key]; 157 | if ((__js_fuzz_.coverage.hash[1687832855]++, value) && (__js_fuzz_.coverage.hash[3612642538]++, typeof value === 'object')) { 158 | __js_fuzz_.coverage.hash[2417573299]++; 159 | for (k in value) { 160 | if (Object.prototype.hasOwnProperty.call(value, k)) { 161 | __js_fuzz_.coverage.hash[99732790]++; 162 | v = walk(value, k); 163 | if (v !== undefined) { 164 | __js_fuzz_.coverage.hash[3089291410]++; 165 | value[k] = v; 166 | } else { 167 | __js_fuzz_.coverage.hash[4249717532]++; 168 | delete value[k]; 169 | } 170 | } 171 | } 172 | } 173 | return reviver.call(holder, key, value); 174 | } 175 | text = String(text); 176 | rx_dangerous.lastIndex = 0; 177 | if (rx_dangerous.test(text)) { 178 | __js_fuzz_.coverage.hash[929821404]++; 179 | text = text.replace(rx_dangerous, function (a) { 180 | return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 181 | }); 182 | } 183 | if (rx_one.test(text.replace(rx_two, '@').replace(rx_three, ']').replace(rx_four, ''))) { 184 | __js_fuzz_.coverage.hash[258148855]++; 185 | j = eval('(' + text + ')'); 186 | return typeof reviver === 'function' ? (__js_fuzz_.coverage.hash[1403979226]++, walk({ '': j }, '')) : (__js_fuzz_.coverage.hash[2050152445]++, j); 187 | } 188 | throw new SyntaxError('JSON.parse'); 189 | }; 190 | } 191 | return JSON; 192 | }(); 193 | JSON.parse(`{ 194 | "type": "Program", 195 | "body": [ 196 | { 197 | "type": "IfStatement", 198 | "test": { 199 | "type": "CallExpression", 200 | "callee": { 201 | "type": "Identifier", 202 | "name": "foo" 203 | }, 204 | "arguments": [] 205 | }, 206 | "consequent": { 207 | "type": "BlockStatement", 208 | "body": [ 209 | { 210 | "type": "ExpressionStatement", 211 | "expression": { 212 | "type": "SequenceExpression", 213 | "expressions": [ 214 | { 215 | "type": "Identifier", 216 | "name": "foo" 217 | }, 218 | { 219 | "type": "CallExpression", 220 | "callee": { 221 | "type": "Identifier", 222 | "name": "bar" 223 | }, 224 | "arguments": [] 225 | } 226 | ] 227 | } 228 | } 229 | ] 230 | }, 231 | "alternate": null 232 | }, 233 | { 234 | "type": "IfStatement", 235 | "test": { 236 | "type": "CallExpression", 237 | "callee": { 238 | "type": "Identifier", 239 | "name": "foo" 240 | }, 241 | "arguments": [] 242 | }, 243 | "consequent": { 244 | "type": "ExpressionStatement", 245 | "expression": { 246 | "type": "CallExpression", 247 | "callee": { 248 | "type": "Identifier", 249 | "name": "bar" 250 | }, 251 | "arguments": [] 252 | } 253 | }, 254 | "alternate": null 255 | }, 256 | { 257 | "type": "IfStatement", 258 | "test": { 259 | "type": "CallExpression", 260 | "callee": { 261 | "type": "Identifier", 262 | "name": "foo" 263 | }, 264 | "arguments": [] 265 | }, 266 | "consequent": { 267 | "type": "ExpressionStatement", 268 | "expression": { 269 | "type": "CallExpression", 270 | "callee": { 271 | "type": "Identifier", 272 | "name": "bar" 273 | }, 274 | "arguments": [] 275 | } 276 | }, 277 | "alternate": { 278 | "type": "ExpressionStatement", 279 | "expression": { 280 | "type": "CallExpression", 281 | "callee": { 282 | "type": "Identifier", 283 | "name": "baz" 284 | }, 285 | "arguments": [] 286 | } 287 | } 288 | }, 289 | { 290 | "type": "IfStatement", 291 | "test": { 292 | "type": "LogicalExpression", 293 | "operator": "||", 294 | "left": { 295 | "type": "SequenceExpression", 296 | "expressions": [ 297 | { 298 | "type": "CallExpression", 299 | "callee": { 300 | "type": "Identifier", 301 | "name": "whatever" 302 | }, 303 | "arguments": [] 304 | }, 305 | { 306 | "type": "CallExpression", 307 | "callee": { 308 | "type": "Identifier", 309 | "name": "foo" 310 | }, 311 | "arguments": [] 312 | } 313 | ] 314 | }, 315 | "right": { 316 | "type": "CallExpression", 317 | "callee": { 318 | "type": "Identifier", 319 | "name": "bin" 320 | }, 321 | "arguments": [] 322 | } 323 | }, 324 | "consequent": { 325 | "type": "BlockStatement", 326 | "body": [ 327 | { 328 | "type": "ExpressionStatement", 329 | "expression": { 330 | "type": "CallExpression", 331 | "callee": { 332 | "type": "Identifier", 333 | "name": "bar" 334 | }, 335 | "arguments": [] 336 | } 337 | } 338 | ] 339 | }, 340 | "alternate": { 341 | "type": "BlockStatement", 342 | "body": [ 343 | { 344 | "type": "ExpressionStatement", 345 | "expression": { 346 | "type": "CallExpression", 347 | "callee": { 348 | "type": "Identifier", 349 | "name": "baz" 350 | }, 351 | "arguments": [] 352 | } 353 | } 354 | ] 355 | } 356 | } 357 | ], 358 | "sourceType": "script" 359 | }`); -------------------------------------------------------------------------------- /packages/js-fuzz-core/test/fixture/instrument/json2.before.txt: -------------------------------------------------------------------------------- 1 | // Douglas Crockford's JSON implementation, from https://github.com/douglascrockford/JSON-js/blob/master/json2.js 2 | // Used as a test for some "real world" code ;) 3 | 4 | const JSON2 = (function () { 5 | "use strict"; 6 | 7 | var rx_one = /^[\],:{}\s]*$/; 8 | var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g; 9 | var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g; 10 | var rx_four = /(?:^|:|,)(?:\s*\[)+/g; 11 | var rx_escapable = /[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; 12 | var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; 13 | 14 | function f(n) { 15 | // Format integers to have at least two digits. 16 | return n < 10 17 | ? "0" + n 18 | : n; 19 | } 20 | 21 | function this_value() { 22 | return this.valueOf(); 23 | } 24 | 25 | if (typeof Date.prototype.toJSON !== "function") { 26 | 27 | Date.prototype.toJSON = function () { 28 | 29 | return isFinite(this.valueOf()) 30 | ? this.getUTCFullYear() + "-" + 31 | f(this.getUTCMonth() + 1) + "-" + 32 | f(this.getUTCDate()) + "T" + 33 | f(this.getUTCHours()) + ":" + 34 | f(this.getUTCMinutes()) + ":" + 35 | f(this.getUTCSeconds()) + "Z" 36 | : null; 37 | }; 38 | 39 | Boolean.prototype.toJSON = this_value; 40 | Number.prototype.toJSON = this_value; 41 | String.prototype.toJSON = this_value; 42 | } 43 | 44 | var gap; 45 | var indent; 46 | var meta; 47 | var rep; 48 | 49 | 50 | function quote(string) { 51 | 52 | // If the string contains no control characters, no quote characters, and no 53 | // backslash characters, then we can safely slap some quotes around it. 54 | // Otherwise we must also replace the offending characters with safe escape 55 | // sequences. 56 | 57 | rx_escapable.lastIndex = 0; 58 | return rx_escapable.test(string) 59 | ? "\"" + string.replace(rx_escapable, function (a) { 60 | var c = meta[a]; 61 | return typeof c === "string" 62 | ? c 63 | : "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4); 64 | }) + "\"" 65 | : "\"" + string + "\""; 66 | } 67 | 68 | 69 | function str(key, holder) { 70 | 71 | // Produce a string from holder[key]. 72 | 73 | var i; // The loop counter. 74 | var k; // The member key. 75 | var v; // The member value. 76 | var length; 77 | var mind = gap; 78 | var partial; 79 | var value = holder[key]; 80 | 81 | // If the value has a toJSON method, call it to obtain a replacement value. 82 | 83 | if (value && typeof value === "object" && 84 | typeof value.toJSON === "function") { 85 | value = value.toJSON(key); 86 | } 87 | 88 | // If we were called with a replacer function, then call the replacer to 89 | // obtain a replacement value. 90 | 91 | if (typeof rep === "function") { 92 | value = rep.call(holder, key, value); 93 | } 94 | 95 | // What happens next depends on the value's type. 96 | 97 | switch (typeof value) { 98 | case "string": 99 | return quote(value); 100 | 101 | case "number": 102 | 103 | // JSON numbers must be finite. Encode non-finite numbers as null. 104 | 105 | return isFinite(value) 106 | ? String(value) 107 | : "null"; 108 | 109 | case "boolean": 110 | case "null": 111 | 112 | // If the value is a boolean or null, convert it to a string. Note: 113 | // typeof null does not produce "null". The case is included here in 114 | // the remote chance that this gets fixed someday. 115 | 116 | return String(value); 117 | 118 | // If the type is "object", we might be dealing with an object or an array or 119 | // null. 120 | 121 | case "object": 122 | 123 | // Due to a specification blunder in ECMAScript, typeof null is "object", 124 | // so watch out for that case. 125 | 126 | if (!value) { 127 | return "null"; 128 | } 129 | 130 | // Make an array to hold the partial results of stringifying this object value. 131 | 132 | gap += indent; 133 | partial = []; 134 | 135 | // Is the value an array? 136 | 137 | if (Object.prototype.toString.apply(value) === "[object Array]") { 138 | 139 | // The value is an array. Stringify every element. Use null as a placeholder 140 | // for non-JSON values. 141 | 142 | length = value.length; 143 | for (i = 0; i < length; i += 1) { 144 | partial[i] = str(i, value) || "null"; 145 | } 146 | 147 | // Join all of the elements together, separated with commas, and wrap them in 148 | // brackets. 149 | 150 | v = partial.length === 0 151 | ? "[]" 152 | : gap 153 | ? "[\n" + gap + partial.join(",\n" + gap) + "\n" + mind + "]" 154 | : "[" + partial.join(",") + "]"; 155 | gap = mind; 156 | return v; 157 | } 158 | 159 | // If the replacer is an array, use it to select the members to be stringified. 160 | 161 | if (rep && typeof rep === "object") { 162 | length = rep.length; 163 | for (i = 0; i < length; i += 1) { 164 | if (typeof rep[i] === "string") { 165 | k = rep[i]; 166 | v = str(k, value); 167 | if (v) { 168 | partial.push(quote(k) + ( 169 | gap 170 | ? ": " 171 | : ":" 172 | ) + v); 173 | } 174 | } 175 | } 176 | } else { 177 | 178 | // Otherwise, iterate through all of the keys in the object. 179 | 180 | for (k in value) { 181 | if (Object.prototype.hasOwnProperty.call(value, k)) { 182 | v = str(k, value); 183 | if (v) { 184 | partial.push(quote(k) + ( 185 | gap 186 | ? ": " 187 | : ":" 188 | ) + v); 189 | } 190 | } 191 | } 192 | } 193 | 194 | // Join all of the member texts together, separated with commas, 195 | // and wrap them in braces. 196 | 197 | v = partial.length === 0 198 | ? "{}" 199 | : gap 200 | ? "{\n" + gap + partial.join(",\n" + gap) + "\n" + mind + "}" 201 | : "{" + partial.join(",") + "}"; 202 | gap = mind; 203 | return v; 204 | } 205 | } 206 | 207 | // If the JSON object does not yet have a stringify method, give it one. 208 | 209 | const JSON = {}; 210 | 211 | if (typeof JSON.stringify !== "function") { 212 | meta = { // table of character substitutions 213 | "\b": "\\b", 214 | "\t": "\\t", 215 | "\n": "\\n", 216 | "\f": "\\f", 217 | "\r": "\\r", 218 | "\"": "\\\"", 219 | "\\": "\\\\" 220 | }; 221 | JSON.stringify = function (value, replacer, space) { 222 | 223 | // The stringify method takes a value and an optional replacer, and an optional 224 | // space parameter, and returns a JSON text. The replacer can be a function 225 | // that can replace values, or an array of strings that will select the keys. 226 | // A default replacer method can be provided. Use of the space parameter can 227 | // produce text that is more easily readable. 228 | 229 | var i; 230 | gap = ""; 231 | indent = ""; 232 | 233 | // If the space parameter is a number, make an indent string containing that 234 | // many spaces. 235 | 236 | if (typeof space === "number") { 237 | for (i = 0; i < space; i += 1) { 238 | indent += " "; 239 | } 240 | 241 | // If the space parameter is a string, it will be used as the indent string. 242 | 243 | } else if (typeof space === "string") { 244 | indent = space; 245 | } 246 | 247 | // If there is a replacer, it must be a function or an array. 248 | // Otherwise, throw an error. 249 | 250 | rep = replacer; 251 | if (replacer && typeof replacer !== "function" && 252 | (typeof replacer !== "object" || 253 | typeof replacer.length !== "number")) { 254 | throw new Error("JSON.stringify"); 255 | } 256 | 257 | // Make a fake root object containing our value under the key of "". 258 | // Return the result of stringifying the value. 259 | 260 | return str("", {"": value}); 261 | }; 262 | } 263 | 264 | 265 | // If the JSON object does not yet have a parse method, give it one. 266 | 267 | if (typeof JSON.parse !== "function") { 268 | JSON.parse = function (text, reviver) { 269 | 270 | // The parse method takes a text and an optional reviver function, and returns 271 | // a JavaScript value if the text is a valid JSON text. 272 | 273 | var j; 274 | 275 | function walk(holder, key) { 276 | 277 | // The walk method is used to recursively walk the resulting structure so 278 | // that modifications can be made. 279 | 280 | var k; 281 | var v; 282 | var value = holder[key]; 283 | if (value && typeof value === "object") { 284 | for (k in value) { 285 | if (Object.prototype.hasOwnProperty.call(value, k)) { 286 | v = walk(value, k); 287 | if (v !== undefined) { 288 | value[k] = v; 289 | } else { 290 | delete value[k]; 291 | } 292 | } 293 | } 294 | } 295 | return reviver.call(holder, key, value); 296 | } 297 | 298 | 299 | // Parsing happens in four stages. In the first stage, we replace certain 300 | // Unicode characters with escape sequences. JavaScript handles many characters 301 | // incorrectly, either silently deleting them, or treating them as line endings. 302 | 303 | text = String(text); 304 | rx_dangerous.lastIndex = 0; 305 | if (rx_dangerous.test(text)) { 306 | text = text.replace(rx_dangerous, function (a) { 307 | return "\\u" + 308 | ("0000" + a.charCodeAt(0).toString(16)).slice(-4); 309 | }); 310 | } 311 | 312 | // In the second stage, we run the text against regular expressions that look 313 | // for non-JSON patterns. We are especially concerned with "()" and "new" 314 | // because they can cause invocation, and "=" because it can cause mutation. 315 | // But just to be safe, we want to reject all unexpected forms. 316 | 317 | // We split the second stage into 4 regexp operations in order to work around 318 | // crippling inefficiencies in IE's and Safari's regexp engines. First we 319 | // replace the JSON backslash pairs with "@" (a non-JSON character). Second, we 320 | // replace all simple value tokens with "]" characters. Third, we delete all 321 | // open brackets that follow a colon or comma or that begin the text. Finally, 322 | // we look to see that the remaining characters are only whitespace or "]" or 323 | // "," or ":" or "{" or "}". If that is so, then the text is safe for eval. 324 | 325 | if ( 326 | rx_one.test( 327 | text 328 | .replace(rx_two, "@") 329 | .replace(rx_three, "]") 330 | .replace(rx_four, "") 331 | ) 332 | ) { 333 | 334 | // In the third stage we use the eval function to compile the text into a 335 | // JavaScript structure. The "{" operator is subject to a syntactic ambiguity 336 | // in JavaScript: it can begin a block or an object literal. We wrap the text 337 | // in parens to eliminate the ambiguity. 338 | 339 | j = eval("(" + text + ")"); 340 | 341 | // In the optional fourth stage, we recursively walk the new structure, passing 342 | // each name/value pair to a reviver function for possible transformation. 343 | 344 | return (typeof reviver === "function") 345 | ? walk({"": j}, "") 346 | : j; 347 | } 348 | 349 | // If the text is not JSON parseable, then a SyntaxError is thrown. 350 | 351 | throw new SyntaxError("JSON.parse"); 352 | }; 353 | } 354 | 355 | return JSON; 356 | }()); 357 | 358 | JSON.parse(`{ 359 | "type": "Program", 360 | "body": [ 361 | { 362 | "type": "IfStatement", 363 | "test": { 364 | "type": "CallExpression", 365 | "callee": { 366 | "type": "Identifier", 367 | "name": "foo" 368 | }, 369 | "arguments": [] 370 | }, 371 | "consequent": { 372 | "type": "BlockStatement", 373 | "body": [ 374 | { 375 | "type": "ExpressionStatement", 376 | "expression": { 377 | "type": "SequenceExpression", 378 | "expressions": [ 379 | { 380 | "type": "Identifier", 381 | "name": "foo" 382 | }, 383 | { 384 | "type": "CallExpression", 385 | "callee": { 386 | "type": "Identifier", 387 | "name": "bar" 388 | }, 389 | "arguments": [] 390 | } 391 | ] 392 | } 393 | } 394 | ] 395 | }, 396 | "alternate": null 397 | }, 398 | { 399 | "type": "IfStatement", 400 | "test": { 401 | "type": "CallExpression", 402 | "callee": { 403 | "type": "Identifier", 404 | "name": "foo" 405 | }, 406 | "arguments": [] 407 | }, 408 | "consequent": { 409 | "type": "ExpressionStatement", 410 | "expression": { 411 | "type": "CallExpression", 412 | "callee": { 413 | "type": "Identifier", 414 | "name": "bar" 415 | }, 416 | "arguments": [] 417 | } 418 | }, 419 | "alternate": null 420 | }, 421 | { 422 | "type": "IfStatement", 423 | "test": { 424 | "type": "CallExpression", 425 | "callee": { 426 | "type": "Identifier", 427 | "name": "foo" 428 | }, 429 | "arguments": [] 430 | }, 431 | "consequent": { 432 | "type": "ExpressionStatement", 433 | "expression": { 434 | "type": "CallExpression", 435 | "callee": { 436 | "type": "Identifier", 437 | "name": "bar" 438 | }, 439 | "arguments": [] 440 | } 441 | }, 442 | "alternate": { 443 | "type": "ExpressionStatement", 444 | "expression": { 445 | "type": "CallExpression", 446 | "callee": { 447 | "type": "Identifier", 448 | "name": "baz" 449 | }, 450 | "arguments": [] 451 | } 452 | } 453 | }, 454 | { 455 | "type": "IfStatement", 456 | "test": { 457 | "type": "LogicalExpression", 458 | "operator": "||", 459 | "left": { 460 | "type": "SequenceExpression", 461 | "expressions": [ 462 | { 463 | "type": "CallExpression", 464 | "callee": { 465 | "type": "Identifier", 466 | "name": "whatever" 467 | }, 468 | "arguments": [] 469 | }, 470 | { 471 | "type": "CallExpression", 472 | "callee": { 473 | "type": "Identifier", 474 | "name": "foo" 475 | }, 476 | "arguments": [] 477 | } 478 | ] 479 | }, 480 | "right": { 481 | "type": "CallExpression", 482 | "callee": { 483 | "type": "Identifier", 484 | "name": "bin" 485 | }, 486 | "arguments": [] 487 | } 488 | }, 489 | "consequent": { 490 | "type": "BlockStatement", 491 | "body": [ 492 | { 493 | "type": "ExpressionStatement", 494 | "expression": { 495 | "type": "CallExpression", 496 | "callee": { 497 | "type": "Identifier", 498 | "name": "bar" 499 | }, 500 | "arguments": [] 501 | } 502 | } 503 | ] 504 | }, 505 | "alternate": { 506 | "type": "BlockStatement", 507 | "body": [ 508 | { 509 | "type": "ExpressionStatement", 510 | "expression": { 511 | "type": "CallExpression", 512 | "callee": { 513 | "type": "Identifier", 514 | "name": "baz" 515 | }, 516 | "arguments": [] 517 | } 518 | } 519 | ] 520 | } 521 | } 522 | ], 523 | "sourceType": "script" 524 | }`); 525 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/test/fixture/instrument/json2.literals.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connor4312/js-fuzz/cb19be3062274dfc62d0f81e18c16b0b6f3f0006/packages/js-fuzz-core/test/fixture/instrument/json2.literals.json -------------------------------------------------------------------------------- /packages/js-fuzz-core/test/fixture/instrument/switch-statements.after.txt: -------------------------------------------------------------------------------- 1 | switch ((__js_fuzz_.coverage.hash[1196083929]++, foo()) || (__js_fuzz_.coverage.hash[4077107047]++, bar())) { 2 | case true: 3 | __js_fuzz_.coverage.hash[3185703906]++; 4 | bar(); 5 | case false: 6 | __js_fuzz_.coverage.hash[3251806186]++; 7 | baz(); 8 | break; 9 | default: 10 | __js_fuzz_.coverage.hash[326101261]++; 11 | bin(); 12 | } -------------------------------------------------------------------------------- /packages/js-fuzz-core/test/fixture/instrument/switch-statements.before.txt: -------------------------------------------------------------------------------- 1 | switch (foo() || bar()) { 2 | case true: 3 | bar(); 4 | case false: 5 | baz(); 6 | break; 7 | default: 8 | bin() 9 | } 10 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/test/fixture/instrument/switch-statements.literals.json: -------------------------------------------------------------------------------- 1 | [ 2 | "true", 3 | "false" 4 | ] -------------------------------------------------------------------------------- /packages/js-fuzz-core/test/fixture/instrument/ternary-statements.after.txt: -------------------------------------------------------------------------------- 1 | if (foo() ? (__js_fuzz_.coverage.hash[2006232292]++, true) : (__js_fuzz_.coverage.hash[976652108]++, false)) { 2 | __js_fuzz_.coverage.hash[3670931746]++; 3 | bar(); 4 | } 5 | const x = foo ? (__js_fuzz_.coverage.hash[1626774998]++, true) : (__js_fuzz_.coverage.hash[2596493524]++, false); 6 | (foo() ? (__js_fuzz_.coverage.hash[3283497825]++, bin) : (__js_fuzz_.coverage.hash[2486947067]++, baz))(); -------------------------------------------------------------------------------- /packages/js-fuzz-core/test/fixture/instrument/ternary-statements.before.txt: -------------------------------------------------------------------------------- 1 | if (foo() ? true : false) { 2 | bar(); 3 | } 4 | 5 | const x = foo ? true : false; 6 | 7 | (foo() ? bin : baz)(); 8 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/test/fixture/instrument/ternary-statements.literals.json: -------------------------------------------------------------------------------- 1 | [ 2 | "true", 3 | "false" 4 | ] -------------------------------------------------------------------------------- /packages/js-fuzz-core/test/util.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | 5 | exports.loadInstrumentationFixtures = () => { 6 | const base = `${__dirname}/fixture/instrument` 7 | const files = fs.readdirSync(base) 8 | const output = [] 9 | 10 | files.forEach(name => { 11 | const match = (/^(.+)\.before\.txt$/).exec(name) 12 | if (!match || !files.includes(`${match[1]}.after.txt`)) { 13 | return 14 | } 15 | 16 | const tcase = match[1] 17 | output.push({ 18 | name, 19 | before: fs.readFileSync(`${base}/${tcase}.before.txt`, 'utf8').trim(), 20 | after: fs.readFileSync(`${base}/${tcase}.after.txt`, 'utf8').trim() 21 | }) 22 | }) 23 | 24 | return output 25 | } 26 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../etc/tsconfig.base.json", 3 | "compilerOptions": { 4 | "experimentalDecorators": true, 5 | "emitDecoratorMetadata": true, 6 | "outDir": "dist" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/tshook.js: -------------------------------------------------------------------------------- 1 | require('reflect-metadata'); 2 | require('ts-node').register({ 3 | transpileOnly: true, 4 | compilerOptions: { 5 | module: 'commonjs', 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /packages/js-fuzz-core/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tslint", 3 | "extends": ["../../etc/config/tslint.base.json"] 4 | } 5 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # js-fuzz 2 | 3 | ![](./demo.gif) 4 | 5 | > This is still very much a work in progress and is probably not suitable for "real" use yet! 6 | 7 | js-fuzz is an [American Fuzzy Lop](http://lcamtuf.coredump.cx/afl/)-inspired fuzz tester for JavaScript code. It provides coverage-driven analysis and minimization while being fast and extraordinarily simple to use. 8 | 9 | ### Example 10 | 11 | This program tests that all valid JSON strings can be parsed with [JSON5](http://json5.org/): 12 | 13 | ```js 14 | const JSON5 = require('json5') 15 | 16 | exports.fuzz = input => { 17 | input = input.toString('utf8') // we give you buffers by default 18 | 19 | let isPlainJSON = true 20 | let isJSON5 = true 21 | try { JSON.parse(input) } catch () { isPlainJSON = false } 22 | try { JSON5.parse(input) } catch () { isJSON5 = false } 23 | 24 | // We catch and thrown errors and mark them as failures 25 | if (isPlainJSON && !isJSON5) { 26 | throw new Error('Found a string that was JSON but not JSON5'); 27 | } 28 | 29 | return isPlainJSON ? 1 : 0 30 | } 31 | ``` 32 | 33 | ### Usage 34 | 35 | ``` 36 | js-fuzz myFile.js 37 | ``` 38 | 39 | Usage is quite similar to [go-fuzz](https://github.com/dvyukov/go-fuzz#usage). You should give the `js-fuzz` tool the path to a module that exports a `fuzz` function. This function will be called with an input Buffer, and should return: 40 | 41 | - `-1` if that input should not be fed back into the fuzzer, even if it gives us better coverage 42 | - `1` if the fuzzer should increase the priority of the given input 43 | - `0` to let the fuzzer decide 44 | 45 | In the above example, we asked to increase the priority of strings that can be parsed as plain JSON, since we want more of that sort of thing in order to test against JSON5. You can also return Promises from the `fuzz` function, or take a callback. 46 | 47 | ```js 48 | exports.fuzz = input => { 49 | return doSomethingAsync(input) 50 | .then(out => anotherThing()) 51 | .then(valid => valid ? 1 : 0) 52 | } 53 | 54 | // or 55 | 56 | exports.fuzz = (input, callback) => { 57 | myNodeStyleThing(input, err => { 58 | if (err) { 59 | callback(err) 60 | } else { 61 | callback(null, 1) 62 | } 63 | }) 64 | } 65 | ``` 66 | 67 | The fuzzer will run until you terminate it, reporting stats in your terminal, and will write output in the form of text files into a `fuzz-output` directory in your current working directory. You'll probably be most interested in `./fuzz-output/crashers`, which will contain all inputs that caused an error in your program! 68 | 69 | js-fuzz does assume that your program's behaviour is deterministic and that the `fuzz` function is [pure](https://en.wikipedia.org/wiki/Pure_function). It will work when this does not hold true, but it will be less efficient at discovering code paths and edge cases. 70 | 71 | ### Internals 72 | 73 | The runner spawns child processes which execute the given input module, by default spawning one child per CPU core. We use Esprima and Escodegen to provide branch coverage on JavaScript code using a `require` hook. The coverage that's injected has little performance impact; running a benchmark on Douglas Crockford's JSON implementation, it's about a 0.6% overhead. Most of the mechanics are based heavily on the [AFL whitepaper](http://lcamtuf.coredump.cx/afl/technical_details.txt). 74 | 75 | #### Limitations 76 | 77 | Most limitations revolve around what Node will let us get at without leaning on native extensions, which I would prefer to avoid for maintainability and compatibility reasons. 78 | 79 | - This doesn't currently work in browsers, though I want it to do so in the future. 80 | - We don't have any coverage analysis for native extensions and I am not planning on adding it (though I am open to PRs). 81 | - This implementation is bounded, at least in simple programs by memory throughput; much copying and moving has to be done as slave processes serialize and unserialize data with communicating with the master. Outside of writing native bindings I am unsure how this problem could be addressed. 82 | - We aren't always able to collect coverage statistics for tasks which time out. While we still record the input, we aren't able to dedupe test cases. 83 | 84 | #### Acknowledgments 85 | 86 | - The [AFL](http://lcamtuf.coredump.cx/afl/) project is the original fuzzer and is where most of the beautiful mechanics originated from. 87 | - Many mechanics in this implementation are derived from [go-fuzz](https://github.com/dvyukov/go-fuzz), which was also my original introduction to fuzzing. Dmitry talks through some of the internals [here](https://www.youtube.com/watch?v=Ef7TtSZlmlk). 88 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "noImplicitAny": true, 5 | "declaration": false, 6 | "module": "commonjs", 7 | "target": "es5", 8 | "outDir": "lib", 9 | "allowJs": true, 10 | "lib": [ 11 | "es5", 12 | "es6" 13 | ], 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ] 17 | }, 18 | "exclude": [ 19 | "node_modules", 20 | "lib" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-microsoft-contrib", 3 | "rulesDirectory": [ 4 | "node_modules/tslint-microsoft-contrib" 5 | ], 6 | "rules": { 7 | "align": false, 8 | "chai-vague-errors": false, 9 | "export-name": false, 10 | "function-name": [true, { 11 | "method-regex": "^[a-z][\\w\\d]+$", 12 | "private-method-regex": "^[a-z][\\w\\d]+$", 13 | "static-method-regex": "^[A-Z][\\w\\d]+$", 14 | "function-regex": "^[a-z][\\w\\d]+$" 15 | }], 16 | "missing-jsdoc": false, 17 | "mocha-no-side-effect-code": false, 18 | "no-any": false, 19 | "no-multiline-string": false, 20 | "no-require-imports": false, 21 | "no-relative-imports": false, 22 | "trailing-comma": [true, {"multiline": "always", "singleline": "never"}], 23 | "no-bitwise": false, 24 | "insecure-random": false, 25 | "no-var-requires": false 26 | } 27 | } 28 | --------------------------------------------------------------------------------