diff --git a/docs/fuzz-settings.md b/docs/fuzz-settings.md index 6ee70f85..96ec6534 100644 --- a/docs/fuzz-settings.md +++ b/docs/fuzz-settings.md @@ -2,6 +2,28 @@ This page describes advanced fuzzing settings. +## Configuration options + +Jazzer.js can be configured in multiple ways depending on the concrete use case. + +The `Options` interface in the [options.ts](../packages/core/options.ts) file +describes all available settings. These can be set via CLI argument, environment +variable or in integration specific ways, e.g. Jest configuration files. + +In general the following preference applies with increasing priority: + +- Default values from the [`defaultOptions`](../packages/core/options.ts) object + (names in camel case format, e.g. `fuzzTarget`) +- Environment variables (names in upper snake case format with `JAZZER_` prefix, + e.g. `JAZZER_FUZZ_TARGET=Foo`) +- CLI arguments (names in lower snake case format, e.g. `--fuzz_target=Foo`) +- Integration specific configuration (e.g. `jazzerjsrc` or Jest configuration + files) + +**Note**: The CLI provides abbreviations for common arguments, e.g. `--includes` +can be abbreviated to `-i`. Only the main argument name is supported in other +configuration approaches, though. + ## Corpus Jazzer.js generates meaningful inputs to a fuzz target based on coverage and diff --git a/docs/fuzz-targets.md b/docs/fuzz-targets.md index 87710ecd..817d1329 100644 --- a/docs/fuzz-targets.md +++ b/docs/fuzz-targets.md @@ -168,39 +168,40 @@ jazzer [corpus...] [-- ] Detailed documentation and some example calls are available using the `--help` flag, so that only the most important parameters are discussed here. -| Parameter | Description | -| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `` | Import path to the fuzz target module. | -| `[corpus...]` | Paths to the corpus directories. If not given, no initial seeds are used nor interesting inputs saved. | -| `-f`, `--fuzz_function` | Name of the fuzz test entry point. It must be an exported function with a single [Buffer](https://nodejs.org/api/buffer.html) parameter. Default is `fuzz`. | -| `-i`, `--instrumentation_includes` / `-e`, `--instrumentation_excludes` | Part of filepath names to include/exclude in the instrumentation. A tailing `/` should be used to include directories and prevent confusion with filenames. `*` can be used to include all files. Can be specified multiple times. Default will include everything outside the `node_modules` directory. If either of these flags are set the default value for the other is ignored. | -| `--sync` | Enables synchronous fuzzing. **May only be used for entirely synchronous code**. | -| `-h`, `--custom_hooks` | Filenames with custom hooks. Several hooks per file are possible. See further details in [docs/fuzz-settings.md](fuzz-settings.md). | -| `--help` | Detailed help message containing all flags. | -| `-- ` | Parameters after `--` are forwarded to the internal fuzzing engine (`libFuzzer`). Available settings can be found in its [options documentation](https://www.llvm.org/docs/LibFuzzer.html#options). | +| Parameter | Description | +| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `` | Import path to the fuzz target module. | +| `[corpus...]` | Paths to the corpus directories. If not given, no initial seeds are used nor interesting inputs saved. | +| `-f`, `--fuzz_function` | Name of the fuzz test entry point. It must be an exported function with a single [Buffer](https://nodejs.org/api/buffer.html) parameter. Default is `fuzz`. | +| `-i`, `--includes` / `-e`, `--excludes` | Part of filepath names to include/exclude in the instrumentation. A tailing `/` should be used to include directories and prevent confusion with filenames. `*` can be used to include all files. Can be specified multiple times. Default will include everything outside the `node_modules` directory. If either of these flags are set the default value for the other is ignored. | +| `--sync` | Enables synchronous fuzzing. **May only be used for entirely synchronous code**. | +| `-h`, `--custom_hooks` | Filenames with custom hooks. Several hooks per file are possible. See further details in [docs/fuzz-settings.md](fuzz-settings.md). | +| `--help` | Detailed help message containing all flags. | +| `-- ` | Parameters after `--` are forwarded to the internal fuzzing engine (`libFuzzer`). Available settings can be found in its [options documentation](https://www.llvm.org/docs/LibFuzzer.html#options). | ## Coverage report generation -To generate a coverage report, add the `--cov`/`--coverage` flag to the -Jazzer.js CLI. In the following example, the `--cov` flag is combined with the -dry run flag `-d` that disables internal instrumentation used by the fuzzer. +To generate a coverage report, add the `--coverage` flag to the Jazzer.js CLI. +In the following example, the `--coverage` flag is combined with the mode flag +`-m=regression` that only uses existing corpus entries without performing any +fuzzing. ```shell -npx jazzer -d --corpus --cov -- +npx jazzer -m=regression --corpus --cov -- ``` Alternatively, you can add a new script to your `package.json`: ```json "scripts": { - "coverage": "jazzer -d -i target -i another_target --corpus --cov -- " + "coverage": "jazzer -m regression -i target -i another_target --corpus --cov -- " } ``` -Files matched by the flags `--instrumentation_includes` or `--custom_hooks`, and -not matched by the flag `--instrumentation_excludes` will be included in the -coverage report. It is recommended to disable coverage report generation during -fuzzing, because of the substantial overhead that it adds. +Files matched by the flags `--includes` or `--custom_hooks`, and not matched by +the flag `--excludes` will be included in the coverage report. It is recommended +to disable coverage report generation during fuzzing, because of the substantial +overhead that it adds. ### Coverage report directory diff --git a/examples/protobufjs/package.json b/examples/protobufjs/package.json index 26859ac1..f610c61d 100644 --- a/examples/protobufjs/package.json +++ b/examples/protobufjs/package.json @@ -4,7 +4,7 @@ "type": "module", "scripts": { "fuzz": "npx jazzer fuzz --sync -i protobuf", - "dryRun": "npx jazzer fuzz -d --sync -i protobuf -- -runs=100 -seed=123456789" + "dryRun": "npx jazzer fuzz -m regression --sync -i protobuf -- -runs=100 -seed=123456789" }, "dependencies": { "protobufjs": "^7.0.0", diff --git a/package-lock.json b/package-lock.json index 24354382..588832a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "jest": "^29.6.2", "lint-staged": "^13.2.3", "prettier": "3.0.1", + "rimraf": "^5.0.1", "run-script-os": "^1.1.6", "ts-jest": "^29.1.1", "typescript": "^5.1.6" @@ -783,6 +784,79 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1300,6 +1374,16 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -3155,9 +3239,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.487", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.487.tgz", - "integrity": "sha512-XbCRs/34l31np/p33m+5tdBrdXu9jJkZxSbNxj5I0H1KtV2ZMSB+i/HYqDiRzHaFx2T5EdytjoBRe8QRJE2vQg==" + "version": "1.4.488", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.488.tgz", + "integrity": "sha512-Dv4sTjiW7t/UWGL+H8ZkgIjtUAVZDgb/PwGWvMsCT7jipzUV/u5skbLXPFKb6iV0tiddVi/bcS2/kUrczeWgIQ==" }, "node_modules/emittery": { "version": "0.13.1", @@ -3743,6 +3827,21 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/flatted": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", @@ -3768,6 +3867,34 @@ } } }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -4493,6 +4620,24 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.2.2.tgz", + "integrity": "sha512-mgNtVv4vUuaKA97yxUHoA3+FkuhtxkjdXEWOyB/N76fjy0FjezEt34oy3epBtvCvS+7DyKwqCFWx/oJLV5+kCg==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest": { "version": "29.6.2", "resolved": "https://registry.npmjs.org/jest/-/jest-29.6.2.tgz", @@ -6847,6 +6992,40 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.0.tgz", + "integrity": "sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.2.tgz", + "integrity": "sha512-eL79dXrE1q9dBbDCLg7xfn/vl7MS4F1gvJAgjJrQli/jbQWdUttuVawphqpffoIYfRdq78LHx6GP4bU/EQ2ATA==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -7786,19 +7965,78 @@ "dev": true }, "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.1.tgz", + "integrity": "sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg==", + "dev": true, "dependencies": { - "glob": "^7.1.3" + "glob": "^10.2.5" }, "bin": { - "rimraf": "bin.js" + "rimraf": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "10.3.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.3.tgz", + "integrity": "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.0.3", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/minipass": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.2.tgz", + "integrity": "sha512-eL79dXrE1q9dBbDCLg7xfn/vl7MS4F1gvJAgjJrQli/jbQWdUttuVawphqpffoIYfRdq78LHx6GP4bU/EQ2ATA==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/rsvp": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.6.2.tgz", @@ -8164,6 +8402,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/string-width/node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", @@ -8202,6 +8470,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -8387,6 +8668,20 @@ "node": ">=8.17.0" } }, + "node_modules/tmp/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -8925,6 +9220,53 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -9199,9 +9541,7 @@ "istanbul-reports": "^3.1.6" }, "devDependencies": { - "@types/istanbul-reports": "^3.0.1", "@types/tmp": "^0.2.3", - "jest": "^29.6.2", "tmp": "^0.2.1" }, "engines": { @@ -9767,6 +10107,54 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -9926,11 +10314,9 @@ "version": "file:packages/jest-runner", "requires": { "@jazzer.js/core": "1.6.1", - "@types/istanbul-reports": "^3.0.1", "@types/tmp": "^0.2.3", "cosmiconfig": "^8.2.0", "istanbul-reports": "^3.1.6", - "jest": "^29.6.2", "tmp": "^0.2.1" } }, @@ -10242,6 +10628,13 @@ "fastq": "^1.6.0" } }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, "@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -11588,9 +11981,9 @@ } }, "electron-to-chromium": { - "version": "1.4.487", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.487.tgz", - "integrity": "sha512-XbCRs/34l31np/p33m+5tdBrdXu9jJkZxSbNxj5I0H1KtV2ZMSB+i/HYqDiRzHaFx2T5EdytjoBRe8QRJE2vQg==" + "version": "1.4.488", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.488.tgz", + "integrity": "sha512-Dv4sTjiW7t/UWGL+H8ZkgIjtUAVZDgb/PwGWvMsCT7jipzUV/u5skbLXPFKb6iV0tiddVi/bcS2/kUrczeWgIQ==" }, "emittery": { "version": "0.13.1", @@ -12030,6 +12423,17 @@ "requires": { "flatted": "^3.1.0", "rimraf": "^3.0.2" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "flatted": { @@ -12043,6 +12447,24 @@ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" }, + "foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + } + } + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -12583,6 +13005,16 @@ "istanbul-lib-report": "^3.0.0" } }, + "jackspeak": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.2.2.tgz", + "integrity": "sha512-mgNtVv4vUuaKA97yxUHoA3+FkuhtxkjdXEWOyB/N76fjy0FjezEt34oy3epBtvCvS+7DyKwqCFWx/oJLV5+kCg==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, "jest": { "version": "29.6.2", "resolved": "https://registry.npmjs.org/jest/-/jest-29.6.2.tgz", @@ -14447,6 +14879,30 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "requires": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.0.tgz", + "integrity": "sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw==", + "dev": true + }, + "minipass": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.2.tgz", + "integrity": "sha512-eL79dXrE1q9dBbDCLg7xfn/vl7MS4F1gvJAgjJrQli/jbQWdUttuVawphqpffoIYfRdq78LHx6GP4bU/EQ2ATA==", + "dev": true + } + } + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -15217,11 +15673,51 @@ "dev": true }, "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.1.tgz", + "integrity": "sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg==", + "dev": true, "requires": { - "glob": "^7.1.3" + "glob": "^10.2.5" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "10.3.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.3.tgz", + "integrity": "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.0.3", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + } + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minipass": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.2.tgz", + "integrity": "sha512-eL79dXrE1q9dBbDCLg7xfn/vl7MS4F1gvJAgjJrQli/jbQWdUttuVawphqpffoIYfRdq78LHx6GP4bU/EQ2ATA==", + "dev": true + } } }, "rsvp": { @@ -15474,6 +15970,31 @@ } } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + } + } + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -15482,6 +16003,15 @@ "ansi-regex": "^5.0.1" } }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, "strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -15634,6 +16164,16 @@ "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", "requires": { "rimraf": "^3.0.0" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + } } }, "tmpl": { @@ -16057,6 +16597,42 @@ } } }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + } + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 395ef59b..5cfd643a 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "scripts": { "prepare": "husky install", "build": "tsc -b tsconfig.build.json", + "clean": "rimraf -g **/node_modules **/tests/**/package-lock.json **/examples/**/package-lock.json **/dist **/coverage packages/fuzzer/build packages/fuzzer/prebuilds", "compile:watch": "tsc -b tsconfig.build.json --incremental --pretty --watch", "test": "run-script-os", "test:jest": "jest && npm run test --ws --if-present", @@ -50,6 +51,7 @@ "jest": "^29.6.2", "lint-staged": "^13.2.3", "prettier": "3.0.1", + "rimraf": "^5.0.1", "run-script-os": "^1.1.6", "ts-jest": "^29.1.1", "typescript": "^5.1.6" diff --git a/packages/core/cli.ts b/packages/core/cli.ts index bf348cfb..38f47bd3 100644 --- a/packages/core/cli.ts +++ b/packages/core/cli.ts @@ -17,7 +17,14 @@ import yargs, { Argv } from "yargs"; import { startFuzzing } from "./core"; -import { ensureFilepath } from "./utils"; +import { prepareArgs } from "./utils"; +import { defaultOptions, processOptions, fromSnakeCase } from "./options"; + +// Use yargs to parse command line arguments and provide a nice CLI experience. +// Default values are provided by the options module and must not be set by yargs. +// To still display the default values in the help message, they are only set as +// descriptions. +// Handling of unsupported parameters is also done via the options module. yargs(process.argv.slice(2)) .scriptName("jazzer") @@ -39,8 +46,9 @@ yargs(process.argv.slice(2)) 'Also pass the "-max_total_time" flag to the internal fuzzing engine ' + "(libFuzzer) to stop the fuzzing run after 60 seconds.", ) + .epilogue("Happy fuzzing!") .command( - "$0 [corpus..]", + "$0 [corpus..]", "Coverage-guided, in-process fuzzer for the Node.js platform. \n\n" + 'The "target" module has to export a function "fuzz" which accepts ' + "a byte array as first parameter and uses that to invoke the actual " + @@ -53,183 +61,176 @@ yargs(process.argv.slice(2)) "An example is shown in the examples section of this help message.", (yargs: Argv) => { yargs - .positional("target", { + .positional("fuzz_target", { + demandOption: true, describe: "Name of the module that exports the fuzz target function.", type: "string", }) - .demandOption("target") - .array("corpus") .positional("corpus", { + array: true, describe: "Paths to the corpus directories. If not given, no initial " + "seeds are used nor interesting inputs saved.", type: "string", }) - .option("fuzz_function", { + .option("fuzz_entry_point", { + alias: ["f", "fuzz_function"], + defaultDescription: defaultOptions.fuzzEntryPoint, describe: "Name of the fuzz test entry point. It must be an exported " + "function with a single Buffer parameter", - alias: "f", - type: "string", - default: "fuzz", group: "Fuzzer:", - }) - - .option("id_sync_file", { - describe: - "File used to sync edge ID generation. " + - "Needed when fuzzing in multi-process modes", type: "string", - default: undefined, - group: "Fuzzer:", }) - .hide("id_sync_file") - - .option("sync", { - describe: "Run the fuzz target synchronously.", - type: "boolean", - default: false, - group: "Fuzzer:", - }) - .array("instrumentation_includes") - .option("instrumentation_includes", { + .option("includes", { + alias: ["i", "instrumentation_includes"], + array: true, + defaultDescription: `${JSON.stringify(defaultOptions.includes)}`, describe: "Part of filepath names to include in the instrumentation. " + 'A tailing "/" should be used to include directories and prevent ' + 'confusion with filenames. "*" can be used to include all files.\n' + - "Can be specified multiple times. By default all files will be " + - "included.", - type: "string", - alias: "i", + "Can be specified multiple times.", group: "Fuzzer:", + type: "string", }) - - .array("instrumentation_excludes") - .option("instrumentation_excludes", { + .option("excludes", { + alias: ["e", "instrumentation_excludes"], + array: true, + defaultDescription: `${JSON.stringify(defaultOptions.excludes)}`, describe: "Part of filepath names to exclude in the instrumentation. " + 'A tailing "/" should be used to exclude directories and prevent ' + 'confusion with filenames. "*" can be used to exclude all files.\n' + - 'Can be specified multiple times. By default, "node_modules/" will ' + - "be excluded.", - type: "string", - alias: "e", + "Can be specified multiple times.", group: "Fuzzer:", + type: "string", }) - .option("dry_run", { + + .option("id_sync_file", { + defaultDescription: `${JSON.stringify(defaultOptions.idSyncFile)}`, describe: - "Perform a dry run with the fuzzing instrumentation disabled. " + - "A dry run only executes the fuzz test with the inputs from the " + - "corpus and returns directly. That is, no fuzzing is performed. " + - "This option can then be used when reporting code coverage for " + - "a fuzz test", - type: "boolean", - alias: "d", + "File used for sync edge ID generation. " + + "Needed when fuzzing in multi-process modes", group: "Fuzzer:", - default: false, + hidden: true, + type: "string", }) - .array("custom_hooks") .option("custom_hooks", { + alias: "h", + array: true, + defaultDescription: `${JSON.stringify(defaultOptions.customHooks)}`, describe: "Allow users to hook functions. This can be used for writing " + "bug detectors, for stubbing, and for writing feedback functions " + "for the fuzzer.", - type: "string", - alias: "h", group: "Fuzzer:", - default: [], + type: "string", }) - .array("expected_errors") .option("expected_errors", { + alias: "x", + array: true, + defaultDescription: `${JSON.stringify( + defaultOptions.expectedErrors, + )}`, describe: "Expected errors can be specified as the class name of the " + "thrown error object or value of a thrown string. If expected " + "errors are defined, but none, or none of the expected ones are " + "raised during execution, the test execution fails." + 'Examples: -x Error -x "My thrown error string"', - type: "string", - alias: "x", group: "Fuzzer:", - default: [], + hidden: true, + type: "string", }) - .hide("expected_errors") - .boolean("verbose") - .option("verbose", { - describe: "Enable verbose debugging logs.", - type: "boolean", - alias: "v", + .option("disable_bug_detectors", { + array: true, + defaultDescription: `${JSON.stringify( + defaultOptions.disableBugDetectors, + )}`, + describe: + "A list of patterns to disable internal bug detectors. By default all internal " + + "bug detectors are enabled. To disable all, use the '.*' pattern." + + "Following bug detectors are available: " + + " command-injection\n" + + " path-traversal\n" + + " prototype-pollution\n", group: "Fuzzer:", - default: false, + type: "string", }) - .boolean("cov") - .option("cov", { - describe: "Enable code coverage.", - alias: "coverage", - type: "boolean", + + .option("mode", { + alias: "m", + defaultDescription: `${JSON.stringify(defaultOptions.mode)}`, + describe: + "Configure if fuzzing should be performed, 'fuzzing' mode, " + + "or if the fuzz target should only be invoked using existing corpus " + + "entries, 'regression' mode." + + "Regression mode is helpful if only coverage reports should be generated.", group: "Fuzzer:", - default: false, - }) - .option("cov_dir", { - describe: "Directory for storing coverage reports.", - alias: "coverage_directory", type: "string", - default: "coverage", - group: "Fuzzer:", }) - .array("cov_reporters") - .option("cov_reporters", { - describe: "A list of reporter names for writing coverage reports.", - alias: "coverage_reporters", - type: "string", + .option("dry_run", { + alias: "d", + defaultDescription: `${JSON.stringify(defaultOptions.dryRun)}`, + describe: "Perform a run with the fuzzing instrumentation disabled.", group: "Fuzzer:", - default: ["json", "text", "lcov", "clover"], + type: "boolean", }) .option("timeout", { + defaultDescription: `${JSON.stringify(defaultOptions.timeout)}`, describe: "Timeout in milliseconds for each fuzz test execution.", + group: "Fuzzer:", type: "number", + }) + .option("sync", { + defaultDescription: `${JSON.stringify(defaultOptions.sync)}`, + describe: "Run the fuzz target synchronously.", group: "Fuzzer:", - default: 5000, + type: "boolean", }) - .array("disable_bug_detectors") - .option("disable_bug_detectors", { - describe: - "A list of patterns to disable internal bug detectors. By default all internal " + - "bug detectors are enabled. To disable all, use the '.*' pattern." + - "Following bug detectors are available: " + - " command-injection\n" + - " path-traversal\n", - type: "string", + .option("verbose", { + alias: "v", + defaultDescription: `${JSON.stringify(defaultOptions.verbose)}`, + describe: "Enable verbose debugging logs.", group: "Fuzzer:", - default: [], + type: "boolean", + }) + + .option("coverage", { + alias: "cov", + defaultDescription: `${JSON.stringify(defaultOptions.coverage)}`, + describe: "Enable code coverage.", + group: "Coverage:", + type: "boolean", + }) + .option("coverage_directory", { + alias: "cov_dir", + defaultDescription: `${JSON.stringify( + defaultOptions.coverageDirectory, + )}`, + describe: "Directory for storing coverage reports.", + group: "Coverage:", + type: "string", + }) + .option("coverage_reporters", { + alias: "cov_reporters", + array: true, + defaultDescription: `${JSON.stringify( + defaultOptions.coverageReporters, + )}`, + describe: "A list of reporter names for writing coverage reports.", + group: "Coverage:", + type: "string", }); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (args: any) => { - // Set verbose mode environment variable. If the environment variable is - // set, the verbose mode flag is ignored. - if (args.verbose) { - process.env.JAZZER_DEBUG = "1"; - } + const options = prepareArgs(args); // noinspection JSIgnoredPromiseFromCall - startFuzzing({ - fuzzTarget: ensureFilepath(args.target), - fuzzEntryPoint: args.fuzz_function, - includes: args.instrumentation_includes, - excludes: args.instrumentation_excludes, - dryRun: args.dry_run, - sync: args.sync, - timeout: args.timeout, - fuzzerOptions: args.corpus.concat(args._), - customHooks: args.custom_hooks, - expectedErrors: args.expected_errors, - idSyncFile: args.id_sync_file, - coverage: args.cov, - coverageDirectory: args.cov_dir, - coverageReporters: args.cov_reporters, - disableBugDetectors: args.disable_bug_detectors, - }); + startFuzzing(processOptions(options, fromSnakeCase)); }, ) .help().argv; diff --git a/packages/core/core.ts b/packages/core/core.ts index 3be231df..5080349a 100644 --- a/packages/core/core.ts +++ b/packages/core/core.ts @@ -37,7 +37,7 @@ import { } from "@jazzer.js/instrumentor"; import { callbacks } from "./callback"; import { ensureFilepath, importModule } from "./utils"; -import { buildFuzzerOption } from "./options"; +import { buildFuzzerOption, Options } from "./options"; // Remove temporary files on exit tmp.setGracefulCleanup(); @@ -49,28 +49,6 @@ const ERROR_UNEXPECTED_CODE = 78; const SIGSEGV = 11; -export interface Options { - // `fuzzTarget` is the name of an external module containing a `fuzzer.FuzzTarget` - // that is resolved by `fuzzEntryPoint`. - fuzzTarget: string; - fuzzEntryPoint: string; - includes: string[]; - excludes: string[]; - dryRun: boolean; - sync: boolean; - fuzzerOptions: string[]; - customHooks: string[]; - expectedErrors: string[]; - timeout: number; - idSyncFile?: string; - coverage: boolean; // Enables source code coverage report generation. - coverageDirectory: string; - coverageReporters: reports.ReportType[]; - disableBugDetectors: string[]; - mode?: "fuzzing" | "regression"; - verbose?: boolean; -} - /* eslint no-var: 0 */ declare global { var Fuzzer: fuzzer.Fuzzer; @@ -89,7 +67,7 @@ export async function initFuzzing(options: Options): Promise { options.customHooks, options.coverage, options.dryRun, - options.idSyncFile !== undefined + options.idSyncFile ? new FileSyncIdStrategy(options.idSyncFile) : new MemorySyncIdStrategy(), ), @@ -237,7 +215,7 @@ function stopFuzzing( err: unknown, expectedErrors: string[], coverageDirectory: string, - coverageReporters: reports.ReportType[], + coverageReporters: string[], sync: boolean, forceShutdownWithCode?: number, ) { @@ -255,7 +233,7 @@ function stopFuzzing( coverageMap: coverageMap, }); coverageReporters.forEach((reporter) => - reports.create(reporter).execute(context), + reports.create(reporter as keyof reports.ReportOptions).execute(context), ); } @@ -349,9 +327,9 @@ export function wrapFuzzFunctionForBugDetection( } if (originalFuzzFn.length === 1) { - return (data: Buffer): void | Promise => { + return (data: Buffer): unknown | Promise => { let fuzzTargetError: unknown; - let result: void | Promise = undefined; + let result: unknown | Promise = undefined; try { callbacks.runBeforeEachCallbacks(); result = (originalFuzzFn as fuzzer.FuzzTargetAsyncOrValue)(data); @@ -382,8 +360,8 @@ export function wrapFuzzFunctionForBugDetection( return ( data: Buffer, done: (err?: Error) => void, - ): void | Promise => { - let result: void | Promise = undefined; + ): unknown | Promise => { + let result: unknown | Promise = undefined; try { callbacks.runBeforeEachCallbacks(); // Return result of fuzz target to enable sanity checks in C++ part. @@ -407,3 +385,4 @@ export function wrapFuzzFunctionForBugDetection( // Export public API from within core module for easy access. export * from "./api"; export { FuzzedDataProvider } from "./FuzzedDataProvider"; +export { Options, processOptions, defaultOptions } from "./options"; diff --git a/packages/core/options.test.ts b/packages/core/options.test.ts new file mode 100644 index 00000000..e87d594d --- /dev/null +++ b/packages/core/options.test.ts @@ -0,0 +1,161 @@ +/* + * Copyright 2023 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { + defaultOptions, + fromSnakeCase, + fromSnakeCaseWithPrefix, + Options, + processOptions, +} from "./options"; + +describe("options", () => { + describe("processOptions", () => { + it("use default options if none given", () => { + expect(processOptions({})).toEqual(defaultOptions); + expect(processOptions(undefined as any)).toEqual(defaultOptions); + expect(processOptions(null as any)).toEqual(defaultOptions); + expect(processOptions("" as any)).toEqual(defaultOptions); + expect(processOptions(false as any)).toEqual(defaultOptions); + }); + it("prefer environment variables to defaults", () => { + withEnv("JAZZER_FUZZ_TARGET", "FOO", () => { + withEnv("JAZZER_INCLUDES", '["BAR", "BAZ"]', () => { + const options = processOptions({}); + expect(options).toHaveProperty("fuzzTarget", "FOO"); + expect(options).toHaveProperty("includes", ["BAR", "BAZ"]); + expectDefaultsExceptKeys(options, "fuzzTarget", "includes"); + }); + }); + }); + it("prefer given values to defaults and environment variables", () => { + withEnv("JAZZER_FUZZ_TARGET", "bar", () => { + const options = processOptions({ fuzzTarget: "foo" }); + expect(options).toHaveProperty("fuzzTarget", "foo"); + expectDefaultsExceptKeys(options, "fuzzTarget"); + }); + }); + it("includes and excludes are set together", () => { + expect(processOptions({ includes: ["foo"] })).toHaveProperty( + "excludes", + [], + ); + expect(processOptions({ excludes: ["foo"] })).toHaveProperty( + "includes", + [], + ); + }); + it("error on unknown option", () => { + const inputs = { unknownOption: "foo" }; + expect(() => processOptions(inputs as any)).toThrow("'unknownOption'"); + }); + it("error on mismatching type", () => { + expect(() => processOptions({ fuzzTarget: false } as any)).toThrow( + "expected type 'string'", + ); + }); + it("does not use parts of input", () => { + const input = { includes: ["foo"] }; + const options = processOptions(input); + input.includes.push("bar"); + expect(options.includes).not.toContain("bar"); + }); + it("lookup keys with transformer function", () => { + const options = processOptions( + { fuzz_target: "foo" } as any, + fromSnakeCase, + ); + expect(options).toHaveProperty("fuzzTarget", "foo"); + }); + it("set debug env variable", () => { + withEnv("JAZZER_DEBUG", "", () => { + processOptions({ verbose: true }); + expect(process.env.JAZZER_DEBUG).toEqual("1"); + }); + withEnv("JAZZER_DEBUG", "", () => { + withEnv("DEBUG", "1", () => { + processOptions({ verbose: true }); + expect(process.env.JAZZER_DEBUG).toEqual("1"); + }); + }); + }); + it("does not merge __proto__", () => { + expect(() => { + processOptions(JSON.parse('{"__proto__": {"polluted": 42}}') as any); + }).toThrow(); + }); + }); +}); + +describe("KeyFormatSource", () => { + describe("fromSnakeCase", () => { + it("converts to camelCase", () => { + expect(fromSnakeCase("snake_case")).toEqual("snakeCase"); + expect(fromSnakeCase("Snake_Case")).toEqual("snakeCase"); + expect(fromSnakeCase("SNAKE_CASE")).toEqual("snakeCase"); + expect(fromSnakeCase("SNAKE_CASE_123")).toEqual("snakeCase123"); + expect(fromSnakeCase("SNAKE_CASE_123_")).toEqual("snakeCase123_"); + expect(fromSnakeCase("word")).toEqual("word"); + expect(fromSnakeCase("kebab-case")).toEqual("kebab-case"); + }); + }); + describe("fromSnakeCaseWithPrefix", () => { + it("converts to camelCase", () => { + expect(fromSnakeCaseWithPrefix("PREFIX")("PREFIX_snake_case")).toEqual( + "snakeCase", + ); + expect(fromSnakeCaseWithPrefix("PREFIX")("PREFIX_Snake_Case")).toEqual( + "snakeCase", + ); + expect(fromSnakeCaseWithPrefix("PREFIX")("PREFIX_SNAKE_CASE")).toEqual( + "snakeCase", + ); + expect( + fromSnakeCaseWithPrefix("PREFIX")("PREFIX_SNAKE_CASE_123"), + ).toEqual("snakeCase123"); + expect( + fromSnakeCaseWithPrefix("PREFIX")("PREFIX_SNAKE_CASE_123_"), + ).toEqual("snakeCase123_"); + expect(fromSnakeCaseWithPrefix("PREFIX")("PREFIX_word")).toEqual("word"); + expect(fromSnakeCaseWithPrefix("PREFIX")("PREFIX_kebab-case")).toEqual( + "kebab-case", + ); + }); + }); +}); + +function expectDefaultsExceptKeys(options: Options, ...ignore: string[]) { + Object.keys(defaultOptions).forEach((key: string) => { + if (ignore.includes(key)) return; + expect(options).toHaveProperty(key, defaultOptions[key as keyof Options]); + }); +} + +function withEnv(property: string, value: string, fn: () => void) { + const current = process.env[property]; + try { + process.env[property] = value; + fn(); + } finally { + if (current) { + process.env[property] = current; + } else { + delete process.env[property]; + } + } +} diff --git a/packages/core/options.ts b/packages/core/options.ts index 4f7afdfe..f969350c 100644 --- a/packages/core/options.ts +++ b/packages/core/options.ts @@ -16,9 +16,170 @@ import * as tmp from "tmp"; import fs from "fs"; -import { Options } from "./core"; import { useDictionaryByParams } from "./dictionary"; +/** + * Jazzer.js options structure expected by the fuzzer. + * + * Entry functions, like the CLI or test framework integrations, need to build + * this structure and should use the same property names for exposing their own + * options. + */ +export interface Options { + // `fuzzTarget` is the name of a module exporting the fuzz function `fuzzEntryPoint`. + fuzzTarget: string; + // Name of the function that is called by the fuzzer exported by `fuzzTarget`. + fuzzEntryPoint: string; + // Part of filepath names to include in the instrumentation. + includes: string[]; + // Part of filepath names to exclude in the instrumentation. + excludes: string[]; + // Whether to add fuzzing instrumentation or not. + dryRun: boolean; + // Whether to run the fuzzer in sync mode or not. + sync: boolean; + // Options to pass on to the underlying fuzzing engine. + fuzzerOptions: string[]; + // Files to load that contain custom hooks. + customHooks: string[]; + // Expected error name that won't trigger the fuzzer to stop with an error exit code. + expectedErrors: string[]; + // Timeout for one fuzzing iteration in milliseconds. + timeout: number; + // Internal: File to sync coverage IDs in fork mode. + idSyncFile?: string; + // Enable source code coverage report generation. + coverage: boolean; + // Directory to write coverage reports to. + coverageDirectory: string; + // Coverage reporters to use during report generation. + coverageReporters: string[]; + // Disable bug detectors by name. + disableBugDetectors: string[]; + // Fuzzing mode. + mode: "fuzzing" | "regression"; + // Verbose logging. + verbose?: boolean; +} + +export const defaultOptions: Options = { + fuzzTarget: "", + fuzzEntryPoint: "fuzz", + includes: ["*"], + excludes: ["node_modules"], + dryRun: false, + sync: false, + fuzzerOptions: [], + customHooks: [], + expectedErrors: [], + timeout: 5000, // default Jest timeout + idSyncFile: "", + coverage: false, + coverageDirectory: "coverage", + coverageReporters: ["json", "text", "lcov", "clover"], // default Jest reporters + disableBugDetectors: [], + mode: "fuzzing", + verbose: false, +}; + +export type KeyFormatSource = (key: string) => string; +export const fromCamelCase: KeyFormatSource = (key: string): string => key; +export const fromSnakeCase: KeyFormatSource = (key: string): string => { + return key + .toLowerCase() + .replaceAll(/(_[a-z0-9])/g, (group) => + group.toUpperCase().replace("_", ""), + ); +}; +export const fromSnakeCaseWithPrefix: (prefix: string) => KeyFormatSource = ( + prefix: string, +): KeyFormatSource => { + const prefixKey = prefix.toLowerCase() + "_"; + return (key: string): string => { + return key.toLowerCase().startsWith(prefixKey) + ? fromSnakeCase(key.substring(prefixKey.length)) + : key; + }; +}; + +/** + * Builds a complete `Option` object based on default options, environment variables and + * the partially given input options. + * Keys in the given option object can be transformed by the `transformKey` function. + * Environment variables need to be set in snake case with the prefix`JAZZER_`. + */ +export function processOptions( + inputs: Partial = {}, + transformKey: KeyFormatSource = fromCamelCase, + defaults: Options = defaultOptions, +): Options { + // Includes and excludes must be set together. + if (inputs && inputs.includes && !inputs.excludes) { + inputs.excludes = []; + } else if (inputs && inputs.excludes && !inputs.includes) { + inputs.includes = []; + } + + const defaultsWithEnv = mergeOptions( + process.env, + defaults, + fromSnakeCaseWithPrefix("JAZZER"), + false, + ); + const options = mergeOptions(inputs, defaultsWithEnv, transformKey); + // Set verbose mode environment variable via option or node DEBUG environment variable. + if (options.verbose || process.env.DEBUG) { + process.env.JAZZER_DEBUG = "1"; + } + return options; +} + +function mergeOptions( + input: unknown, + defaults: Options, + transformKey: (key: string) => string, + errorOnUnknown = true, +): Options { + // Deep close the default options to avoid mutation. + const options: Options = JSON.parse(JSON.stringify(defaults)); + if (!input || typeof input !== "object") { + return options; + } + Object.keys(input as object).forEach((key) => { + const transformedKey = transformKey(key); + if (!Object.hasOwn(options, transformedKey)) { + if (errorOnUnknown) { + throw new Error(`Unknown Jazzer.js option '${key}'`); + } + return; + } + // No way to dynamically resolve the types here, use (implicit) any for now. + // @ts-ignore + let resultValue = input[key]; + // Try to parse strings as JSON values to support setting arrays and + // objects via environment variables. + if (typeof resultValue === "string" || resultValue instanceof String) { + try { + resultValue = JSON.parse(resultValue.toString()); + } catch (ignore) { + // Ignore parsing errors and continue with the string value. + } + } + //@ts-ignore + const keyType = typeof options[transformedKey]; + if (typeof resultValue !== keyType) { + // @ts-ignore + throw new Error( + `Invalid type for Jazzer.js option '${key}', expected type '${keyType}'`, + ); + } + // Deep clone value to avoid reference keeping and unintended mutations. + // @ts-ignore + options[transformedKey] = JSON.parse(JSON.stringify(resultValue)); + }); + return options; +} + export function buildFuzzerOption(options: Options) { if (process.env.JAZZER_DEBUG) { console.debug("DEBUG: [core] Jazzer.js initial fuzzer arguments: "); diff --git a/packages/core/utils.test.ts b/packages/core/utils.test.ts index 23a03a49..c2e02e7e 100644 --- a/packages/core/utils.test.ts +++ b/packages/core/utils.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ensureFilepath } from "./utils"; +import { ensureFilepath, prepareArgs } from "./utils"; import path from "path"; @@ -35,4 +35,24 @@ describe("core", () => { expect(ensureFilepath("filename.js")).toMatch(expectedPath); }); }); + describe("prepareArgs", () => { + it("converts fuzzer args to strings", () => { + const args = { + _: ["-some_arg=value", "-other_arg", 123], + corpus: ["directory1", "directory2"], + fuzz_target: "filename.js", + }; + const options = prepareArgs(args); + expect(options).toEqual({ + fuzz_target: "file://" + path.join(process.cwd(), "filename.js"), + fuzzer_options: [ + "directory1", + "directory2", + "-some_arg=value", + "-other_arg", + "123", + ], + }); + }); + }); }); diff --git a/packages/core/utils.ts b/packages/core/utils.ts index d65cd6e5..0065848b 100644 --- a/packages/core/utils.ts +++ b/packages/core/utils.ts @@ -39,3 +39,26 @@ export function ensureFilepath(filePath: string): string { ? fullPath : fullPath + ".js"; } + +/** + * Transform arguments to common format, add compound properties and + * remove framework specific ones, so that the result can be passed on to the + * regular option handling code. + * + * The function is extracted to "utils" as importing "cli" in tests directly + * tries to parse command line arguments. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function prepareArgs(args: any) { + const options = { + ...args, + fuzz_target: ensureFilepath(args.fuzz_target), + fuzzer_options: (args.corpus ?? []) + .concat(args._) + .map((e: unknown) => e + ""), + }; + delete options._; + delete options.corpus; + delete options.$0; + return options; +} diff --git a/packages/fuzzer/addon.ts b/packages/fuzzer/addon.ts index 87bea915..c66cec6f 100644 --- a/packages/fuzzer/addon.ts +++ b/packages/fuzzer/addon.ts @@ -16,11 +16,13 @@ import { default as bind } from "bindings"; -export type FuzzTargetAsyncOrValue = (data: Buffer) => void | Promise; +export type FuzzTargetAsyncOrValue = ( + data: Buffer, +) => unknown | Promise; export type FuzzTargetCallback = ( data: Buffer, done: (e?: Error) => void, -) => void; +) => unknown; export type FuzzTarget = FuzzTargetAsyncOrValue | FuzzTargetCallback; export type FuzzOpts = string[]; diff --git a/packages/jest-runner/config.test.ts b/packages/jest-runner/config.test.ts index 7a9e9ad6..411c11e7 100644 --- a/packages/jest-runner/config.test.ts +++ b/packages/jest-runner/config.test.ts @@ -14,26 +14,42 @@ * limitations under the License. */ -import { defaultOptions, loadConfig } from "./config"; +import { defaultOptions } from "@jazzer.js/core"; +import { loadConfig } from "./config"; describe("Config", () => { describe("loadConfig", () => { - it("returns default configuration if nothing found", () => { - expect(loadConfig()).toEqual(defaultOptions); + it("return default configuration if nothing found", () => { + const defaults = { ...defaultOptions }; + defaults.mode = "regression"; + expect(loadConfig()).toEqual(defaults); }); - - it("merges found and default options", () => { - const config = loadConfig("test-jazzerjs"); + it("merge found and default options", () => { + const config = loadConfig({}, "test-jazzerjs"); expect(config).not.toEqual(defaultOptions); expect(config.includes).toContain("target"); expect(config.excludes).toContain("nothing"); }); - - it("configs are deep copied", () => { + it("merge explicitly passed in options", () => { + const config = loadConfig({ fuzzTarget: "foo" }, "test-jazzerjs"); + expect(config.fuzzTarget).toEqual("foo"); + }); + it("deep copy configurations", () => { const config1 = loadConfig(); config1.fuzzerOptions.push("-runs=100"); - const config2 = loadConfig("merge-test-jazzerjs"); + const config2 = loadConfig({}, "merge-test-jazzerjs"); expect(config1.fuzzerOptions).not.toEqual(config2.fuzzerOptions); }); + it("default to regression mode", () => { + expect(loadConfig().mode).toEqual("regression"); + }); + it("set fuzzing mode based on environment variable", () => { + try { + process.env.JAZZER_FUZZ = "1"; + expect(loadConfig().mode).toEqual("fuzzing"); + } finally { + delete process.env.JAZZER_FUZZ; + } + }); }); }); diff --git a/packages/jest-runner/config.ts b/packages/jest-runner/config.ts index 2bbf6a41..fab74c33 100644 --- a/packages/jest-runner/config.ts +++ b/packages/jest-runner/config.ts @@ -15,59 +15,25 @@ */ import { cosmiconfigSync } from "cosmiconfig"; -import { Options } from "@jazzer.js/core"; +import { processOptions, Options } from "@jazzer.js/core"; -export const defaultOptions: Options = { - dryRun: true, - includes: ["*"], - excludes: ["node_modules"], - fuzzTarget: "", - fuzzEntryPoint: "", - customHooks: [], - fuzzerOptions: [], - sync: false, - expectedErrors: [], - timeout: 5000, // default Jest timeout - coverage: false, - coverageDirectory: "coverage", - coverageReporters: ["json", "text", "lcov", "clover"], // default Jest reporters - disableBugDetectors: [], - mode: "regression", - verbose: false, -}; - -// Looks up Jazzer.js options via the `jazzer-runner` configuration from -// within different configuration files. -export function loadConfig(optionsKey = "jazzerjs"): Options { +// Lookup `Options` via the `.jazzerjsrc` configuration files. +export function loadConfig( + options: Partial = {}, + optionsKey = "jazzerjs", +): Options { const result = cosmiconfigSync(optionsKey).search(); - const defaultOptionsCopy = JSON.parse(JSON.stringify(defaultOptions)); - let config; - if (result === null) { - config = defaultOptionsCopy; - } else { - config = Object.keys(defaultOptions).reduce( - (config: Options, key: string) => { - if (key in result.config) { - config = { ...config, [key]: result.config[key] }; - } - return config; - }, - defaultOptionsCopy, - ); + const config = result?.config ?? {}; + // Jazzer.js normally runs in "fuzzing" mode, but, + // if not specified otherwise, Jest uses "regression" mode. + if (!config.mode) { + config.mode = "regression"; } - // Switch to fuzzing mode if environment variable `JAZZER_FUZZ` is set. if (process.env.JAZZER_FUZZ) { config.mode = "fuzzing"; } - - if (config.mode === "fuzzing") { - config.dryRun = false; - } - - if (config.verbose) { - process.env.JAZZER_DEBUG = "1"; - } - - return config; + // Merge explicitly passed in options. + Object.assign(config, options); + return processOptions(config); } diff --git a/packages/jest-runner/fuzz.test.ts b/packages/jest-runner/fuzz.test.ts index 2b50a53d..09135170 100644 --- a/packages/jest-runner/fuzz.test.ts +++ b/packages/jest-runner/fuzz.test.ts @@ -46,7 +46,7 @@ import { runInFuzzingMode, runInRegressionMode, } from "./fuzz"; -import { defaultOptions } from "./config"; +import { Options } from "@jazzer.js/core"; // Cleanup created files on exit tmp.setGracefulCleanup(); @@ -60,17 +60,20 @@ describe("fuzz", () => { it("execute only one fuzz target function", async () => { const testFn = jest.fn(); const corpus = new Corpus("", []); + const options = { + fuzzerOptions: ["--runs=1"], + } as Options; // First call should start the fuzzer await withMockTest(() => { - runInFuzzingMode("first", testFn, corpus, defaultOptions); + runInFuzzingMode("first", testFn, corpus, options); }); expect(startFuzzingMock).toBeCalledTimes(1); // Should fail to start the fuzzer a second time await expect( withMockTest(() => { - runInFuzzingMode("second", testFn, corpus, defaultOptions); + runInFuzzingMode("second", testFn, corpus, options); }), ).rejects.toThrow(FuzzerStartError); expect(startFuzzingMock).toBeCalledTimes(1); diff --git a/packages/jest-runner/index.ts b/packages/jest-runner/index.ts index 3c988e12..557d8f60 100644 --- a/packages/jest-runner/index.ts +++ b/packages/jest-runner/index.ts @@ -49,9 +49,11 @@ class FuzzRunner extends CallbackTestRunner { onFailure: OnTestFailure, options: TestRunnerOptions, ): Promise { - const config = loadConfig(); - config.coverage = this.shouldCollectCoverage; - config.coverageReporters = this.coverageReporters as reports.ReportType[]; + // Prefer Jest coverage configuration. + const config = loadConfig({ + coverage: this.shouldCollectCoverage, + coverageReporters: this.coverageReporters as reports.ReportType[], + }); await initFuzzing(config); return this.#runTestsInBand(tests, watcher, onStart, onResult, onFailure); diff --git a/packages/jest-runner/package.json b/packages/jest-runner/package.json index 1f23b143..a18d93e1 100644 --- a/packages/jest-runner/package.json +++ b/packages/jest-runner/package.json @@ -21,8 +21,6 @@ "istanbul-reports": "^3.1.6" }, "devDependencies": { - "jest": "^29.6.2", - "@types/istanbul-reports": "^3.0.1", "@types/tmp": "^0.2.3", "tmp": "^0.2.1" }, diff --git a/tests/FuzzedDataProvider/fuzz.js b/tests/FuzzedDataProvider/fuzz.js index 3899321e..4ac17796 100644 --- a/tests/FuzzedDataProvider/fuzz.js +++ b/tests/FuzzedDataProvider/fuzz.js @@ -23,16 +23,9 @@ module.exports.fuzz = function (fuzzerInputData) { const data = new FuzzedDataProvider(fuzzerInputData); const s1 = data.consumeString(data.consumeIntegralInRange(10, 15), "utf-8"); const i1 = data.consumeIntegral(1); - const i2 = data.consumeIntegral(2); - let i3 = data.consumeIntegral(4); - - if (i3 === 1000) { - if (s1 === "Hello World!") { - if (i1 === 3) { - if (i2 === 3) { - throw new Error("Crash!"); - } - } + if (s1 === "Hello World!") { + if (i1 === 3) { + throw new Error("Crash!"); } } }; diff --git a/tests/FuzzedDataProvider/package.json b/tests/FuzzedDataProvider/package.json index 75f30cda..06a037bc 100644 --- a/tests/FuzzedDataProvider/package.json +++ b/tests/FuzzedDataProvider/package.json @@ -4,7 +4,7 @@ "description": "An example showing how to use FuzzedDataProvider in Jazzer.js", "scripts": { "fuzz": "jazzer fuzz --sync -x Error -i fuzz.js -- -use_value_profile=1 -print_pcs=1 -print_final_stats=1 -max_len=52 -runs=4000000 -seed=605643277", - "dryRun": "jazzer fuzz -d --sync -- -runs=100 -seed=123456789" + "dryRun": "jazzer fuzz --sync -- -runs=100 -seed=123456789" }, "devDependencies": { "@jazzer.js/core": "file:../../packages/core" diff --git a/tests/bug-detectors/prototype-pollution.test.js b/tests/bug-detectors/prototype-pollution.test.js index 8ec303df..90f51279 100644 --- a/tests/bug-detectors/prototype-pollution.test.js +++ b/tests/bug-detectors/prototype-pollution.test.js @@ -393,6 +393,24 @@ describe("Prototype Pollution Jest tests", () => { "Prototype Pollution: Prototype of Object changed", ); }); + + it("Fuzzing mode instrumentation off - variable declaration", () => { + const fuzzTest = new FuzzTestBuilder() + .runs(0) + .customHooks([ + path.join(bugDetectorDirectory, "instrument-all.config.js"), + ]) + .dir(bugDetectorDirectory) + .dryRun(true) + .jestRunInFuzzingMode(true) + .jestTestFile("tests.fuzz.js") + .jestTestName("Variable declarations") + .build(); + expect(() => { + fuzzTest.execute(); + }).toThrow(); + expect(fuzzTest.stderr).toContain("[Prototype Pollution Configuration]"); + }); }); describe("Prototype Pollution instrumentation correctness tests", () => { @@ -448,6 +466,7 @@ describe("Prototype Pollution instrumentation correctness tests", () => { .dryRun(false) .fuzzEntryPoint("LambdaVariableDeclaration") .fuzzFile(fuzzFile) + .verbose(true) .build(); fuzzTest.execute(); }); diff --git a/tests/fork_mode/package.json b/tests/fork_mode/package.json index f4a263b0..7b6b2cd2 100644 --- a/tests/fork_mode/package.json +++ b/tests/fork_mode/package.json @@ -4,7 +4,7 @@ "description": "An example showing how to use libFuzzer's fork mode in Jazzer.js", "scripts": { "fuzz": "jazzer fuzz --sync -- -fork=3", - "dryRun": "jazzer fuzz -d --sync -- -fork=3 -runs=100 -seed=123456789" + "dryRun": "jazzer fuzz --sync -- -fork=3 -runs=100 -seed=123456789" }, "devDependencies": { "@jazzer.js/core": "file:../../packages/core" diff --git a/tests/helpers.js b/tests/helpers.js index b20f6907..7b3b5116 100644 --- a/tests/helpers.js +++ b/tests/helpers.js @@ -46,6 +46,7 @@ class FuzzTest { sync, verbose, coverage, + expectedErrors, ) { this.includes = includes; this.excludes = excludes; @@ -65,6 +66,7 @@ class FuzzTest { this.sync = sync; this.verbose = verbose; this.coverage = coverage; + this.expectedErrors = expectedErrors; } execute() { @@ -80,7 +82,7 @@ class FuzzTest { options.push("-f " + this.fuzzEntryPoint); if (this.sync) options.push("--sync"); if (this.coverage) options.push("--coverage"); - if (this.dryRun !== undefined) options.push("--dryRun=" + this.dryRun); + if (this.dryRun !== undefined) options.push("--dry_run=" + this.dryRun); for (const include of this.includes) { options.push("-i=" + include); } @@ -93,6 +95,9 @@ class FuzzTest { for (const customHook of this.customHooks) { options.push("--custom_hooks=" + customHook); } + for (const expectedError of this.expectedErrors) { + options.push("-x=" + expectedError); + } options.push("--"); if (this.runs !== undefined) options.push("-runs=" + this.runs); if (this.forkMode) options.push("-fork=" + this.forkMode); @@ -201,6 +206,7 @@ class FuzzTestBuilder { _jestRunInFuzzingMode = undefined; _dictionaries = []; _coverage = false; + _expectedErrors = []; /** * @param {boolean} sync - whether to run the fuzz test in synchronous mode. @@ -362,6 +368,11 @@ class FuzzTestBuilder { return this; } + expectedErrors(...expectedError) { + this._expectedErrors = expectedError; + return this; + } + build() { if (this._jestTestFile === "" && this._fuzzEntryPoint === "") { throw new Error("fuzzEntryPoint or jestTestFile are not set."); @@ -390,6 +401,7 @@ class FuzzTestBuilder { this._sync, this._verbose, this._coverage, + this._expectedErrors, ); } } diff --git a/tests/return_values/return_values.test.js b/tests/return_values/return_values.test.js index 3c2b3631..73fa257d 100644 --- a/tests/return_values/return_values.test.js +++ b/tests/return_values/return_values.test.js @@ -14,32 +14,31 @@ * limitations under the License. */ -const { spawnSync } = require("child_process"); const path = require("path"); -const SyncInfo = - "Exclusively observed synchronous return values from fuzzed function. Fuzzing in synchronous mode seems beneficial!"; -const AsyncInfo = - "Observed asynchronous return values from fuzzed function. Fuzzing in asynchronous mode seems beneficial!"; +const { FuzzTestBuilder } = require("../helpers.js"); -// current working directory const testDirectory = __dirname; +const syncInfo = + "Exclusively observed synchronous return values from fuzzed function. Fuzzing in synchronous mode seems beneficial!"; +const asyncInfo = + "Observed asynchronous return values from fuzzed function. Fuzzing in asynchronous mode seems beneficial!"; describe("Execute a sync runner", () => { it("Expect a hint due to async and sync return values", () => { const testCaseDir = path.join(testDirectory, "syncRunnerMixedReturns"); - const log = executeFuzzTest(true, false, testCaseDir); - expect(log).toContain(AsyncInfo.trim()); + const log = executeFuzzTest(true, true, testCaseDir); + expect(log).toContain(asyncInfo.trim()); }); it("Expect a hint due to exclusively async return values", () => { const testCaseDir = path.join(testDirectory, "syncRunnerAsyncReturns"); const log = executeFuzzTest(true, false, testCaseDir); - expect(log.trim()).toContain(AsyncInfo.trim()); + expect(log.trim()).toContain(asyncInfo.trim()); }); it("Expect no hint due to strict synchronous return values", () => { const testCaseDir = path.join(testDirectory, "syncRunnerSyncReturns"); const log = executeFuzzTest(true, false, testCaseDir); - expect(log.includes(SyncInfo)).toBeFalsy(); - expect(log.includes(AsyncInfo)).toBeFalsy(); + expect(log.includes(syncInfo)).toBeFalsy(); + expect(log.includes(asyncInfo)).toBeFalsy(); }); }); @@ -47,34 +46,31 @@ describe("Execute a async runner", () => { it("Expect no hint due to async and sync return values", () => { const testCaseDir = path.join(testDirectory, "asyncRunnerMixedReturns"); const log = executeFuzzTest(false, false, testCaseDir); - expect(log.includes(SyncInfo)).toBeFalsy(); - expect(log.includes(AsyncInfo)).toBeFalsy(); + expect(log.includes(syncInfo)).toBeFalsy(); + expect(log.includes(asyncInfo)).toBeFalsy(); }); it("Expect a hint due to exclusively sync return values", () => { const testCaseDir = path.join(testDirectory, "asyncRunnerSyncReturns"); const log = executeFuzzTest(false, false, testCaseDir); - expect(log.trim()).toContain(SyncInfo.trim()); + expect(log.trim()).toContain(syncInfo.trim()); }); it("Expect no hint due to strict asynchronous return values", () => { const testCaseDir = path.join(testDirectory, "asyncRunnerAsyncReturns"); const log = executeFuzzTest(false, false, testCaseDir); - expect(log.includes(SyncInfo)).toBeFalsy(); - expect(log.includes(AsyncInfo)).toBeFalsy(); + expect(log.includes(syncInfo)).toBeFalsy(); + expect(log.includes(asyncInfo)).toBeFalsy(); }); }); function executeFuzzTest(sync, verbose, dir) { - let options = ["jazzer", "fuzz"]; - // Specify mode - if (sync) options.push("--sync"); - options.push("--"); - const process = spawnSync("npx", options, { - stdio: "pipe", - cwd: dir, - shell: true, - windowsHide: true, - }); - let stdout = process.output.toString(); - if (verbose) console.log(stdout); - return stdout; + const fuzzTest = new FuzzTestBuilder() + .fuzzEntryPoint("fuzz") + .runs(5000) + .dir(dir) + .sync(sync) + .verbose(verbose) + .expectedErrors("Error") + .build(); + fuzzTest.execute(); + return fuzzTest.stderr; } diff --git a/tests/value_profiling/fuzz.js b/tests/value_profiling/fuzz.js index 8c914271..1f430eb9 100644 --- a/tests/value_profiling/fuzz.js +++ b/tests/value_profiling/fuzz.js @@ -30,9 +30,7 @@ module.exports.fuzz = function (data) { } if ( encrypt(data.readInt32BE(0)) === 0x50555637 && - encrypt(data.readInt32BE(4)) === 0x7e4f5664 && - encrypt(data.readInt32BE(8)) === 0x5757493e && - encrypt(data.readInt32BE(12)) === 0x784c5465 + encrypt(data.readInt32BE(4)) === 0x7e4f5664 ) { throw Error("XOR with a constant is not a secure encryption method ;-)"); }