Skip to content

Latest commit

 

History

History
398 lines (312 loc) · 13 KB

jest-integration.md

File metadata and controls

398 lines (312 loc) · 13 KB

Fuzzing using the Jest integration

To use fuzzing in your normal development workflow, a tight integration into the Jest test framework is provided. Using this, fuzz tests can be executed alongside your normal unit tests and seamlessly detect problems on your local machine or in your CI, and check that found bugs stay resolved forever.

Furthermore, the Jest integration enables great IDE support, so that individual inputs can be run, or even debugged, similar to what you would expect from normal Jest tests.

The Jest integration provides two modes of execution, which will be explained in detail further down on this page.

  • Fuzzing Mode: Fuzzing a function through a Jest test.
  • Regression Mode: Using initially provided seeds and inputs of found problems to execute the Jest test with.

Setting up the Jazzer.js Jest integration

Jest enables the execution of tests by third-party tools through custom test runners. Jazzer.js provides such a runner in the @jazzer.js/jest-runner package.

To use the integration, add a dev-dependency to @jazzer.js/jest-runner to your project. To do so, execute the following command in your project root directory.

npm install --save-dev @jazzer.js/jest-runner

This will install the custom Jest runner along with all other required Jazzer.js dependencies.

Note: The Jazzer.js Jest runner requires Jest version 29 or higher.

For Jest to pick up the custom fuzz test runner, it has to be added to the Jest configuration in the package.json or jest.config.js, as described on the Configuring Jest documentation page. The following snippet shows how to add the Jazzer.js fuzz test runner to the Jest configuration in package.json, so that all files with the suffix .fuzz.js will be executed by it. Other tests are still executed by the default test runner.

{
	"name": "jest-integration-example",
	"scripts": {
		"test": "jest",
		"fuzz": "JAZZER_FUZZ=1 jest",
		"coverage": "jest --coverage"
	},
	"devDependencies": {
		"@jazzer.js/jest-runner": "1.1.0",
		"jest": "29.3.1"
	},
	"jest": {
		"projects": [
			{
				"displayName": "test"
			},
			{
				"displayName": {
					"name": "Jazzer.js",
					"color": "cyan"
				},
				"testMatch": ["<rootDir>/**/*.fuzz.js"],
				"testRunner": "@jazzer.js/jest-runner"
			}
		]
	}
}

Further fuzzer configuration can be specified in .jazzerjsrc.json in the project directory using the following format. These are the same properties which can be specified through the CLI client.

{
	"includes": ["*"],
	"excludes": ["node_modules"],
	"customHooks": [],
	"fuzzerOptions": [],
	"sync": false,
	"timeout": 1000
}

Enabling TypeScript Jest tests

Jest supports execution of tests written in other languages than JavaScript via dedicated extensions. Probably most prominent is its TypeScript support, which can be enabled via ts-jest.

We assume you already set up your TypeScript project according to the ts-jest documentation. The following section shows a minimal configuration to enable TypeScript support for Jest fuzz tests. Furthermore, an example project is available at jest_typescript_integration.

In addition to the configuration shown in the last section, ts-jest has to be added as dev-dependency to the project.

npm install --save-dev ts-jest

The Jazzer.js runner configuration also needs to reference ts-jest, most commonly by setting the preset property to ts-jest. Also make sure to actually include test files with the .fuzz.ts extension.

{
  displayName: {
    name: "Jazzer.js",
    color: "cyan",
  },
  preset: "ts-jest",
  testEnvironment: "node",
  testMatch: ["<rootDir>/*.fuzz.[jt]s"],
  testRunner: "@jazzer.js/jest-runner",
},

To introduce the fuzz function types globally, add the following import to globals.d.ts. This could also be done in the individual test files.

import "@jazzer.js/jest-runner";

To provide accurate coverage reports for TypeScript fuzz tests, make sure to enable source map generation in the TypeScript compiler options:

{
	"compilerOptions": {
		"sourceMap": true
	}
}

These settings should be enough to start writing Jest fuzz tests in TypeScript.

Note: Using custom hooks written in TypeScript is currently not supported, as those are not pre-processed by Jest.

