-
Notifications
You must be signed in to change notification settings - Fork 20
/
options.ts
355 lines (324 loc) · 11.1 KB
/
options.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
/*
* 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 fs from "fs";
import * as tmp from "tmp";
import { useDictionaryByParams } from "./dictionary";
import { replaceAll } from "./utils";
/**
* Jazzer.js options structure expected by the fuzzer.
*
* Entry functions, like the CLI or test framework integrations, need to build
* this structure and should use the same property names for exposing their own
* options.
*/
export interface Options {
// `fuzzTarget` is the name of a module exporting the fuzz function `fuzzEntryPoint`.
fuzzTarget: string;
// Name of the function that is called by the fuzzer exported by `fuzzTarget`.
fuzzEntryPoint: string;
// Part of filepath names to include in the instrumentation.
includes: string[];
// Part of filepath names to exclude in the instrumentation.
excludes: string[];
// Whether to add fuzzing instrumentation or not.
dryRun: boolean;
// Whether to run the fuzzer in sync mode or not.
sync: boolean;
// Options to pass on to the underlying fuzzing engine.
fuzzerOptions: string[];
// Files to load that contain custom hooks.
customHooks: string[];
// Expected error name that won't trigger the fuzzer to stop with an error exit code.
expectedErrors: string[];
// Timeout for one fuzzing iteration in milliseconds.
timeout: number;
// Internal: File to sync coverage IDs in fork mode.
idSyncFile?: string;
// Enable source code coverage report generation.
coverage: boolean;
// Directory to write coverage reports to.
coverageDirectory: string;
// Coverage reporters to use during report generation.
coverageReporters: string[];
// Disable bug detectors by name.
disableBugDetectors: string[];
// Fuzzing mode.
mode: "fuzzing" | "regression";
// Verbose logging.
verbose?: boolean;
}
export const defaultOptions: Options = Object.freeze({
fuzzTarget: "",
fuzzEntryPoint: "fuzz",
includes: ["*"],
excludes: ["node_modules"],
dryRun: false,
sync: false,
fuzzerOptions: [],
customHooks: [],
expectedErrors: [],
timeout: 5000, // default Jest timeout
idSyncFile: "",
coverage: false,
coverageDirectory: "coverage",
coverageReporters: ["json", "text", "lcov", "clover"], // default Jest reporters
disableBugDetectors: [],
mode: "fuzzing",
verbose: false,
});
export type KeyFormatSource = (key: string) => string;
export const fromCamelCase: KeyFormatSource = (key: string): string => key;
export const fromSnakeCase: KeyFormatSource = (key: string): string => {
return replaceAll(key.toLowerCase(), /(_[a-z0-9])/g, (group) =>
group.toUpperCase().replace("_", ""),
);
};
export const fromSnakeCaseWithPrefix: (prefix: string) => KeyFormatSource = (
prefix: string,
): KeyFormatSource => {
const prefixKey = prefix.toLowerCase() + "_";
return (key: string): string => {
return key.toLowerCase().startsWith(prefixKey)
? fromSnakeCase(key.substring(prefixKey.length))
: key;
};
};
// Parameters can be passed in via environment variables, command line or
// configuration file, and subsequently overwrite the default ones and each other.
// The passed in values have to be set for externally provided parameters, e.g.
// CLI parameters, before resolving the final options object.
// Higher index means higher priority.
export enum ParameterResolverIndex {
DefaultOptions = 1,
ConfigurationFile,
EnvironmentVariables,
CommandLineArguments,
}
type ParameterResolver = {
name: string;
transformKey: KeyFormatSource;
failOnUnknown: boolean;
parameters: object;
};
type ParameterResolvers = Record<ParameterResolverIndex, ParameterResolver>;
const defaultResolvers: ParameterResolvers = {
[ParameterResolverIndex.DefaultOptions]: {
name: "Default options",
transformKey: fromCamelCase,
failOnUnknown: true,
parameters: defaultOptions,
},
[ParameterResolverIndex.ConfigurationFile]: {
name: "Configuration file",
transformKey: fromCamelCase,
failOnUnknown: true,
parameters: {},
},
[ParameterResolverIndex.EnvironmentVariables]: {
name: "Environment variables",
transformKey: fromSnakeCaseWithPrefix("JAZZER"),
failOnUnknown: false,
parameters: process.env as object,
},
[ParameterResolverIndex.CommandLineArguments]: {
name: "Command line arguments",
transformKey: fromSnakeCase,
failOnUnknown: true,
parameters: {},
},
};
/**
* Set the value object of a parameter resolver. Every resolver expects value
* object parameter names in a specific format, e.g. camel case or snake case,
* see the resolver definitions for details.
*/
export function setParameterResolverValue(
index: ParameterResolverIndex,
inputs: Partial<Options>,
) {
// Includes and excludes must be set together.
if (inputs && inputs.includes && !inputs.excludes) {
inputs.excludes = [];
} else if (inputs && inputs.excludes && !inputs.includes) {
inputs.includes = [];
}
defaultResolvers[index].parameters = inputs;
}
/**
* Build a complete `Option` object based on the parameter resolver chain.
* Add externally passed in values via the `setParameterResolverValue` function,
* before calling `buildOptions`.
*/
export function buildOptions(): Options {
const options = Object.keys(defaultResolvers)
.sort() // Don't presume an ordered object, this could be implementation specific.
.reduce<Options>((accumulator, currentValue) => {
const resolver =
defaultResolvers[parseInt(currentValue) as ParameterResolverIndex];
return mergeOptions(
resolver.parameters,
accumulator,
resolver.transformKey,
resolver.failOnUnknown,
);
}, defaultResolvers[ParameterResolverIndex.DefaultOptions].parameters as Options);
// Set verbose mode environment variable via option or node DEBUG environment variable.
if (options.verbose || process.env.DEBUG) {
process.env.JAZZER_DEBUG = "1";
}
return options;
}
function mergeOptions(
input: unknown,
defaults: Options,
transformKey: (key: string) => string,
errorOnUnknown = true,
): Options {
// Deep close the default options to avoid mutation.
const options: Options = JSON.parse(JSON.stringify(defaults));
if (!options || !input || typeof input !== "object") {
return options;
}
Object.keys(input as object).forEach((key) => {
const transformedKey = transformKey(key);
// Use hasOwnProperty to still support node v14.
// eslint-disable-next-line no-prototype-builtins
if (!(options as object).hasOwnProperty(transformedKey)) {
if (errorOnUnknown) {
throw new Error(`Unknown Jazzer.js option '${key}'`);
}
return;
}
// No way to dynamically resolve the types here, use (implicit) any for now.
// @ts-ignore
let resultValue = input[key];
// Try to parse strings as JSON values to support setting arrays and
// objects via environment variables.
if (typeof resultValue === "string" || resultValue instanceof String) {
try {
resultValue = JSON.parse(resultValue.toString());
} catch (ignore) {
// Ignore parsing errors and continue with the string value.
}
}
//@ts-ignore
const keyType = typeof options[transformedKey];
if (typeof resultValue !== keyType) {
// @ts-ignore
throw new Error(
`Invalid type for Jazzer.js option '${key}', expected type '${keyType}'`,
);
}
// Deep clone value to avoid reference keeping and unintended mutations.
// @ts-ignore
options[transformedKey] = JSON.parse(JSON.stringify(resultValue));
});
return options;
}
export function buildFuzzerOption(options: Options) {
if (process.env.JAZZER_DEBUG) {
console.debug("DEBUG: [core] Jazzer.js initial fuzzer arguments: ");
console.debug(options);
}
let params: string[] = [];
params = optionDependentParams(options, params);
params = forkedExecutionParams(params);
params = useDictionaryByParams(params);
// libFuzzer has to ignore SIGINT and SIGTERM, as it interferes
// with the Node.js signal handling.
params = params.concat("-handle_int=0", "-handle_term=0", "-handle_segv=0");
if (process.env.JAZZER_DEBUG) {
console.debug("DEBUG: [core] Jazzer.js actually used fuzzer arguments: ");
console.debug(params);
}
logInfoAboutFuzzerOptions(params);
return params;
}
function logInfoAboutFuzzerOptions(fuzzerOptions: string[]) {
fuzzerOptions.slice(1).forEach((element) => {
if (element.length > 0 && element[0] != "-") {
console.error("INFO: using inputs from:", element);
}
});
}
function optionDependentParams(options: Options, params: string[]): string[] {
if (!options || !options.fuzzerOptions) {
return params;
}
let opts = options.fuzzerOptions;
if (options.mode === "regression") {
// The last provided option takes precedence
opts = opts.concat("-runs=0");
}
if (options.timeout <= 0) {
throw new Error("timeout must be > 0");
}
const inSeconds = Math.ceil(options.timeout / 1000);
opts = opts.concat(`-timeout=${inSeconds}`);
return opts;
}
function forkedExecutionParams(params: string[]): string[] {
return [prepareLibFuzzerArg0(params), ...params];
}
function prepareLibFuzzerArg0(fuzzerOptions: string[]): string {
// When we run in a libFuzzer mode that spawns subprocesses, we create a wrapper script
// that can be used as libFuzzer's argv[0]. In the fork mode, the main libFuzzer process
// uses argv[0] to spawn further processes that perform the actual fuzzing.
if (!spawnsSubprocess(fuzzerOptions)) {
// Return a fake argv[0] to start the fuzzer if libFuzzer does not spawn new processes.
return "unused_arg0_report_a_bug_if_you_see_this";
} else {
// Create a wrapper script and return its path.
return createWrapperScript(fuzzerOptions);
}
}
// These flags cause libFuzzer to spawn subprocesses.
const SUBPROCESS_FLAGS = ["fork", "jobs", "merge", "minimize_crash"];
export function spawnsSubprocess(fuzzerOptions: string[]): boolean {
return fuzzerOptions.some((option) =>
SUBPROCESS_FLAGS.some((flag) => {
const name = `-${flag}=`;
return option.startsWith(name) && !option.startsWith("0", name.length);
}),
);
}
function createWrapperScript(fuzzerOptions: string[]) {
const jazzerArgs = process.argv.filter(
(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"}
cd "${process.cwd()}"
${jazzerArgs.map((s) => '"' + s + '"').join(" ")} -- ${isWindows ? "%*" : "$@"}
`;
const scriptTempFile = tmp.fileSync({
mode: 0o700,
prefix: "jazzer.js",
postfix: "libfuzzer" + (isWindows ? ".bat" : ".sh"),
});
fs.writeFileSync(scriptTempFile.name, scriptContent);
fs.closeSync(scriptTempFile.fd);
return scriptTempFile.name;
}