From 365fe90e829233c06e31aaf2d2207cd6ff04fa3d Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Mon, 16 Jan 2023 05:51:21 +0100 Subject: [PATCH 01/22] instrumentor: remove unused function --- packages/instrumentor/plugins/helpers.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/instrumentor/plugins/helpers.ts b/packages/instrumentor/plugins/helpers.ts index 6b7cb61a..9f27718a 100644 --- a/packages/instrumentor/plugins/helpers.ts +++ b/packages/instrumentor/plugins/helpers.ts @@ -1,11 +1,7 @@ -import { NumericLiteral, Identifier } from "@babel/types"; +import { NumericLiteral } from "@babel/types"; import { types } from "@babel/core"; import * as crypto from "crypto"; export function fakePC(): NumericLiteral { return types.numericLiteral(crypto.randomInt(512)); } - -export function newIdentifier(prefix: string): Identifier { - return types.identifier(`jazzer_${prefix}_${crypto.randomInt(512)}`); -} From 935e483e91f239d5b5cc7d656153c36b4184f31a Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Mon, 16 Jan 2023 06:14:35 +0100 Subject: [PATCH 02/22] fuzzer: use 0 as the first edge ID The previous implementation did not use the first slot of the coverage map. The first edge ID returned was 1. --- packages/fuzzer/fuzzer.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/fuzzer/fuzzer.ts b/packages/fuzzer/fuzzer.ts index fb591eb9..6314175e 100644 --- a/packages/fuzzer/fuzzer.ts +++ b/packages/fuzzer/fuzzer.ts @@ -26,16 +26,19 @@ addon.registerCoverageMap(coverageMap); addon.registerNewCounters(0, INITIAL_NUM_COUNTERS); let currentNumCounters = INITIAL_NUM_COUNTERS; -let currentCounter = 0; +let nextEdgeID = 0; // Returns the next counter id to use for edge coverage. // If needed, the coverage map is enlarged. function nextCounter(): number { - currentCounter++; + enlargeCountersBufferIfNeeded(nextEdgeID); + return nextEdgeID++; +} +function enlargeCountersBufferIfNeeded(nextEdgeID: number) { // Enlarge registered counters if needed let newNumCounters = currentNumCounters; - while (currentCounter >= newNumCounters) { + while (nextEdgeID >= newNumCounters) { newNumCounters = 2 * newNumCounters; if (newNumCounters > MAX_NUM_COUNTERS) { throw new Error( @@ -50,7 +53,6 @@ function nextCounter(): number { currentNumCounters = newNumCounters; console.log(`INFO: New number of coverage counters ${currentNumCounters}`); } - return currentCounter; } /** From b3a488848ea8e66651842231b0d25867e8a00903 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Mon, 16 Jan 2023 11:09:28 +0100 Subject: [PATCH 03/22] fuzzer: move counter-related code into a dedicated file --- packages/fuzzer/coverage.ts | 78 +++++++++++++++++++++++++++++++++++++ packages/fuzzer/fuzzer.ts | 58 ++++----------------------- 2 files changed, 85 insertions(+), 51 deletions(-) create mode 100644 packages/fuzzer/coverage.ts diff --git a/packages/fuzzer/coverage.ts b/packages/fuzzer/coverage.ts new file mode 100644 index 00000000..035578b6 --- /dev/null +++ b/packages/fuzzer/coverage.ts @@ -0,0 +1,78 @@ +/* + * 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. + */ + +const MAX_NUM_COUNTERS: number = 1 << 20; +const INITIAL_NUM_COUNTERS: number = 1 << 9; +let coverageMap: Buffer; +let currentNumCounters: number; +let nextEdgeID = 0; + +type NativeAddon = { + registerCoverageMap: (buffer: Buffer) => void; + registerNewCounters: (oldNumCounters: number, newNumCounters: number) => void; +}; + +let nativeAddon: NativeAddon; + +export function initializeCounters(addon: NativeAddon) { + coverageMap = Buffer.alloc(MAX_NUM_COUNTERS, 0); + addon.registerCoverageMap(coverageMap); + addon.registerNewCounters(0, INITIAL_NUM_COUNTERS); + currentNumCounters = INITIAL_NUM_COUNTERS; + nativeAddon = addon; +} + +export function enlargeCountersBufferIfNeeded(nextEdgeID: number) { + // Enlarge registered counters if needed + let newNumCounters = currentNumCounters; + while (nextEdgeID >= newNumCounters) { + newNumCounters = 2 * newNumCounters; + if (newNumCounters > MAX_NUM_COUNTERS) { + throw new Error( + `Maximum number (${MAX_NUM_COUNTERS}) of coverage counts exceeded.` + ); + } + } + + // Register new counters if enlarged + if (newNumCounters > currentNumCounters) { + nativeAddon.registerNewCounters(currentNumCounters, newNumCounters); + currentNumCounters = newNumCounters; + console.log(`INFO: New number of coverage counters ${currentNumCounters}`); + } +} + +// Returns the next counter id to use for edge coverage. +// If needed, the coverage map is enlarged. +export function nextCounter(): number { + enlargeCountersBufferIfNeeded(nextEdgeID); + return nextEdgeID++; +} + +/** + * Increments the coverage counter for a given ID. + * This function implements the NeverZero policy from AFL++. + * See https://aflplus.plus//papers/aflpp-woot2020.pdf + * @param id the id of the coverage counter to increment + */ +export function incrementCounter(id: number) { + const counter = coverageMap.readUint8(id); + coverageMap.writeUint8(counter == 255 ? 1 : counter + 1, id); +} + +export function readCounter(id: number): number { + return coverageMap.readUint8(id); +} diff --git a/packages/fuzzer/fuzzer.ts b/packages/fuzzer/fuzzer.ts index 6314175e..ef932dff 100644 --- a/packages/fuzzer/fuzzer.ts +++ b/packages/fuzzer/fuzzer.ts @@ -15,60 +15,16 @@ */ import { default as bind } from "bindings"; +import { + incrementCounter, + initializeCounters, + nextCounter, + readCounter, +} from "./coverage"; export const addon = bind("jazzerjs"); -const MAX_NUM_COUNTERS: number = 1 << 20; -const INITIAL_NUM_COUNTERS: number = 1 << 9; -const coverageMap = Buffer.alloc(MAX_NUM_COUNTERS, 0); - -addon.registerCoverageMap(coverageMap); -addon.registerNewCounters(0, INITIAL_NUM_COUNTERS); - -let currentNumCounters = INITIAL_NUM_COUNTERS; -let nextEdgeID = 0; - -// Returns the next counter id to use for edge coverage. -// If needed, the coverage map is enlarged. -function nextCounter(): number { - enlargeCountersBufferIfNeeded(nextEdgeID); - return nextEdgeID++; -} - -function enlargeCountersBufferIfNeeded(nextEdgeID: number) { - // Enlarge registered counters if needed - let newNumCounters = currentNumCounters; - while (nextEdgeID >= newNumCounters) { - newNumCounters = 2 * newNumCounters; - if (newNumCounters > MAX_NUM_COUNTERS) { - throw new Error( - `Maximum number (${MAX_NUM_COUNTERS}) of coverage counts exceeded.` - ); - } - } - - // Register new counters if enlarged - if (newNumCounters > currentNumCounters) { - addon.registerNewCounters(currentNumCounters, newNumCounters); - currentNumCounters = newNumCounters; - console.log(`INFO: New number of coverage counters ${currentNumCounters}`); - } -} - -/** - * Increments the coverage counter for a given ID. - * This function implements the NeverZero policy from AFL++. - * See https://aflplus.plus//papers/aflpp-woot2020.pdf - * @param id the id of the coverage counter to increment - */ -function incrementCounter(id: number) { - const counter = coverageMap.readUint8(id); - coverageMap.writeUint8(counter == 255 ? 1 : counter + 1, id); -} - -function readCounter(id: number): number { - return coverageMap.readUint8(id); -} +initializeCounters(addon); /** * Performs a string comparison between two strings and calls the corresponding native hook if needed. From e74b57d2860aa09afb5f9d4202ab6bd7952289e3 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Mon, 16 Jan 2023 12:41:11 +0100 Subject: [PATCH 04/22] fuzzer: refactor addon types/functions into dedicated file --- packages/fuzzer/addon.ts | 65 +++++++++++++++++++++++++++++++++++++ packages/fuzzer/coverage.ts | 17 ++++------ packages/fuzzer/fuzzer.ts | 25 +++++++------- 3 files changed, 82 insertions(+), 25 deletions(-) create mode 100644 packages/fuzzer/addon.ts diff --git a/packages/fuzzer/addon.ts b/packages/fuzzer/addon.ts new file mode 100644 index 00000000..aac4c26e --- /dev/null +++ b/packages/fuzzer/addon.ts @@ -0,0 +1,65 @@ +/* + * 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. + */ + +import { default as bind } from "bindings"; + +export type FuzzTargetAsyncOrValue = (data: Buffer) => void | Promise; +export type FuzzTargetCallback = ( + data: Buffer, + done: (e?: Error) => void +) => void; +export type FuzzTarget = FuzzTargetAsyncOrValue | FuzzTargetCallback; +export type FuzzOpts = string[]; + +export type StartFuzzingSyncFn = ( + fuzzFn: FuzzTarget, + fuzzOpts: FuzzOpts +) => void; +export type StartFuzzingAsyncFn = ( + fuzzFn: FuzzTarget, + fuzzOpts: FuzzOpts +) => Promise; + +export type NativeAddon = { + registerCoverageMap: (buffer: Buffer) => void; + registerNewCounters: (oldNumCounters: number, newNumCounters: number) => void; + + traceUnequalStrings: ( + hookId: number, + current: string, + target: string + ) => void; + + traceStringContainment: ( + hookId: number, + needle: string, + haystack: string + ) => void; + traceIntegerCompare: ( + hookId: number, + current: number, + target: number + ) => void; + + tracePcIndir: (hookId: number, state: number) => void; + + printVersion: () => void; + startFuzzing: StartFuzzingSyncFn; + startFuzzingAsync: StartFuzzingAsyncFn; + stopFuzzingAsync: () => void; +}; + +export const addon: NativeAddon = bind("jazzerjs"); diff --git a/packages/fuzzer/coverage.ts b/packages/fuzzer/coverage.ts index 035578b6..5fea53fb 100644 --- a/packages/fuzzer/coverage.ts +++ b/packages/fuzzer/coverage.ts @@ -14,25 +14,20 @@ * limitations under the License. */ +import { addon } from "./addon"; + const MAX_NUM_COUNTERS: number = 1 << 20; const INITIAL_NUM_COUNTERS: number = 1 << 9; let coverageMap: Buffer; let currentNumCounters: number; -let nextEdgeID = 0; - -type NativeAddon = { - registerCoverageMap: (buffer: Buffer) => void; - registerNewCounters: (oldNumCounters: number, newNumCounters: number) => void; -}; - -let nativeAddon: NativeAddon; +let nextEdgeID: number; -export function initializeCounters(addon: NativeAddon) { +export function initializeCounters() { coverageMap = Buffer.alloc(MAX_NUM_COUNTERS, 0); addon.registerCoverageMap(coverageMap); addon.registerNewCounters(0, INITIAL_NUM_COUNTERS); currentNumCounters = INITIAL_NUM_COUNTERS; - nativeAddon = addon; + nextEdgeID = 0; } export function enlargeCountersBufferIfNeeded(nextEdgeID: number) { @@ -49,7 +44,7 @@ export function enlargeCountersBufferIfNeeded(nextEdgeID: number) { // Register new counters if enlarged if (newNumCounters > currentNumCounters) { - nativeAddon.registerNewCounters(currentNumCounters, newNumCounters); + addon.registerNewCounters(currentNumCounters, newNumCounters); currentNumCounters = newNumCounters; console.log(`INFO: New number of coverage counters ${currentNumCounters}`); } diff --git a/packages/fuzzer/fuzzer.ts b/packages/fuzzer/fuzzer.ts index ef932dff..4b503d6c 100644 --- a/packages/fuzzer/fuzzer.ts +++ b/packages/fuzzer/fuzzer.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { default as bind } from "bindings"; +import { addon, StartFuzzingAsyncFn, StartFuzzingSyncFn } from "./addon"; import { incrementCounter, initializeCounters, @@ -22,9 +22,7 @@ import { readCounter, } from "./coverage"; -export const addon = bind("jazzerjs"); - -initializeCounters(addon); +initializeCounters(); /** * Performs a string comparison between two strings and calls the corresponding native hook if needed. @@ -126,19 +124,18 @@ function traceAndReturn(current: unknown, target: unknown, id: number) { return target; } -// Re-export everything from the native library. -export type FuzzTargetAsyncOrValue = (data: Buffer) => void | Promise; -export type FuzzTargetCallback = ( - data: Buffer, - done: (e?: Error) => void -) => void; -export type FuzzTarget = FuzzTargetAsyncOrValue | FuzzTargetCallback; -export type FuzzOpts = string[]; +export type { + FuzzTarget, + FuzzTargetAsyncOrValue, + FuzzTargetCallback, +} from "./addon"; + +export { addon } from "./addon"; export interface Fuzzer { printVersion: () => void; - startFuzzing: (fuzzFn: FuzzTarget, fuzzOpts: FuzzOpts) => void; - startFuzzingAsync: (fuzzFn: FuzzTarget, fuzzOpts: FuzzOpts) => Promise; + startFuzzing: StartFuzzingSyncFn; + startFuzzingAsync: StartFuzzingAsyncFn; stopFuzzingAsync: (status?: number) => void; nextCounter: typeof nextCounter; incrementCounter: typeof incrementCounter; From f95e4f18eb0a6ae49e8da1d4ac4ae948dcc8dd6c Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Mon, 16 Jan 2023 12:47:00 +0100 Subject: [PATCH 05/22] fuzzer: refactor tracing functions into dedicated file --- packages/fuzzer/fuzzer.ts | 101 +------------------------------- packages/fuzzer/trace.ts | 117 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 100 deletions(-) create mode 100644 packages/fuzzer/trace.ts diff --git a/packages/fuzzer/fuzzer.ts b/packages/fuzzer/fuzzer.ts index 4b503d6c..29117020 100644 --- a/packages/fuzzer/fuzzer.ts +++ b/packages/fuzzer/fuzzer.ts @@ -21,109 +21,10 @@ import { nextCounter, readCounter, } from "./coverage"; +import { traceAndReturn, traceNumberCmp, traceStrCmp } from "./trace"; initializeCounters(); -/** - * Performs a string comparison between two strings and calls the corresponding native hook if needed. - * This function replaces the original comparison expression and preserves the semantics by returning - * the original result after calling the native hook. - * @param s1 first compared string - * @param s2 second compared string - * @param operator the operator used in the comparison - * @param id an unique identifier to distinguish between the different comparisons - * @returns result of the comparison - */ -function traceStrCmp( - s1: string, - s2: string, - operator: string, - id: number -): boolean { - let result = false; - let shouldCallLibfuzzer = false; - switch (operator) { - case "==": - result = s1 == s2; - shouldCallLibfuzzer = !result; - break; - case "===": - result = s1 === s2; - shouldCallLibfuzzer = !result; - break; - case "!=": - result = s1 != s2; - shouldCallLibfuzzer = result; - break; - case "!==": - result = s1 !== s2; - shouldCallLibfuzzer = result; - break; - } - if (shouldCallLibfuzzer && s1 && s2) { - addon.traceUnequalStrings(id, s1, s2); - } - return result; -} - -/** - * Performs an integer comparison between two strings and calls the corresponding native hook if needed. - * This function replaces the original comparison expression and preserves the semantics by returning - * the original result after calling the native hook. - * @param n1 first compared number - * @param n2 second compared number - * @param operator the operator used in the comparison - * @param id an unique identifier to distinguish between the different comparisons - * @returns result of the comparison - */ -function traceNumberCmp( - n1: number, - n2: number, - operator: string, - id: number -): boolean { - if (Number.isInteger(n1) && Number.isInteger(n2)) { - addon.traceIntegerCompare(id, n1, n2); - } - switch (operator) { - case "==": - return n1 == n2; - case "===": - return n1 === n2; - case "!=": - return n1 != n2; - case "!==": - return n1 !== n2; - case ">": - return n1 > n2; - case ">=": - return n1 >= n2; - case "<": - return n1 < n2; - case "<=": - return n1 <= n2; - default: - throw `unexpected number comparison operator ${operator}`; - } -} - -function traceAndReturn(current: unknown, target: unknown, id: number) { - switch (typeof target) { - case "number": - if (typeof current === "number") { - if (Number.isInteger(current) && Number.isInteger(target)) { - addon.traceIntegerCompare(id, current, target); - } - } - break; - case "string": - if (typeof current === "string") { - addon.traceUnequalStrings(id, current, target); - } - } - return target; -} - export type { FuzzTarget, FuzzTargetAsyncOrValue, diff --git a/packages/fuzzer/trace.ts b/packages/fuzzer/trace.ts new file mode 100644 index 00000000..d7c163f7 --- /dev/null +++ b/packages/fuzzer/trace.ts @@ -0,0 +1,117 @@ +/* + * 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. + */ + +import { addon } from "./addon"; + +/** + * Performs a string comparison between two strings and calls the corresponding native hook if needed. + * This function replaces the original comparison expression and preserves the semantics by returning + * the original result after calling the native hook. + * @param s1 first compared string + * @param s2 second compared string + * @param operator the operator used in the comparison + * @param id an unique identifier to distinguish between the different comparisons + * @returns result of the comparison + */ +export function traceStrCmp( + s1: string, + s2: string, + operator: string, + id: number +): boolean { + let result = false; + let shouldCallLibfuzzer = false; + switch (operator) { + case "==": + result = s1 == s2; + shouldCallLibfuzzer = !result; + break; + case "===": + result = s1 === s2; + shouldCallLibfuzzer = !result; + break; + case "!=": + result = s1 != s2; + shouldCallLibfuzzer = result; + break; + case "!==": + result = s1 !== s2; + shouldCallLibfuzzer = result; + break; + } + if (shouldCallLibfuzzer && s1 && s2) { + addon.traceUnequalStrings(id, s1, s2); + } + return result; +} + +/** + * Performs an integer comparison between two strings and calls the corresponding native hook if needed. + * This function replaces the original comparison expression and preserves the semantics by returning + * the original result after calling the native hook. + * @param n1 first compared number + * @param n2 second compared number + * @param operator the operator used in the comparison + * @param id an unique identifier to distinguish between the different comparisons + * @returns result of the comparison + */ +export function traceNumberCmp( + n1: number, + n2: number, + operator: string, + id: number +): boolean { + if (Number.isInteger(n1) && Number.isInteger(n2)) { + addon.traceIntegerCompare(id, n1, n2); + } + switch (operator) { + case "==": + return n1 == n2; + case "===": + return n1 === n2; + case "!=": + return n1 != n2; + case "!==": + return n1 !== n2; + case ">": + return n1 > n2; + case ">=": + return n1 >= n2; + case "<": + return n1 < n2; + case "<=": + return n1 <= n2; + default: + throw `unexpected number comparison operator ${operator}`; + } +} + +export function traceAndReturn(current: unknown, target: unknown, id: number) { + switch (typeof target) { + case "number": + if (typeof current === "number") { + if (Number.isInteger(current) && Number.isInteger(target)) { + addon.traceIntegerCompare(id, current, target); + } + } + break; + case "string": + if (typeof current === "string") { + addon.traceUnequalStrings(id, current, target); + } + } + return target; +} From 82bea2ee4f46303a67c030efbbac12bb0ac0f267 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Mon, 16 Jan 2023 13:30:51 +0100 Subject: [PATCH 06/22] instrumentor: add edge ID generation strategy interface and a memory-sync implementation. The implemented policy is a refactoring of the current edge ID generation approach that should be used when fuzzing using a single process. --- packages/fuzzer/coverage.ts | 25 +-- packages/fuzzer/fuzzer.ts | 6 +- packages/instrumentor/edgeIdStrategy.ts | 45 ++++++ packages/instrumentor/instrument.test.ts | 5 +- packages/instrumentor/instrument.ts | 35 ++++- .../instrumentor/plugins/codeCoverage.test.ts | 20 ++- packages/instrumentor/plugins/codeCoverage.ts | 145 ++++++++++-------- 7 files changed, 182 insertions(+), 99 deletions(-) create mode 100644 packages/instrumentor/edgeIdStrategy.ts diff --git a/packages/fuzzer/coverage.ts b/packages/fuzzer/coverage.ts index 5fea53fb..03dac122 100644 --- a/packages/fuzzer/coverage.ts +++ b/packages/fuzzer/coverage.ts @@ -20,20 +20,18 @@ const MAX_NUM_COUNTERS: number = 1 << 20; const INITIAL_NUM_COUNTERS: number = 1 << 9; let coverageMap: Buffer; let currentNumCounters: number; -let nextEdgeID: number; export function initializeCounters() { coverageMap = Buffer.alloc(MAX_NUM_COUNTERS, 0); addon.registerCoverageMap(coverageMap); addon.registerNewCounters(0, INITIAL_NUM_COUNTERS); currentNumCounters = INITIAL_NUM_COUNTERS; - nextEdgeID = 0; } -export function enlargeCountersBufferIfNeeded(nextEdgeID: number) { +export function enlargeCountersBufferIfNeeded(nextEdgeId: number) { // Enlarge registered counters if needed let newNumCounters = currentNumCounters; - while (nextEdgeID >= newNumCounters) { + while (nextEdgeId >= newNumCounters) { newNumCounters = 2 * newNumCounters; if (newNumCounters > MAX_NUM_COUNTERS) { throw new Error( @@ -50,24 +48,17 @@ export function enlargeCountersBufferIfNeeded(nextEdgeID: number) { } } -// Returns the next counter id to use for edge coverage. -// If needed, the coverage map is enlarged. -export function nextCounter(): number { - enlargeCountersBufferIfNeeded(nextEdgeID); - return nextEdgeID++; -} - /** * Increments the coverage counter for a given ID. * This function implements the NeverZero policy from AFL++. * See https://aflplus.plus//papers/aflpp-woot2020.pdf - * @param id the id of the coverage counter to increment + * @param edgeId the edge ID of the coverage counter to increment */ -export function incrementCounter(id: number) { - const counter = coverageMap.readUint8(id); - coverageMap.writeUint8(counter == 255 ? 1 : counter + 1, id); +export function incrementCounter(edgeId: number) { + const counter = coverageMap.readUint8(edgeId); + coverageMap.writeUint8(counter == 255 ? 1 : counter + 1, edgeId); } -export function readCounter(id: number): number { - return coverageMap.readUint8(id); +export function readCounter(edgeId: number): number { + return coverageMap.readUint8(edgeId); } diff --git a/packages/fuzzer/fuzzer.ts b/packages/fuzzer/fuzzer.ts index 29117020..df42f8d9 100644 --- a/packages/fuzzer/fuzzer.ts +++ b/packages/fuzzer/fuzzer.ts @@ -18,8 +18,8 @@ import { addon, StartFuzzingAsyncFn, StartFuzzingSyncFn } from "./addon"; import { incrementCounter, initializeCounters, - nextCounter, readCounter, + enlargeCountersBufferIfNeeded, } from "./coverage"; import { traceAndReturn, traceNumberCmp, traceStrCmp } from "./trace"; @@ -38,12 +38,12 @@ export interface Fuzzer { startFuzzing: StartFuzzingSyncFn; startFuzzingAsync: StartFuzzingAsyncFn; stopFuzzingAsync: (status?: number) => void; - nextCounter: typeof nextCounter; incrementCounter: typeof incrementCounter; readCounter: typeof readCounter; traceStrCmp: typeof traceStrCmp; traceNumberCmp: typeof traceNumberCmp; traceAndReturn: typeof traceAndReturn; + enlargeCountersBufferIfNeeded: typeof enlargeCountersBufferIfNeeded; } export const fuzzer: Fuzzer = { @@ -51,10 +51,10 @@ export const fuzzer: Fuzzer = { startFuzzing: addon.startFuzzing, startFuzzingAsync: addon.startFuzzingAsync, stopFuzzingAsync: addon.stopFuzzingAsync, - nextCounter, incrementCounter, readCounter, traceStrCmp, traceNumberCmp, traceAndReturn, + enlargeCountersBufferIfNeeded, }; diff --git a/packages/instrumentor/edgeIdStrategy.ts b/packages/instrumentor/edgeIdStrategy.ts new file mode 100644 index 00000000..05c93b17 --- /dev/null +++ b/packages/instrumentor/edgeIdStrategy.ts @@ -0,0 +1,45 @@ +/* + * 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. + */ + +import { fuzzer } from "@jazzer.js/fuzzer"; + +export abstract class EdgeIdStrategy { + protected constructor(protected _nextEdgeId: number) {} + + nextEdgeId(): number { + fuzzer.enlargeCountersBufferIfNeeded(this._nextEdgeId); + return this._nextEdgeId++; + } + + abstract startForSourceFile(filename: string): void; + abstract commitIdCount(filename: string): void; +} + +export class MemorySyncIdStrategy extends EdgeIdStrategy { + constructor() { + super(0); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + startForSourceFile(filename: string): void { + // nothing to do here + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + commitIdCount(filename: string) { + // nothing to do here + } +} diff --git a/packages/instrumentor/instrument.test.ts b/packages/instrumentor/instrument.test.ts index 70f52930..a0b621df 100644 --- a/packages/instrumentor/instrument.test.ts +++ b/packages/instrumentor/instrument.test.ts @@ -22,6 +22,7 @@ import { transform, } from "./instrument"; import { codeCoverage } from "./plugins/codeCoverage"; +import { MemorySyncIdStrategy } from "./edgeIdStrategy"; describe("shouldInstrument check", () => { it("should consider includes and excludes", () => { @@ -74,7 +75,9 @@ describe("transform", () => { try { // Use the codeCoverage plugin to add additional lines, so that the // resulting error stack does not match the original code anymore. - const result = transform(sourceFileName, content, [codeCoverage()]); + const result = transform(sourceFileName, content, [ + codeCoverage(new MemorySyncIdStrategy()), + ]); const fn = eval(result?.code || ""); fn(); fail("Error expected but not thrown."); diff --git a/packages/instrumentor/instrument.ts b/packages/instrumentor/instrument.ts index ec5026ad..f54a1c9e 100644 --- a/packages/instrumentor/instrument.ts +++ b/packages/instrumentor/instrument.ts @@ -27,6 +27,7 @@ import { codeCoverage } from "./plugins/codeCoverage"; import { compareHooks } from "./plugins/compareHooks"; import { functionHooks } from "./plugins/functionHooks"; import { hookManager } from "@jazzer.js/hooking"; +import { EdgeIdStrategy, MemorySyncIdStrategy } from "./edgeIdStrategy"; interface SourceMaps { [file: string]: RawSourceMap; @@ -65,12 +66,21 @@ export function registerInstrumentor(includes: string[], excludes: string[]) { unloadInternalModules(); } + const idStrategy: EdgeIdStrategy = new MemorySyncIdStrategy(); + const shouldInstrument = shouldInstrumentFn(includes, excludes); const shouldHook = hookManager.hasFunctionsToHook.bind(hookManager); hookRequire( () => true, - (code: string, options: TransformerOptions): string => - instrument(code, options.filename, shouldInstrument, shouldHook) + (code: string, options: TransformerOptions): string => { + return instrument( + code, + options.filename, + shouldInstrument, + shouldHook, + idStrategy + ); + } ); } @@ -114,16 +124,29 @@ function instrument( code: string, filename: string, shouldInstrument: FilePredicate, - shouldHook: FilePredicate + shouldHook: FilePredicate, + idStrategy: EdgeIdStrategy ) { const transformations: PluginItem[] = []; - if (shouldInstrument(filename)) { - transformations.push(codeCoverage, compareHooks); + const shouldInstrumentFile = shouldInstrument(filename); + if (shouldInstrumentFile) { + transformations.push(codeCoverage(idStrategy), compareHooks); } if (shouldHook(filename)) { transformations.push(functionHooks(filename)); } - return transform(filename, code, transformations)?.code || code; + if (shouldInstrumentFile) { + idStrategy.startForSourceFile(filename); + } + + const transformedCode = + transform(filename, code, transformations)?.code || code; + + if (shouldInstrumentFile) { + idStrategy.commitIdCount(filename); + } + + return transformedCode; } export function transform( diff --git a/packages/instrumentor/plugins/codeCoverage.test.ts b/packages/instrumentor/plugins/codeCoverage.test.ts index dd5fed0b..39c05594 100644 --- a/packages/instrumentor/plugins/codeCoverage.test.ts +++ b/packages/instrumentor/plugins/codeCoverage.test.ts @@ -16,13 +16,23 @@ import { codeCoverage } from "./codeCoverage"; import { instrumentWith } from "./testhelpers"; +import { MemorySyncIdStrategy } from "../edgeIdStrategy"; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const native = require("@jazzer.js/fuzzer").fuzzer; -jest.mock("@jazzer.js/fuzzer"); -native.nextCounter.mockReturnValue(0); +jest.mock("../edgeIdStrategy", () => { + return { + MemorySyncIdStrategy: jest.fn().mockImplementation(() => { + return { + nextEdgeId: () => { + return 0; + }, + }; + }), + }; +}); -const expectInstrumentation = instrumentWith(codeCoverage); +const expectInstrumentation = instrumentWith( + codeCoverage(new MemorySyncIdStrategy()) +); describe("code coverage instrumentation", () => { describe("IfStatement", () => { diff --git a/packages/instrumentor/plugins/codeCoverage.ts b/packages/instrumentor/plugins/codeCoverage.ts index 27af56b7..1cda9d7b 100644 --- a/packages/instrumentor/plugins/codeCoverage.ts +++ b/packages/instrumentor/plugins/codeCoverage.ts @@ -30,77 +30,88 @@ import { isLogicalExpression, } from "@babel/types"; import { NodePath, PluginTarget, types } from "@babel/core"; -import { fuzzer } from "@jazzer.js/fuzzer"; +import { EdgeIdStrategy } from "../edgeIdStrategy"; -export function codeCoverage(): PluginTarget { - return { - visitor: { - // eslint-disable-next-line @typescript-eslint/ban-types - Function(path: NodePath) { - if (isBlockStatement(path.node.body)) { - const bodyStmt = path.node.body as BlockStatement; - if (bodyStmt) { - bodyStmt.body.unshift(makeCounterIncStmt()); +export function codeCoverage(idStrategy: EdgeIdStrategy): () => PluginTarget { + return () => { + return { + visitor: { + // eslint-disable-next-line @typescript-eslint/ban-types + Function(path: NodePath) { + if (isBlockStatement(path.node.body)) { + const bodyStmt = path.node.body as BlockStatement; + if (bodyStmt) { + bodyStmt.body.unshift(makeCounterIncStmt(idStrategy)); + } } - } - }, - IfStatement(path: NodePath) { - path.node.consequent = addCounterToStmt(path.node.consequent); - if (path.node.alternate) { - path.node.alternate = addCounterToStmt(path.node.alternate); - } - path.insertAfter(makeCounterIncStmt()); - }, - SwitchStatement(path: NodePath) { - path.node.cases.forEach((caseStmt) => - caseStmt.consequent.unshift(makeCounterIncStmt()) - ); - path.insertAfter(makeCounterIncStmt()); - }, - Loop(path: NodePath) { - path.node.body = addCounterToStmt(path.node.body); - path.insertAfter(makeCounterIncStmt()); - }, - TryStatement(path: NodePath) { - const catchStmt = path.node.handler; - if (catchStmt) { - catchStmt.body.body.unshift(makeCounterIncStmt()); - } - path.insertAfter(makeCounterIncStmt()); - }, - LogicalExpression(path: NodePath) { - if (!isLogicalExpression(path.node.left)) { - path.node.left = types.sequenceExpression([ - makeCounterIncExpr(), - path.node.left, + }, + IfStatement(path: NodePath) { + path.node.consequent = addCounterToStmt( + path.node.consequent, + idStrategy + ); + if (path.node.alternate) { + path.node.alternate = addCounterToStmt( + path.node.alternate, + idStrategy + ); + } + path.insertAfter(makeCounterIncStmt(idStrategy)); + }, + SwitchStatement(path: NodePath) { + path.node.cases.forEach((caseStmt) => + caseStmt.consequent.unshift(makeCounterIncStmt(idStrategy)) + ); + path.insertAfter(makeCounterIncStmt(idStrategy)); + }, + Loop(path: NodePath) { + path.node.body = addCounterToStmt(path.node.body, idStrategy); + path.insertAfter(makeCounterIncStmt(idStrategy)); + }, + TryStatement(path: NodePath) { + const catchStmt = path.node.handler; + if (catchStmt) { + catchStmt.body.body.unshift(makeCounterIncStmt(idStrategy)); + } + path.insertAfter(makeCounterIncStmt(idStrategy)); + }, + LogicalExpression(path: NodePath) { + if (!isLogicalExpression(path.node.left)) { + path.node.left = types.sequenceExpression([ + makeCounterIncExpr(idStrategy), + path.node.left, + ]); + } + if (!isLogicalExpression(path.node.right)) { + path.node.right = types.sequenceExpression([ + makeCounterIncExpr(idStrategy), + path.node.right, + ]); + } + }, + ConditionalExpression(path: NodePath) { + path.node.consequent = types.sequenceExpression([ + makeCounterIncExpr(idStrategy), + path.node.consequent, ]); - } - if (!isLogicalExpression(path.node.right)) { - path.node.right = types.sequenceExpression([ - makeCounterIncExpr(), - path.node.right, + path.node.alternate = types.sequenceExpression([ + makeCounterIncExpr(idStrategy), + path.node.alternate, ]); - } - }, - ConditionalExpression(path: NodePath) { - path.node.consequent = types.sequenceExpression([ - makeCounterIncExpr(), - path.node.consequent, - ]); - path.node.alternate = types.sequenceExpression([ - makeCounterIncExpr(), - path.node.alternate, - ]); - if (isBlockStatement(path.parent)) { - path.insertAfter(makeCounterIncStmt()); - } + if (isBlockStatement(path.parent)) { + path.insertAfter(makeCounterIncStmt(idStrategy)); + } + }, }, - }, + }; }; } -function addCounterToStmt(stmt: Statement): BlockStatement { - const counterStmt = makeCounterIncStmt(); +function addCounterToStmt( + stmt: Statement, + strategy: EdgeIdStrategy +): BlockStatement { + const counterStmt = makeCounterIncStmt(strategy); if (isBlockStatement(stmt)) { const br = stmt as BlockStatement; br.body.unshift(counterStmt); @@ -110,12 +121,12 @@ function addCounterToStmt(stmt: Statement): BlockStatement { } } -function makeCounterIncStmt(): ExpressionStatement { - return types.expressionStatement(makeCounterIncExpr()); +function makeCounterIncStmt(strategy: EdgeIdStrategy): ExpressionStatement { + return types.expressionStatement(makeCounterIncExpr(strategy)); } -function makeCounterIncExpr(): Expression { +function makeCounterIncExpr(strategy: EdgeIdStrategy): Expression { return types.callExpression(types.identifier("Fuzzer.incrementCounter"), [ - types.numericLiteral(fuzzer.nextCounter()), + types.numericLiteral(strategy.nextEdgeId()), ]); } From b1989cfccb15bdeaf538a6c9cd78c95f7eb7a110 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Wed, 18 Jan 2023 17:10:38 +0100 Subject: [PATCH 07/22] instrumentor: add file sync edge ID strategy for multi-process modes --- package-lock.json | 67 ++++++++++ packages/instrumentor/edgeIdStrategy.ts | 155 ++++++++++++++++++++++++ packages/instrumentor/package.json | 2 + 3 files changed, 224 insertions(+) diff --git a/package-lock.json b/package-lock.json index e7d31333..ab5248ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1254,6 +1254,21 @@ "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz", "integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==" }, + "node_modules/@types/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@types/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-kd4LMvcnpYkspDcp7rmXKedn8iJSCoa331zRRamUp5oanKt/CefbEGPQP7G89enz7sKD4bvsr8mHSsC8j5WOvA==", + "dev": true, + "dependencies": { + "@types/retry": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true + }, "node_modules/@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", @@ -6900,6 +6915,16 @@ "node": ">= 6" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -7123,6 +7148,14 @@ "node": ">=8" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -8458,12 +8491,14 @@ "@jazzer.js/fuzzer": "*", "@jazzer.js/hooking": "*", "istanbul-lib-hook": "^3.0.0", + "proper-lockfile": "^4.1.2", "source-map-support": "^0.5.21" }, "devDependencies": { "@types/babel__core": "^7.20.0", "@types/istanbul-lib-hook": "^2.0.1", "@types/node": "^18.11.18", + "@types/proper-lockfile": "^4.1.2", "@types/source-map-support": "^0.5.6" }, "engines": { @@ -9089,8 +9124,10 @@ "@types/babel__core": "^7.20.0", "@types/istanbul-lib-hook": "^2.0.1", "@types/node": "^18.11.18", + "@types/proper-lockfile": "*", "@types/source-map-support": "^0.5.6", "istanbul-lib-hook": "^3.0.0", + "proper-lockfile": "^4.1.2", "source-map-support": "^0.5.21" }, "dependencies": { @@ -9515,6 +9552,21 @@ "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz", "integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==" }, + "@types/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@types/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-kd4LMvcnpYkspDcp7rmXKedn8iJSCoa331zRRamUp5oanKt/CefbEGPQP7G89enz7sKD4bvsr8mHSsC8j5WOvA==", + "dev": true, + "requires": { + "@types/retry": "*" + } + }, + "@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true + }, "@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", @@ -13859,6 +13911,16 @@ "sisteransi": "^1.0.5" } }, + "proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "requires": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, "psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -14022,6 +14084,11 @@ "signal-exit": "^3.0.2" } }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==" + }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", diff --git a/packages/instrumentor/edgeIdStrategy.ts b/packages/instrumentor/edgeIdStrategy.ts index 05c93b17..80f7798e 100644 --- a/packages/instrumentor/edgeIdStrategy.ts +++ b/packages/instrumentor/edgeIdStrategy.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import * as lock from "proper-lockfile"; +import * as fs from "fs"; +import process from "process"; + import { fuzzer } from "@jazzer.js/fuzzer"; export abstract class EdgeIdStrategy { @@ -43,3 +47,154 @@ export class MemorySyncIdStrategy extends EdgeIdStrategy { // nothing to do here } } + +interface EdgeIdInfo { + filename: string; + firstId: number; + idCount: number; +} + +const newLine = process.platform === "win32" ? "\r\n" : "\n"; + +/** + * A strategy for edge ID generation that synchronizes the IDs assigned to a source file + * with other processes via the specified `idSyncFile`. The edge information stored as a + * line of the format: ,, + * + * This class takes care of synchronizing the access to the file between + * multiple processes accessing it during instrumentation. + */ +export class FileSyncIdStrategy extends EdgeIdStrategy { + private cachedIdCount: number | undefined; + private firstEdgeId: number | undefined; + + constructor(private idSyncFile: string) { + super(0); + } + + startForSourceFile(filename: string): void { + console.log(`startForSourceFile(${filename})`); + // We resort to busy waiting since the `Transformer` required by istanbul's `hookRequire` + // must be a synchronous function returning the transformed code. + for (;;) { + const isLocked = lock.checkSync(this.idSyncFile); + if (isLocked) { + // If the ID sync file is already locked, wait for a random period of time + // between 0 and 100 milliseconds. Waiting for different periods reduces + // the chance of all processes wanting to acquire the lock at the same time. + this.wait(this.randomIntFromInterval(0, 100)); + continue; + } + try { + // Acquire the lock for the ID sync file and look for the initial edge ID and + // corresponding number of inserted counters. + lock.lockSync(this.idSyncFile); + const idInfo = fs + .readFileSync(this.idSyncFile, "utf8") + .toString() + .split(newLine) + .filter((line) => line.length !== 0) + .map((line): EdgeIdInfo => { + const parts = line.split(","); + if (parts.length !== 3) { + lock.unlockSync(this.idSyncFile); + throw Error( + `Expected ID file line to be of the form ,,", got "${line}"` + ); + } + return { + filename: parts[0], + firstId: parseInt(parts[1], 10), + idCount: parseInt(parts[2], 10), + }; + }); + const idInfoForFile = idInfo.filter( + (info) => info.filename === filename + ); + + switch (idInfoForFile.length) { + case 0: + // We are the first to encounter this source file and thus need to hold the lock + // until the file has been instrumented and we know the required number of edge IDs. + // + // Compute the next free ID as the maximum over the sums of first ID and ID count, starting at 0 if + // this is the first ID to be assigned. Since this is the only way new lines are added to + // the file, the maximum is always attained by the last line. + this.firstEdgeId = + idInfo.length !== 0 + ? idInfo[idInfo.length - 1].firstId + + idInfo[idInfo.length - 1].idCount + : 0; + break; + case 1: + // This source file has already been instrumented elsewhere, so we just return the first ID and + // ID count reported from there and release the lock right away. The caller is still expected + // to call commitIdCount. + this.firstEdgeId = idInfoForFile[0].firstId; + this.cachedIdCount = idInfoForFile[0].idCount; + lock.unlockSync(this.idSyncFile); + break; + default: + lock.unlockSync(this.idSyncFile); + throw Error(`Multiple entries for ${filename} in ID sync file`); + } + break; + } catch (e) { + // Retry to wait for the lock to be release it is acquired by another process + // in the time window between last successful check and trying to acquire it. + if (this.isLockAlreadyHeldError(e)) { + continue; + } + + // Stop waiting for the lock if we encounter other errors. Also, rethrow the error. + throw e; + } + } + + this._nextEdgeId = this.firstEdgeId; + } + commitIdCount(filename: string): void { + if (this.firstEdgeId === undefined) { + throw Error("commitIdCount() is called before startForSourceFile()"); + } + + const idCount = this._nextEdgeId - this.firstEdgeId; + if (this.cachedIdCount !== undefined) { + // We released the lock already in startForSourceFile since the file had already been instrumented + // elsewhere. As we know the expected number of IDs for the current class in this case, check for + // deviations. + if (this.cachedIdCount !== idCount) { + throw Error( + `${filename} has ${idCount} edges, but ${this.cachedIdCount} edges reserved in ID sync file` + ); + } + } else { + // We are the first to instrument this file and should record the number of IDs in the sync file. + fs.appendFileSync( + this.idSyncFile, + `${filename},${this.firstEdgeId},${idCount}${newLine}` + ); + this.firstEdgeId = undefined; + this.cachedIdCount = undefined; + lock.unlockSync(this.idSyncFile); + } + } + + private wait(timeout: number) { + // This is a workaround to synchronously sleep for a `timout` milliseconds. + // The static Atomics.wait() method verifies that a given position in an Int32Array + // still contains a given value and if so sleeps, awaiting a wakeup or a timeout. + // Here, we deliberately cause a timeout. + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, timeout); + } + + private randomIntFromInterval(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1) + min); + } + + private isLockAlreadyHeldError(e: unknown): boolean { + return ( + e != null && typeof e === "object" && "code" in e && e.code === "ELOCKED" + ); + } +} diff --git a/packages/instrumentor/package.json b/packages/instrumentor/package.json index 15593a40..8de2cefa 100644 --- a/packages/instrumentor/package.json +++ b/packages/instrumentor/package.json @@ -21,12 +21,14 @@ "@jazzer.js/fuzzer": "*", "@jazzer.js/hooking": "*", "istanbul-lib-hook": "^3.0.0", + "proper-lockfile": "^4.1.2", "source-map-support": "^0.5.21" }, "devDependencies": { "@types/babel__core": "^7.20.0", "@types/istanbul-lib-hook": "^2.0.1", "@types/node": "^18.11.18", + "@types/proper-lockfile": "^4.1.2", "@types/source-map-support": "^0.5.6" }, "engines": { From 79321a20dd3dd867009abcf18824273671054a06 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Wed, 18 Jan 2023 17:12:07 +0100 Subject: [PATCH 08/22] core: use file sync strategy for libFuzzer's multi-process modes --- packages/core/cli.ts | 10 ++++++++++ packages/core/core.ts | 17 ++++++++++++++++- packages/instrumentor/instrument.ts | 17 ++++++++++++++--- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/packages/core/cli.ts b/packages/core/cli.ts index d3ffbabc..7b14550f 100644 --- a/packages/core/cli.ts +++ b/packages/core/cli.ts @@ -74,6 +74,15 @@ yargs(process.argv.slice(2)) }) .hide("fuzzFunction") + .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:", + }) + .option("sync", { describe: "Run the fuzz target synchronously.", type: "boolean", @@ -167,6 +176,7 @@ yargs(process.argv.slice(2)) fuzzerOptions: args.corpus.concat(args._), customHooks: args.custom_hooks.map(ensureFilepath), expectedErrors: args.expected_errors, + idSyncFile: args.id_sync_file, }); } ) diff --git a/packages/core/core.ts b/packages/core/core.ts index 78aa2c99..79ecd87a 100644 --- a/packages/core/core.ts +++ b/packages/core/core.ts @@ -44,6 +44,7 @@ export interface Options { customHooks: string[]; expectedErrors: string[]; timeout?: number; + idSyncFile?: string; } interface FuzzModule { @@ -60,7 +61,11 @@ export async function initFuzzing(options: Options) { registerGlobals(); await Promise.all(options.customHooks.map(importModule)); if (!options.dryRun) { - registerInstrumentor(options.includes, options.excludes); + registerInstrumentor( + options.includes, + options.excludes, + options.idSyncFile + ); } } @@ -121,6 +126,16 @@ function createWrapperScript(fuzzerOptions: string[]) { (arg) => arg !== "--" && fuzzerOptions.indexOf(arg) === -1 ); + if (jazzerArgs.indexOf("--id_sync_file") === -1) { + const idSyncFile = tmp.fileSync({ + mode: 0o600, + prefix: "jazzer.js", + postfix: "idSync", + }); + jazzerArgs.push("--id_sync_file", idSyncFile.name); + fs.closeSync(idSyncFile.fd); + } + const isWindows = process.platform === "win32"; const scriptContent = `${isWindows ? "@echo off" : "#!/usr/bin/env sh"} diff --git a/packages/instrumentor/instrument.ts b/packages/instrumentor/instrument.ts index f54a1c9e..ca9cd015 100644 --- a/packages/instrumentor/instrument.ts +++ b/packages/instrumentor/instrument.ts @@ -27,7 +27,11 @@ import { codeCoverage } from "./plugins/codeCoverage"; import { compareHooks } from "./plugins/compareHooks"; import { functionHooks } from "./plugins/functionHooks"; import { hookManager } from "@jazzer.js/hooking"; -import { EdgeIdStrategy, MemorySyncIdStrategy } from "./edgeIdStrategy"; +import { + EdgeIdStrategy, + FileSyncIdStrategy, + MemorySyncIdStrategy, +} from "./edgeIdStrategy"; interface SourceMaps { [file: string]: RawSourceMap; @@ -60,13 +64,20 @@ export function installSourceMapSupport(): () => void { export type FilePredicate = (filepath: string) => boolean; -export function registerInstrumentor(includes: string[], excludes: string[]) { +export function registerInstrumentor( + includes: string[], + excludes: string[], + idSyncFile: string | undefined +) { installSourceMapSupport(); if (includes.includes("jazzer.js")) { unloadInternalModules(); } - const idStrategy: EdgeIdStrategy = new MemorySyncIdStrategy(); + const idStrategy: EdgeIdStrategy = + idSyncFile !== undefined + ? new FileSyncIdStrategy(idSyncFile) + : new MemorySyncIdStrategy(); const shouldInstrument = shouldInstrumentFn(includes, excludes); const shouldHook = hookManager.hasFunctionsToHook.bind(hookManager); From 4a6375babf11c8ce68a4859e6f6dfc6948a92f21 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Sat, 21 Jan 2023 07:07:50 +0100 Subject: [PATCH 09/22] fuzzer: refactor the fuzzer object and type This removes duplicate type definitions and makes it clear when calling functions from the native addon from the other packages. --- packages/core/core.ts | 12 ++++++------ packages/core/jazzer.ts | 8 ++++---- packages/fuzzer/addon.ts | 2 +- packages/fuzzer/fuzzer.ts | 14 +++----------- packages/instrumentor/edgeIdStrategy.ts | 13 ++++++------- 5 files changed, 20 insertions(+), 29 deletions(-) diff --git a/packages/core/core.ts b/packages/core/core.ts index 79ecd87a..3cbf35a0 100644 --- a/packages/core/core.ts +++ b/packages/core/core.ts @@ -94,8 +94,8 @@ export async function startFuzzingNoInit( ) { const fuzzerOptions = buildFuzzerOptions(options); const fuzzerFn = options.sync - ? Fuzzer.startFuzzing - : Fuzzer.startFuzzingAsync; + ? Fuzzer.nativeAddon.startFuzzing + : Fuzzer.nativeAddon.startFuzzingAsync; // Wrap the potentially sync fuzzer call, so that resolve and exception // handlers are always executed. return Promise.resolve().then(() => fuzzerFn(fuzzFn, fuzzerOptions)); @@ -165,7 +165,7 @@ function stopFuzzing(err: unknown, expectedErrors: string[]) { console.error( `ERROR: Received no error, but expected one of [${expectedErrors}].` ); - Fuzzer.stopFuzzingAsync(ERROR_UNEXPECTED_CODE); + Fuzzer.nativeAddon.stopFuzzingAsync(ERROR_UNEXPECTED_CODE); } return; } @@ -175,13 +175,13 @@ function stopFuzzing(err: unknown, expectedErrors: string[]) { const name = errorName(err); if (expectedErrors.includes(name)) { console.error(`INFO: Received expected error "${name}".`); - Fuzzer.stopFuzzingAsync(ERROR_EXPECTED_CODE); + Fuzzer.nativeAddon.stopFuzzingAsync(ERROR_EXPECTED_CODE); } else { printError(err); console.error( `ERROR: Received error "${name}" is not in expected errors [${expectedErrors}].` ); - Fuzzer.stopFuzzingAsync(ERROR_UNEXPECTED_CODE); + Fuzzer.nativeAddon.stopFuzzingAsync(ERROR_UNEXPECTED_CODE); } return; } @@ -189,7 +189,7 @@ function stopFuzzing(err: unknown, expectedErrors: string[]) { // Error found, but no specific one expected. This case is used for normal // fuzzing runs, so no dedicated exit code is given to the stop fuzzing function. printError(err); - Fuzzer.stopFuzzingAsync(); + Fuzzer.nativeAddon.stopFuzzingAsync(); } function errorName(error: unknown): string { diff --git a/packages/core/jazzer.ts b/packages/core/jazzer.ts index 68d79f6d..2d56b490 100644 --- a/packages/core/jazzer.ts +++ b/packages/core/jazzer.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { addon } from "@jazzer.js/fuzzer"; +import { fuzzer } from "@jazzer.js/fuzzer"; /** * Instructs the fuzzer to guide its mutations towards making `current` equal to `target` @@ -29,7 +29,7 @@ import { addon } from "@jazzer.js/fuzzer"; * @param id a (probabilistically) unique identifier for this particular compare hint */ function guideTowardsEquality(current: string, target: string, id: number) { - addon.traceUnequalStrings(id, current, target); + fuzzer.nativeAddon.traceUnequalStrings(id, current, target); } /** @@ -45,7 +45,7 @@ function guideTowardsEquality(current: string, target: string, id: number) { * @param id a (probabilistically) unique identifier for this particular compare hint */ function guideTowardsContainment(needle: string, haystack: string, id: number) { - addon.traceStringContainment(id, needle, haystack); + fuzzer.nativeAddon.traceStringContainment(id, needle, haystack); } /** @@ -63,7 +63,7 @@ function guideTowardsContainment(needle: string, haystack: string, id: number) { * @param id a (probabilistically) unique identifier for this particular state hint */ function exploreState(state: number, id: number) { - addon.tracePcIndir(id, state); + fuzzer.nativeAddon.tracePcIndir(id, state); } export interface Jazzer { diff --git a/packages/fuzzer/addon.ts b/packages/fuzzer/addon.ts index aac4c26e..60867571 100644 --- a/packages/fuzzer/addon.ts +++ b/packages/fuzzer/addon.ts @@ -59,7 +59,7 @@ export type NativeAddon = { printVersion: () => void; startFuzzing: StartFuzzingSyncFn; startFuzzingAsync: StartFuzzingAsyncFn; - stopFuzzingAsync: () => void; + stopFuzzingAsync: (status?: number) => void; }; export const addon: NativeAddon = bind("jazzerjs"); diff --git a/packages/fuzzer/fuzzer.ts b/packages/fuzzer/fuzzer.ts index df42f8d9..e334a251 100644 --- a/packages/fuzzer/fuzzer.ts +++ b/packages/fuzzer/fuzzer.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { addon, StartFuzzingAsyncFn, StartFuzzingSyncFn } from "./addon"; +import { addon, NativeAddon } from "./addon"; import { incrementCounter, initializeCounters, @@ -31,13 +31,8 @@ export type { FuzzTargetCallback, } from "./addon"; -export { addon } from "./addon"; - export interface Fuzzer { - printVersion: () => void; - startFuzzing: StartFuzzingSyncFn; - startFuzzingAsync: StartFuzzingAsyncFn; - stopFuzzingAsync: (status?: number) => void; + nativeAddon: NativeAddon; incrementCounter: typeof incrementCounter; readCounter: typeof readCounter; traceStrCmp: typeof traceStrCmp; @@ -47,10 +42,7 @@ export interface Fuzzer { } export const fuzzer: Fuzzer = { - printVersion: addon.printVersion, - startFuzzing: addon.startFuzzing, - startFuzzingAsync: addon.startFuzzingAsync, - stopFuzzingAsync: addon.stopFuzzingAsync, + nativeAddon: addon, incrementCounter, readCounter, traceStrCmp, diff --git a/packages/instrumentor/edgeIdStrategy.ts b/packages/instrumentor/edgeIdStrategy.ts index 80f7798e..c539ce88 100644 --- a/packages/instrumentor/edgeIdStrategy.ts +++ b/packages/instrumentor/edgeIdStrategy.ts @@ -73,7 +73,6 @@ export class FileSyncIdStrategy extends EdgeIdStrategy { } startForSourceFile(filename: string): void { - console.log(`startForSourceFile(${filename})`); // We resort to busy waiting since the `Transformer` required by istanbul's `hookRequire` // must be a synchronous function returning the transformed code. for (;;) { @@ -158,21 +157,21 @@ export class FileSyncIdStrategy extends EdgeIdStrategy { throw Error("commitIdCount() is called before startForSourceFile()"); } - const idCount = this._nextEdgeId - this.firstEdgeId; + const usedIdsCount = this._nextEdgeId - this.firstEdgeId; if (this.cachedIdCount !== undefined) { // We released the lock already in startForSourceFile since the file had already been instrumented - // elsewhere. As we know the expected number of IDs for the current class in this case, check for - // deviations. - if (this.cachedIdCount !== idCount) { + // elsewhere. As we know the expected number of IDs for the current source file in this case, check + // for deviations. + if (this.cachedIdCount !== usedIdsCount) { throw Error( - `${filename} has ${idCount} edges, but ${this.cachedIdCount} edges reserved in ID sync file` + `${filename} has ${usedIdsCount} edges, but ${this.cachedIdCount} edges reserved in ID sync file` ); } } else { // We are the first to instrument this file and should record the number of IDs in the sync file. fs.appendFileSync( this.idSyncFile, - `${filename},${this.firstEdgeId},${idCount}${newLine}` + `${filename},${this.firstEdgeId},${usedIdsCount}${newLine}` ); this.firstEdgeId = undefined; this.cachedIdCount = undefined; From 817d2f47fb6aaf44cd4be9b845fae6c0a574519a Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Sun, 22 Jan 2023 07:00:22 +0100 Subject: [PATCH 10/22] instrumentor: use os.EOL for adding a new line into the ID sync file --- packages/instrumentor/edgeIdStrategy.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/instrumentor/edgeIdStrategy.ts b/packages/instrumentor/edgeIdStrategy.ts index c539ce88..6dc48cc0 100644 --- a/packages/instrumentor/edgeIdStrategy.ts +++ b/packages/instrumentor/edgeIdStrategy.ts @@ -16,6 +16,7 @@ import * as lock from "proper-lockfile"; import * as fs from "fs"; +import * as os from "os"; import process from "process"; import { fuzzer } from "@jazzer.js/fuzzer"; @@ -54,8 +55,6 @@ interface EdgeIdInfo { idCount: number; } -const newLine = process.platform === "win32" ? "\r\n" : "\n"; - /** * A strategy for edge ID generation that synchronizes the IDs assigned to a source file * with other processes via the specified `idSyncFile`. The edge information stored as a @@ -91,7 +90,7 @@ export class FileSyncIdStrategy extends EdgeIdStrategy { const idInfo = fs .readFileSync(this.idSyncFile, "utf8") .toString() - .split(newLine) + .split(os.EOL) .filter((line) => line.length !== 0) .map((line): EdgeIdInfo => { const parts = line.split(","); @@ -171,7 +170,7 @@ export class FileSyncIdStrategy extends EdgeIdStrategy { // We are the first to instrument this file and should record the number of IDs in the sync file. fs.appendFileSync( this.idSyncFile, - `${filename},${this.firstEdgeId},${usedIdsCount}${newLine}` + `${filename},${this.firstEdgeId},${usedIdsCount}${os.EOL}` ); this.firstEdgeId = undefined; this.cachedIdCount = undefined; From 4c0179150438e7a3953c978d94eba2e8150fe520 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Sat, 21 Jan 2023 07:43:04 +0100 Subject: [PATCH 11/22] instrumentor: move counter-generating functions into codeCoverage() This way, these functions will have access to `EdgeIdStrategy` object --- packages/instrumentor/plugins/codeCoverage.ts | 81 +++++++++---------- 1 file changed, 36 insertions(+), 45 deletions(-) diff --git a/packages/instrumentor/plugins/codeCoverage.ts b/packages/instrumentor/plugins/codeCoverage.ts index 1cda9d7b..19f30152 100644 --- a/packages/instrumentor/plugins/codeCoverage.ts +++ b/packages/instrumentor/plugins/codeCoverage.ts @@ -33,6 +33,27 @@ import { NodePath, PluginTarget, types } from "@babel/core"; import { EdgeIdStrategy } from "../edgeIdStrategy"; export function codeCoverage(idStrategy: EdgeIdStrategy): () => PluginTarget { + function addCounterToStmt(stmt: Statement): BlockStatement { + const counterStmt = makeCounterIncStmt(); + if (isBlockStatement(stmt)) { + const br = stmt as BlockStatement; + br.body.unshift(counterStmt); + return br; + } else { + return types.blockStatement([counterStmt, stmt]); + } + } + + function makeCounterIncStmt(): ExpressionStatement { + return types.expressionStatement(makeCounterIncExpr()); + } + + function makeCounterIncExpr(): Expression { + return types.callExpression(types.identifier("Fuzzer.incrementCounter"), [ + types.numericLiteral(idStrategy.nextEdgeId()), + ]); + } + return () => { return { visitor: { @@ -41,92 +62,62 @@ export function codeCoverage(idStrategy: EdgeIdStrategy): () => PluginTarget { if (isBlockStatement(path.node.body)) { const bodyStmt = path.node.body as BlockStatement; if (bodyStmt) { - bodyStmt.body.unshift(makeCounterIncStmt(idStrategy)); + bodyStmt.body.unshift(makeCounterIncStmt()); } } }, IfStatement(path: NodePath) { - path.node.consequent = addCounterToStmt( - path.node.consequent, - idStrategy - ); + path.node.consequent = addCounterToStmt(path.node.consequent); if (path.node.alternate) { - path.node.alternate = addCounterToStmt( - path.node.alternate, - idStrategy - ); + path.node.alternate = addCounterToStmt(path.node.alternate); } - path.insertAfter(makeCounterIncStmt(idStrategy)); + path.insertAfter(makeCounterIncStmt()); }, SwitchStatement(path: NodePath) { path.node.cases.forEach((caseStmt) => - caseStmt.consequent.unshift(makeCounterIncStmt(idStrategy)) + caseStmt.consequent.unshift(makeCounterIncStmt()) ); - path.insertAfter(makeCounterIncStmt(idStrategy)); + path.insertAfter(makeCounterIncStmt()); }, Loop(path: NodePath) { - path.node.body = addCounterToStmt(path.node.body, idStrategy); - path.insertAfter(makeCounterIncStmt(idStrategy)); + path.node.body = addCounterToStmt(path.node.body); + path.insertAfter(makeCounterIncStmt()); }, TryStatement(path: NodePath) { const catchStmt = path.node.handler; if (catchStmt) { - catchStmt.body.body.unshift(makeCounterIncStmt(idStrategy)); + catchStmt.body.body.unshift(makeCounterIncStmt()); } - path.insertAfter(makeCounterIncStmt(idStrategy)); + path.insertAfter(makeCounterIncStmt()); }, LogicalExpression(path: NodePath) { if (!isLogicalExpression(path.node.left)) { path.node.left = types.sequenceExpression([ - makeCounterIncExpr(idStrategy), + makeCounterIncExpr(), path.node.left, ]); } if (!isLogicalExpression(path.node.right)) { path.node.right = types.sequenceExpression([ - makeCounterIncExpr(idStrategy), + makeCounterIncExpr(), path.node.right, ]); } }, ConditionalExpression(path: NodePath) { path.node.consequent = types.sequenceExpression([ - makeCounterIncExpr(idStrategy), + makeCounterIncExpr(), path.node.consequent, ]); path.node.alternate = types.sequenceExpression([ - makeCounterIncExpr(idStrategy), + makeCounterIncExpr(), path.node.alternate, ]); if (isBlockStatement(path.parent)) { - path.insertAfter(makeCounterIncStmt(idStrategy)); + path.insertAfter(makeCounterIncStmt()); } }, }, }; }; } - -function addCounterToStmt( - stmt: Statement, - strategy: EdgeIdStrategy -): BlockStatement { - const counterStmt = makeCounterIncStmt(strategy); - if (isBlockStatement(stmt)) { - const br = stmt as BlockStatement; - br.body.unshift(counterStmt); - return br; - } else { - return types.blockStatement([counterStmt, stmt]); - } -} - -function makeCounterIncStmt(strategy: EdgeIdStrategy): ExpressionStatement { - return types.expressionStatement(makeCounterIncExpr(strategy)); -} - -function makeCounterIncExpr(strategy: EdgeIdStrategy): Expression { - return types.callExpression(types.identifier("Fuzzer.incrementCounter"), [ - types.numericLiteral(strategy.nextEdgeId()), - ]); -} From 9ba1ca8193558c58a70806d5e15bf45e51db9b9f Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Sun, 22 Jan 2023 06:46:21 +0100 Subject: [PATCH 12/22] instrumentor: use returned release function to release lock on id file --- packages/instrumentor/edgeIdStrategy.ts | 26 ++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/instrumentor/edgeIdStrategy.ts b/packages/instrumentor/edgeIdStrategy.ts index 6dc48cc0..38e4eee5 100644 --- a/packages/instrumentor/edgeIdStrategy.ts +++ b/packages/instrumentor/edgeIdStrategy.ts @@ -64,8 +64,10 @@ interface EdgeIdInfo { * multiple processes accessing it during instrumentation. */ export class FileSyncIdStrategy extends EdgeIdStrategy { + private static fatalExitCode = 79; private cachedIdCount: number | undefined; private firstEdgeId: number | undefined; + private releaseLockOnSyncFile: (() => void) | undefined; constructor(private idSyncFile: string) { super(0); @@ -86,7 +88,7 @@ export class FileSyncIdStrategy extends EdgeIdStrategy { try { // Acquire the lock for the ID sync file and look for the initial edge ID and // corresponding number of inserted counters. - lock.lockSync(this.idSyncFile); + this.releaseLockOnSyncFile = lock.lockSync(this.idSyncFile); const idInfo = fs .readFileSync(this.idSyncFile, "utf8") .toString() @@ -130,11 +132,12 @@ export class FileSyncIdStrategy extends EdgeIdStrategy { // to call commitIdCount. this.firstEdgeId = idInfoForFile[0].firstId; this.cachedIdCount = idInfoForFile[0].idCount; - lock.unlockSync(this.idSyncFile); + this.releaseLockOnSyncFile(); break; default: - lock.unlockSync(this.idSyncFile); - throw Error(`Multiple entries for ${filename} in ID sync file`); + this.releaseLockOnSyncFile(); + console.error(`Multiple entries for ${filename} in ID sync file`); + process.exit(FileSyncIdStrategy.fatalExitCode); } break; } catch (e) { @@ -144,6 +147,11 @@ export class FileSyncIdStrategy extends EdgeIdStrategy { continue; } + // Before rethrowing the exception, release the lock if we have already acquired it. + if (this.releaseLockOnSyncFile !== undefined) { + this.releaseLockOnSyncFile(); + } + // Stop waiting for the lock if we encounter other errors. Also, rethrow the error. throw e; } @@ -167,14 +175,22 @@ export class FileSyncIdStrategy extends EdgeIdStrategy { ); } } else { + if (this.releaseLockOnSyncFile === undefined) { + console.error( + `Lock on ID sync file is not acquired by the first processing instrumenting: ${filename}` + ); + process.exit(FileSyncIdStrategy.fatalExitCode); + } + // We are the first to instrument this file and should record the number of IDs in the sync file. fs.appendFileSync( this.idSyncFile, `${filename},${this.firstEdgeId},${usedIdsCount}${os.EOL}` ); + this.releaseLockOnSyncFile(); + this.releaseLockOnSyncFile = undefined; this.firstEdgeId = undefined; this.cachedIdCount = undefined; - lock.unlockSync(this.idSyncFile); } } From 430c6cf8cc531d024093e8246ccf4403bb11fee7 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Sun, 22 Jan 2023 07:01:45 +0100 Subject: [PATCH 13/22] instrumentor: extract EdgeIdStrategy into an interface --- packages/instrumentor/edgeIdStrategy.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/instrumentor/edgeIdStrategy.ts b/packages/instrumentor/edgeIdStrategy.ts index 38e4eee5..c3d7f033 100644 --- a/packages/instrumentor/edgeIdStrategy.ts +++ b/packages/instrumentor/edgeIdStrategy.ts @@ -21,7 +21,13 @@ import process from "process"; import { fuzzer } from "@jazzer.js/fuzzer"; -export abstract class EdgeIdStrategy { +export interface EdgeIdStrategy { + nextEdgeId(): number; + startForSourceFile(filename: string): void; + commitIdCount(filename: string): void; +} + +export abstract class IncrementingEdgeIdStrategy implements EdgeIdStrategy { protected constructor(protected _nextEdgeId: number) {} nextEdgeId(): number { @@ -33,7 +39,7 @@ export abstract class EdgeIdStrategy { abstract commitIdCount(filename: string): void; } -export class MemorySyncIdStrategy extends EdgeIdStrategy { +export class MemorySyncIdStrategy extends IncrementingEdgeIdStrategy { constructor() { super(0); } @@ -63,7 +69,7 @@ interface EdgeIdInfo { * This class takes care of synchronizing the access to the file between * multiple processes accessing it during instrumentation. */ -export class FileSyncIdStrategy extends EdgeIdStrategy { +export class FileSyncIdStrategy extends IncrementingEdgeIdStrategy { private static fatalExitCode = 79; private cachedIdCount: number | undefined; private firstEdgeId: number | undefined; From 40e3c5546b158c74f129b55ca6b344838ec1b089 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Sun, 22 Jan 2023 10:00:43 +0100 Subject: [PATCH 14/22] instrumentor: extract coverage tracking functions into CoverageTracker --- packages/fuzzer/coverage.ts | 78 ++++++++++--------- packages/fuzzer/fuzzer.test.ts | 12 +-- packages/fuzzer/fuzzer.ts | 19 ++--- packages/instrumentor/edgeIdStrategy.ts | 2 +- packages/instrumentor/instrument.test.ts | 7 +- .../instrumentor/plugins/codeCoverage.test.ts | 34 ++++---- packages/instrumentor/plugins/codeCoverage.ts | 7 +- 7 files changed, 79 insertions(+), 80 deletions(-) diff --git a/packages/fuzzer/coverage.ts b/packages/fuzzer/coverage.ts index 03dac122..89b7df69 100644 --- a/packages/fuzzer/coverage.ts +++ b/packages/fuzzer/coverage.ts @@ -16,49 +16,53 @@ import { addon } from "./addon"; -const MAX_NUM_COUNTERS: number = 1 << 20; -const INITIAL_NUM_COUNTERS: number = 1 << 9; -let coverageMap: Buffer; -let currentNumCounters: number; +export class CoverageTracker { + private static readonly MAX_NUM_COUNTERS: number = 1 << 20; + private static readonly INITIAL_NUM_COUNTERS: number = 1 << 9; + private readonly coverageMap: Buffer; + private currentNumCounters: number; -export function initializeCounters() { - coverageMap = Buffer.alloc(MAX_NUM_COUNTERS, 0); - addon.registerCoverageMap(coverageMap); - addon.registerNewCounters(0, INITIAL_NUM_COUNTERS); - currentNumCounters = INITIAL_NUM_COUNTERS; -} + constructor() { + this.coverageMap = Buffer.alloc(CoverageTracker.MAX_NUM_COUNTERS, 0); + this.currentNumCounters = CoverageTracker.INITIAL_NUM_COUNTERS; + addon.registerCoverageMap(this.coverageMap); + addon.registerNewCounters(0, this.currentNumCounters); + } -export function enlargeCountersBufferIfNeeded(nextEdgeId: number) { - // Enlarge registered counters if needed - let newNumCounters = currentNumCounters; - while (nextEdgeId >= newNumCounters) { - newNumCounters = 2 * newNumCounters; - if (newNumCounters > MAX_NUM_COUNTERS) { - throw new Error( - `Maximum number (${MAX_NUM_COUNTERS}) of coverage counts exceeded.` + enlargeCountersBufferIfNeeded(nextEdgeId: number) { + // Enlarge registered counters if needed + let newNumCounters = this.currentNumCounters; + while (nextEdgeId >= newNumCounters) { + newNumCounters = 2 * newNumCounters; + if (newNumCounters > CoverageTracker.MAX_NUM_COUNTERS) { + throw new Error( + `Maximum number (${CoverageTracker.MAX_NUM_COUNTERS}) of coverage counts exceeded.` + ); + } + } + + // Register new counters if enlarged + if (newNumCounters > this.currentNumCounters) { + addon.registerNewCounters(this.currentNumCounters, newNumCounters); + this.currentNumCounters = newNumCounters; + console.log( + `INFO: New number of coverage counters ${this.currentNumCounters}` ); } } - // Register new counters if enlarged - if (newNumCounters > currentNumCounters) { - addon.registerNewCounters(currentNumCounters, newNumCounters); - currentNumCounters = newNumCounters; - console.log(`INFO: New number of coverage counters ${currentNumCounters}`); + /** + * Increments the coverage counter for a given ID. + * This function implements the NeverZero policy from AFL++. + * See https://aflplus.plus//papers/aflpp-woot2020.pdf + * @param edgeId the edge ID of the coverage counter to increment + */ + incrementCounter(edgeId: number) { + const counter = this.coverageMap.readUint8(edgeId); + this.coverageMap.writeUint8(counter == 255 ? 1 : counter + 1, edgeId); } -} -/** - * Increments the coverage counter for a given ID. - * This function implements the NeverZero policy from AFL++. - * See https://aflplus.plus//papers/aflpp-woot2020.pdf - * @param edgeId the edge ID of the coverage counter to increment - */ -export function incrementCounter(edgeId: number) { - const counter = coverageMap.readUint8(edgeId); - coverageMap.writeUint8(counter == 255 ? 1 : counter + 1, edgeId); -} - -export function readCounter(edgeId: number): number { - return coverageMap.readUint8(edgeId); + readCounter(edgeId: number): number { + return this.coverageMap.readUint8(edgeId); + } } diff --git a/packages/fuzzer/fuzzer.test.ts b/packages/fuzzer/fuzzer.test.ts index 54030438..091c0f4e 100644 --- a/packages/fuzzer/fuzzer.test.ts +++ b/packages/fuzzer/fuzzer.test.ts @@ -28,17 +28,17 @@ describe("compare hooks", () => { describe("incrementCounter", () => { it("should support the NeverZero policy", () => { - expect(fuzzer.readCounter(0)).toBe(0); + expect(fuzzer.coverageTracker.readCounter(0)).toBe(0); for (let counter = 1; counter <= 512; counter++) { - fuzzer.incrementCounter(0); + fuzzer.coverageTracker.incrementCounter(0); if (counter < 256) { - expect(fuzzer.readCounter(0)).toBe(counter); + expect(fuzzer.coverageTracker.readCounter(0)).toBe(counter); } else if (counter < 511) { - expect(fuzzer.readCounter(0)).toBe((counter % 256) + 1); + expect(fuzzer.coverageTracker.readCounter(0)).toBe((counter % 256) + 1); } else if (counter == 511) { - expect(fuzzer.readCounter(0)).toBe(1); + expect(fuzzer.coverageTracker.readCounter(0)).toBe(1); } else { - expect(fuzzer.readCounter(0)).toBe((counter % 256) + 2); + expect(fuzzer.coverageTracker.readCounter(0)).toBe((counter % 256) + 2); } } }); diff --git a/packages/fuzzer/fuzzer.ts b/packages/fuzzer/fuzzer.ts index e334a251..3c9585d8 100644 --- a/packages/fuzzer/fuzzer.ts +++ b/packages/fuzzer/fuzzer.ts @@ -15,16 +15,9 @@ */ import { addon, NativeAddon } from "./addon"; -import { - incrementCounter, - initializeCounters, - readCounter, - enlargeCountersBufferIfNeeded, -} from "./coverage"; +import { CoverageTracker } from "./coverage"; import { traceAndReturn, traceNumberCmp, traceStrCmp } from "./trace"; -initializeCounters(); - export type { FuzzTarget, FuzzTargetAsyncOrValue, @@ -33,20 +26,18 @@ export type { export interface Fuzzer { nativeAddon: NativeAddon; - incrementCounter: typeof incrementCounter; - readCounter: typeof readCounter; + coverageTracker: CoverageTracker; traceStrCmp: typeof traceStrCmp; traceNumberCmp: typeof traceNumberCmp; traceAndReturn: typeof traceAndReturn; - enlargeCountersBufferIfNeeded: typeof enlargeCountersBufferIfNeeded; } export const fuzzer: Fuzzer = { nativeAddon: addon, - incrementCounter, - readCounter, + coverageTracker: new CoverageTracker(), traceStrCmp, traceNumberCmp, traceAndReturn, - enlargeCountersBufferIfNeeded, }; + +export type { CoverageTracker } from "./coverage"; diff --git a/packages/instrumentor/edgeIdStrategy.ts b/packages/instrumentor/edgeIdStrategy.ts index c3d7f033..12fc9d83 100644 --- a/packages/instrumentor/edgeIdStrategy.ts +++ b/packages/instrumentor/edgeIdStrategy.ts @@ -31,7 +31,7 @@ export abstract class IncrementingEdgeIdStrategy implements EdgeIdStrategy { protected constructor(protected _nextEdgeId: number) {} nextEdgeId(): number { - fuzzer.enlargeCountersBufferIfNeeded(this._nextEdgeId); + fuzzer.coverageTracker.enlargeCountersBufferIfNeeded(this._nextEdgeId); return this._nextEdgeId++; } diff --git a/packages/instrumentor/instrument.test.ts b/packages/instrumentor/instrument.test.ts index a0b621df..8368fe67 100644 --- a/packages/instrumentor/instrument.test.ts +++ b/packages/instrumentor/instrument.test.ts @@ -98,8 +98,11 @@ function withSourceMap(fn: () => void) { const oldFuzzer = globalThis.Fuzzer; // @ts-ignore globalThis.Fuzzer = { - incrementCounter: () => { - // ignore + // @ts-ignore + coverageTracker: { + incrementCounter: (edgeId: number) => { + // ignore + }, }, }; const resetSourceMapHandlers = installSourceMapSupport(); diff --git a/packages/instrumentor/plugins/codeCoverage.test.ts b/packages/instrumentor/plugins/codeCoverage.test.ts index 39c05594..01f43e41 100644 --- a/packages/instrumentor/plugins/codeCoverage.test.ts +++ b/packages/instrumentor/plugins/codeCoverage.test.ts @@ -42,11 +42,11 @@ describe("code coverage instrumentation", () => { | true;`; const output = ` |if (1 < 2) { - | Fuzzer.incrementCounter(0); + | Fuzzer.coverageTracker.incrementCounter(0); | true; |} | - |Fuzzer.incrementCounter(0);`; + |Fuzzer.coverageTracker.incrementCounter(0);`; expectInstrumentation(input, output); }); it("should add counter in alternate branch and afterwards", () => { @@ -57,14 +57,14 @@ describe("code coverage instrumentation", () => { | false;`; const output = ` |if (1 < 2) { - | Fuzzer.incrementCounter(0); + | Fuzzer.coverageTracker.incrementCounter(0); | true; |} else { - | Fuzzer.incrementCounter(0); + | Fuzzer.coverageTracker.incrementCounter(0); | false; |} | - |Fuzzer.incrementCounter(0);`; + |Fuzzer.coverageTracker.incrementCounter(0);`; expectInstrumentation(input, output); }); }); @@ -80,20 +80,20 @@ describe("code coverage instrumentation", () => { const output = ` |switch (a) { | case 1: - | Fuzzer.incrementCounter(0); + | Fuzzer.coverageTracker.incrementCounter(0); | true; | | case 2: - | Fuzzer.incrementCounter(0); + | Fuzzer.coverageTracker.incrementCounter(0); | false; | break; | | default: - | Fuzzer.incrementCounter(0); + | Fuzzer.coverageTracker.incrementCounter(0); | true; |} | - |Fuzzer.incrementCounter(0);`; + |Fuzzer.coverageTracker.incrementCounter(0);`; expectInstrumentation(input, output); }); }); @@ -110,11 +110,11 @@ describe("code coverage instrumentation", () => { |try { | dangerousCall(); |} catch (e) { - | Fuzzer.incrementCounter(0); + | Fuzzer.coverageTracker.incrementCounter(0); | console.error(e, e.stack); |} | - |Fuzzer.incrementCounter(0);`; + |Fuzzer.coverageTracker.incrementCounter(0);`; expectInstrumentation(input, output); }); }); @@ -127,11 +127,11 @@ describe("code coverage instrumentation", () => { |}`; const output = ` |for (let i = 0; i < 100; i++) { - | Fuzzer.incrementCounter(0); + | Fuzzer.coverageTracker.incrementCounter(0); | counter++; |} | - |Fuzzer.incrementCounter(0);`; + |Fuzzer.coverageTracker.incrementCounter(0);`; expectInstrumentation(input, output); }); }); @@ -146,9 +146,9 @@ describe("code coverage instrumentation", () => { |};`; const output = ` |let foo = function add(a) { - | Fuzzer.incrementCounter(0); + | Fuzzer.coverageTracker.incrementCounter(0); | return b => { - | Fuzzer.incrementCounter(0); + | Fuzzer.coverageTracker.incrementCounter(0); | return a + b; | }; |};`; @@ -159,7 +159,7 @@ describe("code coverage instrumentation", () => { describe("LogicalExpression", () => { it("should add counters in leaves", () => { const input = `let condition = (a === "a" || (potentiallyNull ?? b === "b")) && c !== "c"`; - const output = `let condition = ((Fuzzer.incrementCounter(0), a === "a") || ((Fuzzer.incrementCounter(0), potentiallyNull) ?? (Fuzzer.incrementCounter(0), b === "b"))) && (Fuzzer.incrementCounter(0), c !== "c");`; + const output = `let condition = ((Fuzzer.coverageTracker.incrementCounter(0), a === "a") || ((Fuzzer.coverageTracker.incrementCounter(0), potentiallyNull) ?? (Fuzzer.coverageTracker.incrementCounter(0), b === "b"))) && (Fuzzer.coverageTracker.incrementCounter(0), c !== "c");`; expectInstrumentation(input, output); }); }); @@ -168,7 +168,7 @@ describe("code coverage instrumentation", () => { it("should add counters branches", () => { const input = `(a === "a" ? x : y) + 1`; const output = ` - |(a === "a" ? (Fuzzer.incrementCounter(0), x) : (Fuzzer.incrementCounter(0), y)) + 1;`; + |(a === "a" ? (Fuzzer.coverageTracker.incrementCounter(0), x) : (Fuzzer.coverageTracker.incrementCounter(0), y)) + 1;`; expectInstrumentation(input, output); }); }); diff --git a/packages/instrumentor/plugins/codeCoverage.ts b/packages/instrumentor/plugins/codeCoverage.ts index 19f30152..bb14f955 100644 --- a/packages/instrumentor/plugins/codeCoverage.ts +++ b/packages/instrumentor/plugins/codeCoverage.ts @@ -49,9 +49,10 @@ export function codeCoverage(idStrategy: EdgeIdStrategy): () => PluginTarget { } function makeCounterIncExpr(): Expression { - return types.callExpression(types.identifier("Fuzzer.incrementCounter"), [ - types.numericLiteral(idStrategy.nextEdgeId()), - ]); + return types.callExpression( + types.identifier("Fuzzer.coverageTracker.incrementCounter"), + [types.numericLiteral(idStrategy.nextEdgeId())] + ); } return () => { From 718269c9b02536fcf510ea6d480af0e7af4e142e Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Sun, 22 Jan 2023 10:01:27 +0100 Subject: [PATCH 15/22] instrumentor: fix naming in mock function --- .../instrumentor/plugins/compareHooks.test.ts | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/instrumentor/plugins/compareHooks.test.ts b/packages/instrumentor/plugins/compareHooks.test.ts index 934f7be4..86546cca 100644 --- a/packages/instrumentor/plugins/compareHooks.test.ts +++ b/packages/instrumentor/plugins/compareHooks.test.ts @@ -19,7 +19,7 @@ import { compareHooks } from "./compareHooks"; import { instrumentAndEvalWith, instrumentWith } from "./testhelpers"; import { types } from "@babel/core"; -const native = mockNativeAddonApi(); +const fuzzer = mockFuzzerApi(); const expectInstrumentationAndEval = instrumentAndEvalWith(compareHooks); const expectInstrumentation = instrumentWith(compareHooks); @@ -27,7 +27,7 @@ const expectInstrumentation = instrumentWith(compareHooks); describe("compare hooks instrumentation", () => { describe("string compares", () => { it("intercepts equals (`==` and `===`)", () => { - native.traceStrCmp.mockClear().mockReturnValue(false); + fuzzer.traceStrCmp.mockClear().mockReturnValue(false); helpers.fakePC.mockClear().mockReturnValue(types.numericLiteral(0)); const input = ` |let a = "a" @@ -38,9 +38,9 @@ describe("compare hooks instrumentation", () => { const result = expectInstrumentationAndEval(input, output); expect(result).toBe(false); - expect(native.traceStrCmp).toHaveBeenCalledTimes(2); - expect(native.traceStrCmp).toHaveBeenNthCalledWith(1, "a", "b", "===", 0); - expect(native.traceStrCmp).toHaveBeenNthCalledWith( + expect(fuzzer.traceStrCmp).toHaveBeenCalledTimes(2); + expect(fuzzer.traceStrCmp).toHaveBeenNthCalledWith(1, "a", "b", "===", 0); + expect(fuzzer.traceStrCmp).toHaveBeenNthCalledWith( 2, false, "c", @@ -50,7 +50,7 @@ describe("compare hooks instrumentation", () => { }); it("intercepts not equals (`!=` and `!==`)", () => { - native.traceStrCmp.mockClear().mockReturnValue(true); + fuzzer.traceStrCmp.mockClear().mockReturnValue(true); helpers.fakePC.mockClear().mockReturnValue(types.numericLiteral(0)); const input = ` @@ -62,15 +62,15 @@ describe("compare hooks instrumentation", () => { const result = expectInstrumentationAndEval(input, output); expect(result).toBe(true); - expect(native.traceStrCmp).toHaveBeenCalledTimes(2); - expect(native.traceStrCmp).toHaveBeenNthCalledWith(1, "a", "b", "!==", 0); - expect(native.traceStrCmp).toHaveBeenNthCalledWith(2, true, "c", "!=", 0); + expect(fuzzer.traceStrCmp).toHaveBeenCalledTimes(2); + expect(fuzzer.traceStrCmp).toHaveBeenNthCalledWith(1, "a", "b", "!==", 0); + expect(fuzzer.traceStrCmp).toHaveBeenNthCalledWith(2, true, "c", "!=", 0); }); }); describe("integer compares", () => { it("intercepts equals (`==` and `===`))", () => { - native.traceNumberCmp.mockClear().mockReturnValue(false); + fuzzer.traceNumberCmp.mockClear().mockReturnValue(false); helpers.fakePC.mockClear().mockReturnValue(types.numericLiteral(0)); const input = ` @@ -81,15 +81,15 @@ describe("compare hooks instrumentation", () => { |Fuzzer.traceNumberCmp(Fuzzer.traceNumberCmp(a, 20, "===", 0), 30, "==", 0);`; const result = expectInstrumentationAndEval(input, output); expect(result).toBe(false); - expect(native.traceNumberCmp).toHaveBeenCalledTimes(2); - expect(native.traceNumberCmp).toHaveBeenNthCalledWith( + expect(fuzzer.traceNumberCmp).toHaveBeenCalledTimes(2); + expect(fuzzer.traceNumberCmp).toHaveBeenNthCalledWith( 1, 10, 20, "===", 0 ); - expect(native.traceNumberCmp).toHaveBeenNthCalledWith( + expect(fuzzer.traceNumberCmp).toHaveBeenNthCalledWith( 2, false, 30, @@ -99,7 +99,7 @@ describe("compare hooks instrumentation", () => { }); it("intercepts not equals (`!=` and `!==`))", () => { - native.traceNumberCmp.mockClear().mockReturnValue(true); + fuzzer.traceNumberCmp.mockClear().mockReturnValue(true); helpers.fakePC.mockClear().mockReturnValue(types.numericLiteral(0)); const input = ` @@ -110,15 +110,15 @@ describe("compare hooks instrumentation", () => { |Fuzzer.traceNumberCmp(Fuzzer.traceNumberCmp(a, 20, "!==", 0), 30, "!=", 0);`; const result = expectInstrumentationAndEval(input, output); expect(result).toBe(true); - expect(native.traceNumberCmp).toHaveBeenCalledTimes(2); - expect(native.traceNumberCmp).toHaveBeenNthCalledWith( + expect(fuzzer.traceNumberCmp).toHaveBeenCalledTimes(2); + expect(fuzzer.traceNumberCmp).toHaveBeenNthCalledWith( 1, 10, 20, "!==", 0 ); - expect(native.traceNumberCmp).toHaveBeenNthCalledWith( + expect(fuzzer.traceNumberCmp).toHaveBeenNthCalledWith( 2, true, 30, @@ -129,7 +129,7 @@ describe("compare hooks instrumentation", () => { it("intercepts greater and less them", () => { [">", "<", ">=", "<="].forEach((operator) => { - native.traceNumberCmp.mockClear().mockReturnValue(false); + fuzzer.traceNumberCmp.mockClear().mockReturnValue(false); helpers.fakePC.mockClear().mockReturnValue(types.numericLiteral(0)); const input = ` |let a = 10 @@ -139,8 +139,8 @@ describe("compare hooks instrumentation", () => { |Fuzzer.traceNumberCmp(a, 20, "${operator}", 0);`; const result = expectInstrumentationAndEval(input, output); expect(result).toBe(false); - expect(native.traceNumberCmp).toHaveBeenCalledTimes(1); - expect(native.traceNumberCmp).toHaveBeenNthCalledWith( + expect(fuzzer.traceNumberCmp).toHaveBeenCalledTimes(1); + expect(fuzzer.traceNumberCmp).toHaveBeenNthCalledWith( 1, 10, 20, @@ -228,17 +228,17 @@ describe("compare hooks instrumentation", () => { }); }); -// Mock global native addon API +// Mock global Fuzzer API // This is normally done by the jest environment. Here we replace every // API function with a jest mock, which can be configured in the test. -function mockNativeAddonApi() { +function mockFuzzerApi() { // eslint-disable-next-line @typescript-eslint/no-var-requires - const native = require("@jazzer.js/fuzzer").fuzzer; + const fuzzer = require("@jazzer.js/fuzzer").fuzzer; jest.mock("@jazzer.js/fuzzer"); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - global.Fuzzer = native; - return native; + global.Fuzzer = fuzzer; + return fuzzer; } function mockHelpers() { From 8a695263b57dcfb754fce4abd34a255c9fe750f4 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Sun, 22 Jan 2023 10:24:49 +0100 Subject: [PATCH 16/22] instrumentor: extract compare tracing functions into dedicated object --- packages/fuzzer/fuzzer.test.ts | 8 +- packages/fuzzer/fuzzer.ts | 11 +-- packages/fuzzer/trace.ts | 18 ++++- .../instrumentor/plugins/compareHooks.test.ts | 78 ++++++++++++------- packages/instrumentor/plugins/compareHooks.ts | 6 +- 5 files changed, 74 insertions(+), 47 deletions(-) diff --git a/packages/fuzzer/fuzzer.test.ts b/packages/fuzzer/fuzzer.test.ts index 091c0f4e..0c3b8cd0 100644 --- a/packages/fuzzer/fuzzer.test.ts +++ b/packages/fuzzer/fuzzer.test.ts @@ -19,10 +19,10 @@ import { fuzzer } from "./fuzzer"; describe("compare hooks", () => { it("traceStrCmp supports equals operators", () => { - expect(fuzzer.traceStrCmp("a", "b", "==", 0)).toBe(false); - expect(fuzzer.traceStrCmp("a", "b", "===", 0)).toBe(false); - expect(fuzzer.traceStrCmp("a", "b", "!=", 0)).toBe(true); - expect(fuzzer.traceStrCmp("a", "b", "!==", 0)).toBe(true); + expect(fuzzer.tracer.traceStrCmp("a", "b", "==", 0)).toBe(false); + expect(fuzzer.tracer.traceStrCmp("a", "b", "===", 0)).toBe(false); + expect(fuzzer.tracer.traceStrCmp("a", "b", "!=", 0)).toBe(true); + expect(fuzzer.tracer.traceStrCmp("a", "b", "!==", 0)).toBe(true); }); }); diff --git a/packages/fuzzer/fuzzer.ts b/packages/fuzzer/fuzzer.ts index 3c9585d8..218b579c 100644 --- a/packages/fuzzer/fuzzer.ts +++ b/packages/fuzzer/fuzzer.ts @@ -16,7 +16,7 @@ import { addon, NativeAddon } from "./addon"; import { CoverageTracker } from "./coverage"; -import { traceAndReturn, traceNumberCmp, traceStrCmp } from "./trace"; +import { Tracer, tracer } from "./trace"; export type { FuzzTarget, @@ -27,17 +27,14 @@ export type { export interface Fuzzer { nativeAddon: NativeAddon; coverageTracker: CoverageTracker; - traceStrCmp: typeof traceStrCmp; - traceNumberCmp: typeof traceNumberCmp; - traceAndReturn: typeof traceAndReturn; + tracer: Tracer; } export const fuzzer: Fuzzer = { nativeAddon: addon, coverageTracker: new CoverageTracker(), - traceStrCmp, - traceNumberCmp, - traceAndReturn, + tracer: tracer, }; export type { CoverageTracker } from "./coverage"; +export type { Tracer } from "./trace"; diff --git a/packages/fuzzer/trace.ts b/packages/fuzzer/trace.ts index d7c163f7..25625405 100644 --- a/packages/fuzzer/trace.ts +++ b/packages/fuzzer/trace.ts @@ -26,7 +26,7 @@ import { addon } from "./addon"; * @param id an unique identifier to distinguish between the different comparisons * @returns result of the comparison */ -export function traceStrCmp( +function traceStrCmp( s1: string, s2: string, operator: string, @@ -68,7 +68,7 @@ export function traceStrCmp( * @param id an unique identifier to distinguish between the different comparisons * @returns result of the comparison */ -export function traceNumberCmp( +function traceNumberCmp( n1: number, n2: number, operator: string, @@ -99,7 +99,7 @@ export function traceNumberCmp( } } -export function traceAndReturn(current: unknown, target: unknown, id: number) { +function traceAndReturn(current: unknown, target: unknown, id: number) { switch (typeof target) { case "number": if (typeof current === "number") { @@ -115,3 +115,15 @@ export function traceAndReturn(current: unknown, target: unknown, id: number) { } return target; } + +export interface Tracer { + traceStrCmp: typeof traceStrCmp; + traceNumberCmp: typeof traceNumberCmp; + traceAndReturn: typeof traceAndReturn; +} + +export const tracer: Tracer = { + traceStrCmp, + traceNumberCmp, + traceAndReturn, +}; diff --git a/packages/instrumentor/plugins/compareHooks.test.ts b/packages/instrumentor/plugins/compareHooks.test.ts index 86546cca..32a1cf7d 100644 --- a/packages/instrumentor/plugins/compareHooks.test.ts +++ b/packages/instrumentor/plugins/compareHooks.test.ts @@ -27,20 +27,26 @@ const expectInstrumentation = instrumentWith(compareHooks); describe("compare hooks instrumentation", () => { describe("string compares", () => { it("intercepts equals (`==` and `===`)", () => { - fuzzer.traceStrCmp.mockClear().mockReturnValue(false); + fuzzer.tracer.traceStrCmp.mockClear().mockReturnValue(false); helpers.fakePC.mockClear().mockReturnValue(types.numericLiteral(0)); const input = ` |let a = "a" |a === "b" == "c"`; const output = ` |let a = "a"; - |Fuzzer.traceStrCmp(Fuzzer.traceStrCmp(a, "b", "===", 0), "c", "==", 0);`; + |Fuzzer.tracer.traceStrCmp(Fuzzer.tracer.traceStrCmp(a, "b", "===", 0), "c", "==", 0);`; const result = expectInstrumentationAndEval(input, output); expect(result).toBe(false); - expect(fuzzer.traceStrCmp).toHaveBeenCalledTimes(2); - expect(fuzzer.traceStrCmp).toHaveBeenNthCalledWith(1, "a", "b", "===", 0); - expect(fuzzer.traceStrCmp).toHaveBeenNthCalledWith( + expect(fuzzer.tracer.traceStrCmp).toHaveBeenCalledTimes(2); + expect(fuzzer.tracer.traceStrCmp).toHaveBeenNthCalledWith( + 1, + "a", + "b", + "===", + 0 + ); + expect(fuzzer.tracer.traceStrCmp).toHaveBeenNthCalledWith( 2, false, "c", @@ -50,7 +56,7 @@ describe("compare hooks instrumentation", () => { }); it("intercepts not equals (`!=` and `!==`)", () => { - fuzzer.traceStrCmp.mockClear().mockReturnValue(true); + fuzzer.tracer.traceStrCmp.mockClear().mockReturnValue(true); helpers.fakePC.mockClear().mockReturnValue(types.numericLiteral(0)); const input = ` @@ -58,19 +64,31 @@ describe("compare hooks instrumentation", () => { |a !== "b" != "c"`; const output = ` |let a = "a"; - |Fuzzer.traceStrCmp(Fuzzer.traceStrCmp(a, "b", "!==", 0), "c", "!=", 0);`; + |Fuzzer.tracer.traceStrCmp(Fuzzer.tracer.traceStrCmp(a, "b", "!==", 0), "c", "!=", 0);`; const result = expectInstrumentationAndEval(input, output); expect(result).toBe(true); - expect(fuzzer.traceStrCmp).toHaveBeenCalledTimes(2); - expect(fuzzer.traceStrCmp).toHaveBeenNthCalledWith(1, "a", "b", "!==", 0); - expect(fuzzer.traceStrCmp).toHaveBeenNthCalledWith(2, true, "c", "!=", 0); + expect(fuzzer.tracer.traceStrCmp).toHaveBeenCalledTimes(2); + expect(fuzzer.tracer.traceStrCmp).toHaveBeenNthCalledWith( + 1, + "a", + "b", + "!==", + 0 + ); + expect(fuzzer.tracer.traceStrCmp).toHaveBeenNthCalledWith( + 2, + true, + "c", + "!=", + 0 + ); }); }); describe("integer compares", () => { it("intercepts equals (`==` and `===`))", () => { - fuzzer.traceNumberCmp.mockClear().mockReturnValue(false); + fuzzer.tracer.traceNumberCmp.mockClear().mockReturnValue(false); helpers.fakePC.mockClear().mockReturnValue(types.numericLiteral(0)); const input = ` @@ -78,18 +96,18 @@ describe("compare hooks instrumentation", () => { |a === 20 == 30`; const output = ` |let a = 10; - |Fuzzer.traceNumberCmp(Fuzzer.traceNumberCmp(a, 20, "===", 0), 30, "==", 0);`; + |Fuzzer.tracer.traceNumberCmp(Fuzzer.tracer.traceNumberCmp(a, 20, "===", 0), 30, "==", 0);`; const result = expectInstrumentationAndEval(input, output); expect(result).toBe(false); - expect(fuzzer.traceNumberCmp).toHaveBeenCalledTimes(2); - expect(fuzzer.traceNumberCmp).toHaveBeenNthCalledWith( + expect(fuzzer.tracer.traceNumberCmp).toHaveBeenCalledTimes(2); + expect(fuzzer.tracer.traceNumberCmp).toHaveBeenNthCalledWith( 1, 10, 20, "===", 0 ); - expect(fuzzer.traceNumberCmp).toHaveBeenNthCalledWith( + expect(fuzzer.tracer.traceNumberCmp).toHaveBeenNthCalledWith( 2, false, 30, @@ -99,7 +117,7 @@ describe("compare hooks instrumentation", () => { }); it("intercepts not equals (`!=` and `!==`))", () => { - fuzzer.traceNumberCmp.mockClear().mockReturnValue(true); + fuzzer.tracer.traceNumberCmp.mockClear().mockReturnValue(true); helpers.fakePC.mockClear().mockReturnValue(types.numericLiteral(0)); const input = ` @@ -107,18 +125,18 @@ describe("compare hooks instrumentation", () => { |a !== 20 != 30`; const output = ` |let a = 10; - |Fuzzer.traceNumberCmp(Fuzzer.traceNumberCmp(a, 20, "!==", 0), 30, "!=", 0);`; + |Fuzzer.tracer.traceNumberCmp(Fuzzer.tracer.traceNumberCmp(a, 20, "!==", 0), 30, "!=", 0);`; const result = expectInstrumentationAndEval(input, output); expect(result).toBe(true); - expect(fuzzer.traceNumberCmp).toHaveBeenCalledTimes(2); - expect(fuzzer.traceNumberCmp).toHaveBeenNthCalledWith( + expect(fuzzer.tracer.traceNumberCmp).toHaveBeenCalledTimes(2); + expect(fuzzer.tracer.traceNumberCmp).toHaveBeenNthCalledWith( 1, 10, 20, "!==", 0 ); - expect(fuzzer.traceNumberCmp).toHaveBeenNthCalledWith( + expect(fuzzer.tracer.traceNumberCmp).toHaveBeenNthCalledWith( 2, true, 30, @@ -129,18 +147,18 @@ describe("compare hooks instrumentation", () => { it("intercepts greater and less them", () => { [">", "<", ">=", "<="].forEach((operator) => { - fuzzer.traceNumberCmp.mockClear().mockReturnValue(false); + fuzzer.tracer.traceNumberCmp.mockClear().mockReturnValue(false); helpers.fakePC.mockClear().mockReturnValue(types.numericLiteral(0)); const input = ` |let a = 10 |a ${operator} 20`; const output = ` |let a = 10; - |Fuzzer.traceNumberCmp(a, 20, "${operator}", 0);`; + |Fuzzer.tracer.traceNumberCmp(a, 20, "${operator}", 0);`; const result = expectInstrumentationAndEval(input, output); expect(result).toBe(false); - expect(fuzzer.traceNumberCmp).toHaveBeenCalledTimes(1); - expect(fuzzer.traceNumberCmp).toHaveBeenNthCalledWith( + expect(fuzzer.tracer.traceNumberCmp).toHaveBeenCalledTimes(1); + expect(fuzzer.tracer.traceNumberCmp).toHaveBeenNthCalledWith( 1, 10, 20, @@ -170,15 +188,15 @@ describe("compare hooks instrumentation", () => { |}`; const output = ` |switch (day) { - | case Fuzzer.traceAndReturn(day, "Monday", 0): + | case Fuzzer.tracer.traceAndReturn(day, "Monday", 0): | console.log("monday"); | break; | - | case Fuzzer.traceAndReturn(day, "Tuesday", 0): + | case Fuzzer.tracer.traceAndReturn(day, "Tuesday", 0): | console.log("Tuesday"); | break; | - | case Fuzzer.traceAndReturn(day, "Friday", 0): + | case Fuzzer.tracer.traceAndReturn(day, "Friday", 0): | console.log("Friday"); | break; | @@ -207,15 +225,15 @@ describe("compare hooks instrumentation", () => { |}`; const output = ` |switch (count) { - | case Fuzzer.traceAndReturn(count, 1, 0): + | case Fuzzer.tracer.traceAndReturn(count, 1, 0): | console.log("1"); | break; | - | case Fuzzer.traceAndReturn(count, 2, 0): + | case Fuzzer.tracer.traceAndReturn(count, 2, 0): | console.log("2"); | break; | - | case Fuzzer.traceAndReturn(count, 5, 0): + | case Fuzzer.tracer.traceAndReturn(count, 5, 0): | console.log("5"); | break; | diff --git a/packages/instrumentor/plugins/compareHooks.ts b/packages/instrumentor/plugins/compareHooks.ts index e9f77153..68eedb36 100644 --- a/packages/instrumentor/plugins/compareHooks.ts +++ b/packages/instrumentor/plugins/compareHooks.ts @@ -36,9 +36,9 @@ export function compareHooks(): PluginTarget { let hookFunctionName: string; if (isStringCompare(path.node)) { - hookFunctionName = "Fuzzer.traceStrCmp"; + hookFunctionName = "Fuzzer.tracer.traceStrCmp"; } else if (isNumberCompare(path.node)) { - hookFunctionName = "Fuzzer.traceNumberCmp"; + hookFunctionName = "Fuzzer.tracer.traceNumberCmp"; } else { return; } @@ -61,7 +61,7 @@ export function compareHooks(): PluginTarget { const test = path.node.cases[i].test; if (test) { path.node.cases[i].test = types.callExpression( - types.identifier("Fuzzer.traceAndReturn"), + types.identifier("Fuzzer.tracer.traceAndReturn"), [id, test, fakePC()] ); } From 116aa718ac6a9a9a3bd5c9cb19ed386097128089 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Mon, 23 Jan 2023 13:12:42 +0100 Subject: [PATCH 17/22] cli: mark the id_sync_file as hidden --- packages/core/cli.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/cli.ts b/packages/core/cli.ts index 7b14550f..eabbf410 100644 --- a/packages/core/cli.ts +++ b/packages/core/cli.ts @@ -82,6 +82,7 @@ yargs(process.argv.slice(2)) default: undefined, group: "Fuzzer:", }) + .hide("id_sync_file") .option("sync", { describe: "Run the fuzz target synchronously.", From dafff5ae6c2141edf10bd58a144d5286c72d8dbb Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Mon, 23 Jan 2023 13:12:56 +0100 Subject: [PATCH 18/22] instrumentor: mark fatalExitCode of ID file sync strategy as readonly --- packages/instrumentor/edgeIdStrategy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/instrumentor/edgeIdStrategy.ts b/packages/instrumentor/edgeIdStrategy.ts index 12fc9d83..f3e70400 100644 --- a/packages/instrumentor/edgeIdStrategy.ts +++ b/packages/instrumentor/edgeIdStrategy.ts @@ -70,7 +70,7 @@ interface EdgeIdInfo { * multiple processes accessing it during instrumentation. */ export class FileSyncIdStrategy extends IncrementingEdgeIdStrategy { - private static fatalExitCode = 79; + private static readonly fatalExitCode = 79; private cachedIdCount: number | undefined; private firstEdgeId: number | undefined; private releaseLockOnSyncFile: (() => void) | undefined; From fa950e3f24dba21da271a720e50f007ae63a785d Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Mon, 23 Jan 2023 16:46:48 +0100 Subject: [PATCH 19/22] fuzzer: do not export the native addon through the fuzzer interface --- package-lock.json | 2 +- packages/core/core.ts | 12 ++++++------ packages/core/jazzer.ts | 6 +++--- packages/fuzzer/addon.ts | 2 +- packages/fuzzer/coverage.ts | 2 ++ packages/fuzzer/fuzzer.ts | 16 ++++++++++------ packages/fuzzer/trace.ts | 6 ++++++ 7 files changed, 29 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index ab5248ef..85aa3100 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9124,7 +9124,7 @@ "@types/babel__core": "^7.20.0", "@types/istanbul-lib-hook": "^2.0.1", "@types/node": "^18.11.18", - "@types/proper-lockfile": "*", + "@types/proper-lockfile": "^4.1.2", "@types/source-map-support": "^0.5.6", "istanbul-lib-hook": "^3.0.0", "proper-lockfile": "^4.1.2", diff --git a/packages/core/core.ts b/packages/core/core.ts index 3cbf35a0..79ecd87a 100644 --- a/packages/core/core.ts +++ b/packages/core/core.ts @@ -94,8 +94,8 @@ export async function startFuzzingNoInit( ) { const fuzzerOptions = buildFuzzerOptions(options); const fuzzerFn = options.sync - ? Fuzzer.nativeAddon.startFuzzing - : Fuzzer.nativeAddon.startFuzzingAsync; + ? Fuzzer.startFuzzing + : Fuzzer.startFuzzingAsync; // Wrap the potentially sync fuzzer call, so that resolve and exception // handlers are always executed. return Promise.resolve().then(() => fuzzerFn(fuzzFn, fuzzerOptions)); @@ -165,7 +165,7 @@ function stopFuzzing(err: unknown, expectedErrors: string[]) { console.error( `ERROR: Received no error, but expected one of [${expectedErrors}].` ); - Fuzzer.nativeAddon.stopFuzzingAsync(ERROR_UNEXPECTED_CODE); + Fuzzer.stopFuzzingAsync(ERROR_UNEXPECTED_CODE); } return; } @@ -175,13 +175,13 @@ function stopFuzzing(err: unknown, expectedErrors: string[]) { const name = errorName(err); if (expectedErrors.includes(name)) { console.error(`INFO: Received expected error "${name}".`); - Fuzzer.nativeAddon.stopFuzzingAsync(ERROR_EXPECTED_CODE); + Fuzzer.stopFuzzingAsync(ERROR_EXPECTED_CODE); } else { printError(err); console.error( `ERROR: Received error "${name}" is not in expected errors [${expectedErrors}].` ); - Fuzzer.nativeAddon.stopFuzzingAsync(ERROR_UNEXPECTED_CODE); + Fuzzer.stopFuzzingAsync(ERROR_UNEXPECTED_CODE); } return; } @@ -189,7 +189,7 @@ function stopFuzzing(err: unknown, expectedErrors: string[]) { // Error found, but no specific one expected. This case is used for normal // fuzzing runs, so no dedicated exit code is given to the stop fuzzing function. printError(err); - Fuzzer.nativeAddon.stopFuzzingAsync(); + Fuzzer.stopFuzzingAsync(); } function errorName(error: unknown): string { diff --git a/packages/core/jazzer.ts b/packages/core/jazzer.ts index 2d56b490..2774d9e7 100644 --- a/packages/core/jazzer.ts +++ b/packages/core/jazzer.ts @@ -29,7 +29,7 @@ import { fuzzer } from "@jazzer.js/fuzzer"; * @param id a (probabilistically) unique identifier for this particular compare hint */ function guideTowardsEquality(current: string, target: string, id: number) { - fuzzer.nativeAddon.traceUnequalStrings(id, current, target); + fuzzer.tracer.traceUnequalStrings(id, current, target); } /** @@ -45,7 +45,7 @@ function guideTowardsEquality(current: string, target: string, id: number) { * @param id a (probabilistically) unique identifier for this particular compare hint */ function guideTowardsContainment(needle: string, haystack: string, id: number) { - fuzzer.nativeAddon.traceStringContainment(id, needle, haystack); + fuzzer.tracer.traceStringContainment(id, needle, haystack); } /** @@ -63,7 +63,7 @@ function guideTowardsContainment(needle: string, haystack: string, id: number) { * @param id a (probabilistically) unique identifier for this particular state hint */ function exploreState(state: number, id: number) { - fuzzer.nativeAddon.tracePcIndir(id, state); + fuzzer.tracer.tracePcIndir(id, state); } export interface Jazzer { diff --git a/packages/fuzzer/addon.ts b/packages/fuzzer/addon.ts index 60867571..84cf75fb 100644 --- a/packages/fuzzer/addon.ts +++ b/packages/fuzzer/addon.ts @@ -33,7 +33,7 @@ export type StartFuzzingAsyncFn = ( fuzzOpts: FuzzOpts ) => Promise; -export type NativeAddon = { +type NativeAddon = { registerCoverageMap: (buffer: Buffer) => void; registerNewCounters: (oldNumCounters: number, newNumCounters: number) => void; diff --git a/packages/fuzzer/coverage.ts b/packages/fuzzer/coverage.ts index 89b7df69..ee24df38 100644 --- a/packages/fuzzer/coverage.ts +++ b/packages/fuzzer/coverage.ts @@ -66,3 +66,5 @@ export class CoverageTracker { return this.coverageMap.readUint8(edgeId); } } + +export const coverageTracker = new CoverageTracker(); diff --git a/packages/fuzzer/fuzzer.ts b/packages/fuzzer/fuzzer.ts index 218b579c..25ef92a6 100644 --- a/packages/fuzzer/fuzzer.ts +++ b/packages/fuzzer/fuzzer.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { addon, NativeAddon } from "./addon"; -import { CoverageTracker } from "./coverage"; -import { Tracer, tracer } from "./trace"; +import { coverageTracker, CoverageTracker } from "./coverage"; +import { tracer, Tracer } from "./trace"; +import { addon } from "./addon"; export type { FuzzTarget, @@ -25,15 +25,19 @@ export type { } from "./addon"; export interface Fuzzer { - nativeAddon: NativeAddon; coverageTracker: CoverageTracker; tracer: Tracer; + startFuzzing: typeof addon.startFuzzing; + startFuzzingAsync: typeof addon.startFuzzingAsync; + stopFuzzingAsync: typeof addon.stopFuzzingAsync; } export const fuzzer: Fuzzer = { - nativeAddon: addon, - coverageTracker: new CoverageTracker(), + coverageTracker: coverageTracker, tracer: tracer, + startFuzzing: addon.startFuzzing, + startFuzzingAsync: addon.startFuzzingAsync, + stopFuzzingAsync: addon.stopFuzzingAsync, }; export type { CoverageTracker } from "./coverage"; diff --git a/packages/fuzzer/trace.ts b/packages/fuzzer/trace.ts index 25625405..b377b186 100644 --- a/packages/fuzzer/trace.ts +++ b/packages/fuzzer/trace.ts @@ -118,12 +118,18 @@ function traceAndReturn(current: unknown, target: unknown, id: number) { export interface Tracer { traceStrCmp: typeof traceStrCmp; + traceUnequalStrings: typeof addon.traceUnequalStrings; + traceStringContainment: typeof addon.traceStringContainment; traceNumberCmp: typeof traceNumberCmp; traceAndReturn: typeof traceAndReturn; + tracePcIndir: typeof addon.tracePcIndir; } export const tracer: Tracer = { traceStrCmp, + traceUnequalStrings: addon.traceUnequalStrings, + traceStringContainment: addon.traceStringContainment, traceNumberCmp, traceAndReturn, + tracePcIndir: addon.tracePcIndir, }; From 185ac86335a246717ce4b0f3473b4822ed414f32 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Mon, 23 Jan 2023 17:18:49 +0100 Subject: [PATCH 20/22] instrumentor: add an edge ID strategy returning zero IDs This strategy is only used for testing purposes --- packages/instrumentor/edgeIdStrategy.ts | 16 ++++++++++++++++ .../instrumentor/plugins/codeCoverage.test.ts | 16 ++-------------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/instrumentor/edgeIdStrategy.ts b/packages/instrumentor/edgeIdStrategy.ts index f3e70400..c739c784 100644 --- a/packages/instrumentor/edgeIdStrategy.ts +++ b/packages/instrumentor/edgeIdStrategy.ts @@ -218,3 +218,19 @@ export class FileSyncIdStrategy extends IncrementingEdgeIdStrategy { ); } } + +export class ZeroEdgeIdStrategy implements EdgeIdStrategy { + nextEdgeId(): number { + return 0; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + startForSourceFile(filename: string): void { + // Nothing to do here + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + commitIdCount(filename: string): void { + // Nothing to do here + } +} diff --git a/packages/instrumentor/plugins/codeCoverage.test.ts b/packages/instrumentor/plugins/codeCoverage.test.ts index 01f43e41..24591325 100644 --- a/packages/instrumentor/plugins/codeCoverage.test.ts +++ b/packages/instrumentor/plugins/codeCoverage.test.ts @@ -16,22 +16,10 @@ import { codeCoverage } from "./codeCoverage"; import { instrumentWith } from "./testhelpers"; -import { MemorySyncIdStrategy } from "../edgeIdStrategy"; - -jest.mock("../edgeIdStrategy", () => { - return { - MemorySyncIdStrategy: jest.fn().mockImplementation(() => { - return { - nextEdgeId: () => { - return 0; - }, - }; - }), - }; -}); +import { ZeroEdgeIdStrategy } from "../edgeIdStrategy"; const expectInstrumentation = instrumentWith( - codeCoverage(new MemorySyncIdStrategy()) + codeCoverage(new ZeroEdgeIdStrategy()) ); describe("code coverage instrumentation", () => { From 1d59313b47120de2afd6ffd60e3e69f11c2406b7 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Tue, 24 Jan 2023 13:09:54 +0100 Subject: [PATCH 21/22] instrumentor: extract instrumentor into own class This is a first step to simplify the interface to the instrumentor package and makes testing easier. --- packages/core/core.ts | 25 +- packages/instrumentor/instrument.test.ts | 66 +++-- packages/instrumentor/instrument.ts | 257 +++++++++---------- packages/instrumentor/plugins/testhelpers.ts | 5 +- 4 files changed, 182 insertions(+), 171 deletions(-) diff --git a/packages/core/core.ts b/packages/core/core.ts index 79ecd87a..d451ee60 100644 --- a/packages/core/core.ts +++ b/packages/core/core.ts @@ -15,14 +15,20 @@ */ import path from "path"; -import * as fuzzer from "@jazzer.js/fuzzer"; -import * as hooking from "@jazzer.js/hooking"; -import { registerInstrumentor } from "@jazzer.js/instrumentor"; -import { trackedHooks } from "@jazzer.js/hooking"; import * as process from "process"; import * as tmp from "tmp"; import * as fs from "fs"; +import * as fuzzer from "@jazzer.js/fuzzer"; +import * as hooking from "@jazzer.js/hooking"; +import { trackedHooks } from "@jazzer.js/hooking"; +import { + registerInstrumentor, + Instrumentor, + FileSyncIdStrategy, + MemorySyncIdStrategy, +} from "@jazzer.js/instrumentor"; + // Remove temporary files on exit tmp.setGracefulCleanup(); @@ -62,9 +68,14 @@ export async function initFuzzing(options: Options) { await Promise.all(options.customHooks.map(importModule)); if (!options.dryRun) { registerInstrumentor( - options.includes, - options.excludes, - options.idSyncFile + new Instrumentor( + options.includes, + options.excludes, + + options.idSyncFile !== undefined + ? new FileSyncIdStrategy(options.idSyncFile) + : new MemorySyncIdStrategy() + ) ); } } diff --git a/packages/instrumentor/instrument.test.ts b/packages/instrumentor/instrument.test.ts index 8368fe67..7c3854a4 100644 --- a/packages/instrumentor/instrument.test.ts +++ b/packages/instrumentor/instrument.test.ts @@ -16,48 +16,59 @@ /* eslint @typescript-eslint/ban-ts-comment:0 */ -import { - installSourceMapSupport, - shouldInstrumentFn, - transform, -} from "./instrument"; import { codeCoverage } from "./plugins/codeCoverage"; import { MemorySyncIdStrategy } from "./edgeIdStrategy"; +import { Instrumentor } from "./instrument"; describe("shouldInstrument check", () => { it("should consider includes and excludes", () => { - const check = shouldInstrumentFn(["include"], ["exclude"]); - expect(check("include")).toBeTruthy(); - expect(check("exclude")).toBeFalsy(); - expect(check("/some/package/include/files")).toBeTruthy(); - expect(check("/some/package/exclude/files")).toBeFalsy(); - expect(check("/something/else")).toBeFalsy(); + const instrumentor = new Instrumentor(["include"], ["exclude"]); + expect(instrumentor.shouldInstrument("include")).toBeTruthy(); + expect(instrumentor.shouldInstrument("exclude")).toBeFalsy(); + expect( + instrumentor.shouldInstrument("/some/package/include/files") + ).toBeTruthy(); + expect( + instrumentor.shouldInstrument("/some/package/exclude/files") + ).toBeFalsy(); + expect(instrumentor.shouldInstrument("/something/else")).toBeFalsy(); }); it("should include everything with *", () => { - const check = shouldInstrumentFn(["*"], []); - expect(check("include")).toBeTruthy(); - expect(check("/something/else")).toBeTruthy(); + const instrumentor = new Instrumentor(["*"], []); + expect(instrumentor.shouldInstrument("include")).toBeTruthy(); + expect(instrumentor.shouldInstrument("/something/else")).toBeTruthy(); }); it("should include nothing with emtpy string", () => { - const emtpyInclude = shouldInstrumentFn(["include", ""], []); - expect(emtpyInclude("include")).toBeTruthy(); - expect(emtpyInclude("/something/else")).toBeFalsy(); - const emtpyExclude = shouldInstrumentFn(["include"], [""]); - expect(emtpyExclude("include")).toBeTruthy(); - expect(emtpyExclude("/something/else")).toBeFalsy(); + const instrumentorWithEmptyInclude = new Instrumentor(["include", ""], []); + expect( + instrumentorWithEmptyInclude.shouldInstrument("include") + ).toBeTruthy(); + expect( + instrumentorWithEmptyInclude.shouldInstrument("/something/else") + ).toBeFalsy(); + + const instrumentorWithEmptyExclude = new Instrumentor(["include"], [""]); + expect( + instrumentorWithEmptyExclude.shouldInstrument("include") + ).toBeTruthy(); + expect( + instrumentorWithEmptyExclude.shouldInstrument("/something/else") + ).toBeFalsy(); }); it("should exclude with precedence", () => { - const check = shouldInstrumentFn(["include"], ["*"]); - expect(check("/some/package/include/files")).toBeFalsy(); + const instrumentor = new Instrumentor(["include"], ["*"]); + expect( + instrumentor.shouldInstrument("/some/package/include/files") + ).toBeFalsy(); }); }); describe("transform", () => { it("should use source maps to correct error stack traces", () => { - withSourceMap(() => { + withSourceMap((instrumentor: Instrumentor) => { const sourceFileName = "sourcemap-test.js"; const errorLocation = sourceFileName + ":5:13"; const content = ` @@ -75,7 +86,7 @@ describe("transform", () => { try { // Use the codeCoverage plugin to add additional lines, so that the // resulting error stack does not match the original code anymore. - const result = transform(sourceFileName, content, [ + const result = instrumentor.transform(sourceFileName, content, [ codeCoverage(new MemorySyncIdStrategy()), ]); const fn = eval(result?.code || ""); @@ -93,7 +104,7 @@ describe("transform", () => { }); }); -function withSourceMap(fn: () => void) { +function withSourceMap(fn: (instrumentor: Instrumentor) => void) { // @ts-ignore const oldFuzzer = globalThis.Fuzzer; // @ts-ignore @@ -105,9 +116,10 @@ function withSourceMap(fn: () => void) { }, }, }; - const resetSourceMapHandlers = installSourceMapSupport(); + const instrumentor = new Instrumentor(); + const resetSourceMapHandlers = instrumentor.init(); try { - fn(); + fn(instrumentor); } finally { resetSourceMapHandlers(); // @ts-ignore diff --git a/packages/instrumentor/instrument.ts b/packages/instrumentor/instrument.ts index ca9cd015..cba8fc48 100644 --- a/packages/instrumentor/instrument.ts +++ b/packages/instrumentor/instrument.ts @@ -27,11 +27,7 @@ import { codeCoverage } from "./plugins/codeCoverage"; import { compareHooks } from "./plugins/compareHooks"; import { functionHooks } from "./plugins/functionHooks"; import { hookManager } from "@jazzer.js/hooking"; -import { - EdgeIdStrategy, - FileSyncIdStrategy, - MemorySyncIdStrategy, -} from "./edgeIdStrategy"; +import { EdgeIdStrategy, MemorySyncIdStrategy } from "./edgeIdStrategy"; interface SourceMaps { [file: string]: RawSourceMap; @@ -39,88 +35,126 @@ interface SourceMaps { const sourceMaps: SourceMaps = {}; -/* Installs source-map-support handlers and returns a reset function */ -export function installSourceMapSupport(): () => void { - // Use the source-map-support library to enable in-memory source maps of - // transformed code and error stack rewrites. - // As there is no way to populate the source map cache of source-map-support, - // an additional buffer is used to pass on the source maps from babel to the - // library. This could be memory intensive and should be replaced by - // tmp source map files, if it really becomes a problem. - sms.install({ - hookRequire: true, - retrieveSourceMap: (source) => { - if (sourceMaps[source]) { - return { - map: sourceMaps[source], - url: source, - }; - } - return null; - }, - }); - return sms.resetRetrieveHandlers; -} +export { + EdgeIdStrategy, + FileSyncIdStrategy, + MemorySyncIdStrategy, +} from "./edgeIdStrategy"; -export type FilePredicate = (filepath: string) => boolean; +export class Instrumentor { + constructor( + private readonly includes: string[] = ["*"], + private readonly excludes: string[] = ["node_modules"], + private readonly idStrategy: EdgeIdStrategy = new MemorySyncIdStrategy() + ) {} -export function registerInstrumentor( - includes: string[], - excludes: string[], - idSyncFile: string | undefined -) { - installSourceMapSupport(); - if (includes.includes("jazzer.js")) { - unloadInternalModules(); + init(): () => void { + if (this.includes.includes("jazzer.js")) { + this.unloadInternalModules(); + } + return Instrumentor.installSourceMapSupport(); } - const idStrategy: EdgeIdStrategy = - idSyncFile !== undefined - ? new FileSyncIdStrategy(idSyncFile) - : new MemorySyncIdStrategy(); + instrument(code: string, filename: string): string { + const transformations: PluginItem[] = []; - const shouldInstrument = shouldInstrumentFn(includes, excludes); - const shouldHook = hookManager.hasFunctionsToHook.bind(hookManager); - hookRequire( - () => true, - (code: string, options: TransformerOptions): string => { - return instrument( - code, - options.filename, - shouldInstrument, - shouldHook, - idStrategy - ); + const shouldInstrumentFile = this.shouldInstrument(filename); + + if (shouldInstrumentFile) { + transformations.push(codeCoverage(this.idStrategy), compareHooks); } - ); -} -function unloadInternalModules() { - console.log( - "DEBUG: Unloading internal Jazzer.js modules for instrumentation..." - ); - [ - "@jazzer.js/core", - "@jazzer.js/fuzzer", - "@jazzer.js/hooking", - "@jazzer.js/instrumentor", - "@jazzer.js/jest-runner", - ].forEach((module) => { - delete require.cache[require.resolve(module)]; - }); -} + if (hookManager.hasFunctionsToHook(filename)) { + transformations.push(functionHooks(filename)); + } + + if (shouldInstrumentFile) { + this.idStrategy.startForSourceFile(filename); + } + + const transformedCode = + this.transform(filename, code, transformations)?.code || code; + + if (shouldInstrumentFile) { + this.idStrategy.commitIdCount(filename); + } -export function shouldInstrumentFn( - includes: string[], - excludes: string[] -): FilePredicate { - const cleanup = (settings: string[]) => - settings - .filter((setting) => setting) - .map((setting) => (setting === "*" ? "" : setting)); // empty string matches every file - const cleanedIncludes = cleanup(includes); - const cleanedExcludes = cleanup(excludes); - return (filepath: string) => { + return transformedCode; + } + + transform( + filename: string, + code: string, + plugins: PluginItem[], + options: TransformOptions = {} + ): BabelFileResult | null { + if (plugins.length === 0) { + return null; + } + const result = transformSync(code, { + filename: filename, + sourceFileName: filename, + sourceMaps: true, + plugins: plugins, + ...options, + }); + if (result?.map) { + const sourceMap = result.map; + sourceMaps[filename] = { + version: sourceMap.version.toString(), + sources: sourceMap.sources ?? [], + names: sourceMap.names, + sourcesContent: sourceMap.sourcesContent, + mappings: sourceMap.mappings, + }; + } + return result; + } + + /* Installs source-map-support handlers and returns a reset function */ + static installSourceMapSupport(): () => void { + // Use the source-map-support library to enable in-memory source maps of + // transformed code and error stack rewrites. + // As there is no way to populate the source map cache of source-map-support, + // an additional buffer is used to pass on the source maps from babel to the + // library. This could be memory intensive and should be replaced by + // tmp source map files, if it really becomes a problem. + sms.install({ + hookRequire: true, + retrieveSourceMap: (source) => { + if (sourceMaps[source]) { + return { + map: sourceMaps[source], + url: source, + }; + } + return null; + }, + }); + return sms.resetRetrieveHandlers; + } + + private unloadInternalModules() { + console.log( + "DEBUG: Unloading internal Jazzer.js modules for instrumentation..." + ); + [ + "@jazzer.js/core", + "@jazzer.js/fuzzer", + "@jazzer.js/hooking", + "@jazzer.js/instrumentor", + "@jazzer.js/jest-runner", + ].forEach((module) => { + delete require.cache[require.resolve(module)]; + }); + } + shouldInstrument(filepath: string): boolean { + const cleanup = (settings: string[]) => + settings + .filter((setting) => setting) + .map((setting) => (setting === "*" ? "" : setting)); // empty string matches every file + const cleanedIncludes = cleanup(this.includes); + const cleanedExcludes = cleanup(this.excludes); const included = cleanedIncludes.find((include) => filepath.includes(include)) !== undefined; @@ -128,63 +162,16 @@ export function shouldInstrumentFn( cleanedExcludes.find((exclude) => filepath.includes(exclude)) !== undefined; return included && !excluded; - }; -} - -function instrument( - code: string, - filename: string, - shouldInstrument: FilePredicate, - shouldHook: FilePredicate, - idStrategy: EdgeIdStrategy -) { - const transformations: PluginItem[] = []; - const shouldInstrumentFile = shouldInstrument(filename); - if (shouldInstrumentFile) { - transformations.push(codeCoverage(idStrategy), compareHooks); - } - if (shouldHook(filename)) { - transformations.push(functionHooks(filename)); } - if (shouldInstrumentFile) { - idStrategy.startForSourceFile(filename); - } - - const transformedCode = - transform(filename, code, transformations)?.code || code; - - if (shouldInstrumentFile) { - idStrategy.commitIdCount(filename); - } - - return transformedCode; } -export function transform( - filename: string, - code: string, - plugins: PluginItem[], - options: TransformOptions = {} -): BabelFileResult | null { - if (plugins.length === 0) { - return null; - } - const result = transformSync(code, { - filename: filename, - sourceFileName: filename, - sourceMaps: true, - plugins: plugins, - ...options, - }); - if (result?.map) { - const sourceMap = result.map; - sourceMaps[filename] = { - version: sourceMap.version.toString(), - sources: sourceMap.sources ?? [], - names: sourceMap.names, - sourcesContent: sourceMap.sourcesContent, - mappings: sourceMap.mappings, - }; - } - return result; +export function registerInstrumentor(instrumentor: Instrumentor) { + instrumentor.init(); + + hookRequire( + () => true, + (code: string, opts: TransformerOptions): string => { + return instrumentor.instrument(code, opts.filename); + } + ); } diff --git a/packages/instrumentor/plugins/testhelpers.ts b/packages/instrumentor/plugins/testhelpers.ts index 617fa47a..63601c84 100644 --- a/packages/instrumentor/plugins/testhelpers.ts +++ b/packages/instrumentor/plugins/testhelpers.ts @@ -15,7 +15,7 @@ */ import { PluginTarget } from "@babel/core"; -import { transform } from "../instrument"; +import { Instrumentor } from "../instrument"; export function instrumentAndEvalWith(...plugins: PluginTarget[]) { const instrument = instrumentWith(plugins); @@ -34,7 +34,8 @@ function expectInstrumentation( output: string ): string { const code = removeIndentation(input); - const result = transform("test.js", code, plugins)?.code || code; + const instrumentor = new Instrumentor(); + const result = instrumentor.transform("test.js", code, plugins)?.code || code; expect(removeIndentation(result)).toBe(removeIndentation(output)); return result; } From e36b00077c2b86058b44d924240a0e01bb1c9f4d Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Tue, 24 Jan 2023 13:10:20 +0100 Subject: [PATCH 22/22] instrumentor: add unit test for sync file ID strategy --- .../instrumentor/plugins/codeCoverage.test.ts | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/instrumentor/plugins/codeCoverage.test.ts b/packages/instrumentor/plugins/codeCoverage.test.ts index 24591325..45f5954f 100644 --- a/packages/instrumentor/plugins/codeCoverage.test.ts +++ b/packages/instrumentor/plugins/codeCoverage.test.ts @@ -16,7 +16,14 @@ import { codeCoverage } from "./codeCoverage"; import { instrumentWith } from "./testhelpers"; -import { ZeroEdgeIdStrategy } from "../edgeIdStrategy"; +import { FileSyncIdStrategy, ZeroEdgeIdStrategy } from "../edgeIdStrategy"; +import { Instrumentor } from "../instrument"; + +import * as tmp from "tmp"; +import * as fs from "fs"; +import * as os from "os"; + +tmp.setGracefulCleanup(); const expectInstrumentation = instrumentWith( codeCoverage(new ZeroEdgeIdStrategy()) @@ -160,4 +167,59 @@ describe("code coverage instrumentation", () => { expectInstrumentation(input, output); }); }); + + describe("FileSyncIdStrategy", () => { + it("should add correct number of edges", () => { + const idSyncFile = tmp.fileSync({ + mode: 0o600, + prefix: "jazzer.js", + postfix: "idSync", + }); + fs.closeSync(idSyncFile.fd); + + const testCases: { file: string; code: string }[] = [ + { + file: "foo.js", + code: "if (1 < 2) { true; } else { false; }", + }, + { + file: "bar.js", + code: "for (let i = 0; i < 100; i++) { counter++; }", + }, + { + file: "do_not_instrument.js", + code: "some invalid code to throw a SyntaxError if we try to instrument it", + }, + { + file: "baz.js", + code: "switch(a) {case 1: true; case 2: false; break; default: true;}", + }, + ]; + + const instrumentor = new Instrumentor( + ["*"], + ["do_not_instrument"], + new FileSyncIdStrategy(idSyncFile.name) + ); + + for (const testCase of testCases) { + instrumentor.instrument(testCase.code, testCase.file); + } + + for (let i = 0; i < 100; i++) { + // Randomly select a file to instrument. At this point all files should have been instrumented + // and thus instrumenting new files should not change the ID sync file. + const testCase = testCases[Math.floor(Math.random() * 4)]; + instrumentor.instrument(testCase.code, testCase.file); + } + + expect( + fs + .readFileSync(idSyncFile.name) + .toString() + .split(os.EOL) + .filter((line) => line !== "") + ).toEqual(["foo.js,0,3", "bar.js,3,2", "baz.js,5,4"]); + }); + }); });