Writing a Jest fuzz test

To create a fuzz test, the fuzz function on Jest's test and it can be used.

fuzz expects similar parameters as normal Jest tests, but provides fuzzer generated input as a first parameter to the test function. As expected, the input generation is guided by coverage and other execution feedback, which is possible due to code instrumentation.

The passed in fuzzer input is of type Buffer, a subclass of Uint8Array, and can be used to create needed parameters for the actual code under test, so that the fuzzer can detect the usage of parts of the input and mutate them in the next iterations, to reach new code paths.

The resulting code looks very similar to normal Jest tests.

describe("My describe", () => {
	it.fuzz(
		"My fuzz test",
		(data) => {
			target.fuzzMe(data);
		},
		2000,
	);
});

If the function you want to test expects parameters of types other than Buffer, you have to take care of constructing the corresponding arguments from the provided buffer. To facilitate this step, Jazzer.js provides a utility class FuzzedDataProvider that you can use to consume data of certain types from the fuzzer input. An example on how to use everything in combination is shown below.

const target = require("./target");
const { FuzzedDataProvider } = require("@jazzer.js/core");

describe("My describe", () => {
  it.fuzz("My fuzz test", (data) => {
    const provider = new FuzzedDataProvider(data);
    target.fuzzMeMore(
      provider.consumeNumber(),
      provider.consumeBoolean(),
      provider.consumeRemainingAsString());
  });

For more information on how to use the FuzzedDataProvider class, please refer to the example, the tests, and the implementation of the FuzzedDataProvider class.

Async Jest fuzz tests

Similar on how one would test asynchronous code in Jest, it's possible to use async/await, Promise and done callback based tests.

describe("My describe", () => {
  it.fuzz("My callback fuzz test", (data, done) => {
    target.callbackFuzzMe(data, done);
  });

  it.fuzz("My async fuzz test", async (data) => {
    await target.asyncFuzzMe(data);
  });
)};

TypeScript Jest fuzz tests

After the setup mentioned previously, Jest fuzz tests can be written in TypeScript, just as one would expect.

import "@jazzer.js/jest-runner";
import * as target from "./target";

describe("Target", () => {
	it.fuzz("executes a method", (data: Buffer) => {
		target.fuzzMe(data);
	});
});

Executing Jest fuzz tests

As mentioned above, the Jazzer.js fuzz test runner provides two modes of execution: fuzzing mode and regression mode.

Fuzzing mode

As the name suggests, the fuzzing mode uses Jazzer.js to fuzz a test function.

To execute tests in the fuzzing mode, set the environment variable JAZZER_FUZZ to a non-null value and execute Jest as normal.

JAZZER_FUZZ=1 npm test

Note: Due to limitations of the underlying fuzzer, only one fuzz test can be executed in this mode, all others are skipped by the runner. To start different fuzz tests the Jest parameter --testNamePattern can be used, e.g. npx jest --testNamePattern="My describe".

Inputs triggering issues, like uncaught exceptions, timeouts, etc., are stored in a directory structure named according to the test file and internal test structure. For example, an issue found in the test "My fuzz test" in the describe block "My describe" in the test file fuzztests.fuzz.js would end up in the directory ./fuzztest.fuzz/My_describe/My_fuzz_test. Files in these directories will be used in the regression mode to verify that the underlying issues have been resolved.

Furthermore, as fuzzing is a time-consuming task, interesting intermediate inputs are stored in a similar directory structure located in .cifuzz-corpus at the project root. These inputs are used by the fuzzer to speed up the next run by reusing the already discovered inputs.

Executing fuzz tests in fuzzing mode on the command line could look like the following snippet. This output may differ depending on the used Jest reporter.

» JAZZER_FUZZ=1 npm test

 RUNS   Jazzer.js  ./integration.fuzz.js
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 3712434369
INFO: Loaded 1 modules   (512 inline 8-bit counters): 512 [0x7fb804068010, 0x7fb804068210),
INFO: Loaded 1 PC tables (512 PCs): 512 [0x7fb7fc7fa010,0x7fb7fc7fc010),
INFO:        0 files found in /jazzer.js/examples/jest_integration/.cifuzz-corpus/integration.fuzz/My_describe/My_fuzz_test/
INFO:        1 files found in /jazzer.js/examples/jest_integration/integration.fuzz/My_describe/My_fuzz_test/
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: seed corpus: files: 1 min: 4b max: 4b total: 4b rss: 173Mb
#2      INITED cov: 3 ft: 3 corp: 1/4b exec/s: 0 rss: 173Mb
#7      REDUCE cov: 3 ft: 3 corp: 1/2b lim: 4 exec/s: 0 rss: 173Mb L: 2/2 MS: 5 CopyPart-ChangeByte-ShuffleBytes-CopyPart-EraseBytes-
#8      REDUCE cov: 3 ft: 3 corp: 1/1b lim: 4 exec/s: 0 rss: 173Mb L: 1/1 MS: 1 EraseBytes-
#1377   REDUCE cov: 6 ft: 6 corp: 2/17b lim: 17 exec/s: 0 rss: 173Mb L: 16/16 MS: 4 CopyPart-CMP-CopyPart-InsertRepeatedBytes- DE: "\377\377\377\37
 FAIL   Jazzer.js  ./integration.fuzz.js (19.887 s)
  ● My describe › My fuzz test

        Error: Welcome to Awesome Fuzzing!

      25 |              s[15] === "!"
      26 |      ) {
    > 27 |              throw Error("Welcome to Awesome Fuzzing!");
         |                      ^
      28 |      }
      29 | };
      30 |

      at Object.fuzzMe (target.js:27:11)
      at integration.fuzz.js:24:12

  ● My describe › My callback fuzz test

        [FuzzerStartError: Fuzzer already started. Please provide single fuzz test using --testNamePattern. Skipping test "My callback fuzz test"]

  ● My describe › My async fuzz test

        [FuzzerStartError: Fuzzer already started. Please provide single fuzz test using --testNamePattern. Skipping test "My async fuzz test"]

Test Suites: 1 failed, 0 passed, 1 total
Tests:       1 failed, 2 skipped, 0 passed, 3 total
Snapshots:   0 total
Time:        21.137 s
Ran all test suites

Regression mode

The regression mode is the default execution mode and verifies that once found issues are resolved and stay that way.

In this mode, the previously found problematic inputs are used to invoke the fuzz tests and verify that no error is generated anymore. If the described directory structure does not contain inputs for a given test, it will be skipped by the runner.

The file name of every input is used to generate a dedicated test entry in the overall Jest report.

Executing fuzz tests in the regression mode on the command line could look like the following snippet. This output may differ depending on the used Jest reporter.

» npm  test

 PASS   Jazzer.js  ./integration.fuzz.js
  My describe
    ○ skipped Sync timeout
    ○ skipped Async timeout
    ○ skipped Done callback timeout
    My fuzz test
      ✓ one
    My callback fuzz test
      ✓ two
    My async fuzz test
      ✓ three

Test Suites: 1 passed, 1 total
Tests:       3 skipped, 3 passed, 6 total
Snapshots:   0 total
Time:        0.335 s, estimated 1 s
Ran all test suites.

Coverage report generation

To generate a coverage report, run jest with the --coverage flag:

npx jest --coverage

Note: Unlike the Jazzer.js CLI, Jest only accepts the long flag of --coverage!

Additional options for coverage report generation are described in the fuzz targets documentation.

The desired report format can be set by the flag --coverage_reporters, which by default is set to --coverage_reporters clover json lcov text. See here for a list of supported coverage reporters.

IDE Integration

As the Jest test framework foundations are used by the Jazzer.js fuzz test runner, all major IDEs supporting Jest should pick it up as well. This means that IDE specific Jest configuration can be used for fuzz tests.

Especially handy is the debug functionality of individual tests provided by most IDEs. Simply set a breakpoint inside your IDE and re-run a failed fuzz test to enter the debugger for that particular input.

VS code Jest support

VS code Jest integration

IntelliJ Jest support

IntelliJ Jest integration