as-test 1.1.6 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +4 -9
- package/assembly/index.ts +10 -15
- package/assembly/src/expectation.ts +11 -11
- package/assembly/src/fuzz.ts +11 -7
- package/assembly/src/log.ts +2 -2
- package/assembly/src/suite.ts +5 -5
- package/assembly/src/tests.ts +8 -8
- package/assembly/util/wipc.ts +5 -1
- package/bin/build-worker-pool.js +146 -142
- package/bin/build-worker.js +37 -34
- package/bin/commands/build-core.js +577 -465
- package/bin/commands/build.js +49 -29
- package/bin/commands/clean-core.js +120 -113
- package/bin/commands/clean.js +14 -8
- package/bin/commands/doctor-core.js +288 -289
- package/bin/commands/doctor.js +1 -1
- package/bin/commands/fuzz-core.js +467 -414
- package/bin/commands/fuzz.js +27 -10
- package/bin/commands/init-core.js +905 -794
- package/bin/commands/init.js +2 -2
- package/bin/commands/run-core.js +2675 -2344
- package/bin/commands/run.js +43 -25
- package/bin/commands/test.js +56 -32
- package/bin/commands/web-runner-source.js +1 -1
- package/bin/commands/web-session.js +516 -525
- package/bin/coverage-points.js +363 -341
- package/bin/crash-store.js +56 -66
- package/bin/index.js +4092 -3150
- package/bin/reporters/default.js +1090 -890
- package/bin/reporters/tap.js +319 -325
- package/bin/types.js +67 -67
- package/bin/util.js +1290 -1239
- package/bin/wipc.js +70 -73
- package/lib/build/index.d.ts +3 -1
- package/lib/build/index.js +1039 -1034
- package/lib/build/web-runner/client.js +1 -1
- package/lib/build/web-runner/html.js +1 -1
- package/lib/build/web-runner/worker.js +1 -1
- package/package.json +6 -3
- package/transform/lib/log.js +9 -5
- package/assembly/util/json.ts +0 -112
|
@@ -9,465 +9,518 @@ const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
|
|
|
9
9
|
const MAGIC = Buffer.from("WIPC");
|
|
10
10
|
const HEADER_SIZE = 9;
|
|
11
11
|
const MAX_DEFAULT_SEED = 0x7fffffff;
|
|
12
|
-
export async function fuzz(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
12
|
+
export async function fuzz(
|
|
13
|
+
configPath = DEFAULT_CONFIG_PATH,
|
|
14
|
+
selectors = [],
|
|
15
|
+
modeName,
|
|
16
|
+
overrides = {},
|
|
17
|
+
fuzzerSelectors = [],
|
|
18
|
+
) {
|
|
19
|
+
const loadedConfig = loadConfig(configPath, false);
|
|
20
|
+
const mode = applyMode(loadedConfig, modeName);
|
|
21
|
+
const activeConfig = mode.config;
|
|
22
|
+
const config = resolveFuzzConfig(activeConfig.fuzz, overrides);
|
|
23
|
+
const inputPatterns = resolveFuzzInputPatterns(config.input, selectors);
|
|
24
|
+
const inputFiles = (await glob(inputPatterns)).sort((a, b) =>
|
|
25
|
+
a.localeCompare(b),
|
|
26
|
+
);
|
|
27
|
+
if (!inputFiles.length) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`No fuzz files matched: ${selectors.length ? selectors.join(", ") : "configured input patterns"}`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const duplicateBasenames = resolveDuplicateBasenames(inputFiles);
|
|
33
|
+
const results = [];
|
|
34
|
+
for (const file of inputFiles) {
|
|
35
|
+
const buildStartedAt = Date.now();
|
|
36
|
+
await build(
|
|
37
|
+
configPath,
|
|
38
|
+
[file],
|
|
39
|
+
modeName,
|
|
40
|
+
{ coverage: false },
|
|
41
|
+
{ target: "bindings", args: ["--use", "AS_TEST_FUZZ=1"], kind: "fuzz" },
|
|
42
|
+
loadedConfig,
|
|
43
|
+
);
|
|
44
|
+
const buildFinishedAt = Date.now();
|
|
45
|
+
const buildTime = buildFinishedAt - buildStartedAt;
|
|
46
|
+
results.push(
|
|
47
|
+
await runFuzzTarget(
|
|
48
|
+
file,
|
|
49
|
+
activeConfig.outDir,
|
|
50
|
+
duplicateBasenames,
|
|
51
|
+
config,
|
|
52
|
+
fuzzerSelectors,
|
|
53
|
+
buildStartedAt,
|
|
54
|
+
buildFinishedAt,
|
|
55
|
+
buildTime,
|
|
56
|
+
modeName,
|
|
57
|
+
),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return results;
|
|
32
61
|
}
|
|
33
62
|
function resolveFuzzConfig(raw, overrides) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
63
|
+
const config = Object.assign({}, raw);
|
|
64
|
+
if (typeof overrides.seed == "number") {
|
|
65
|
+
config.seed = overrides.seed;
|
|
66
|
+
} else if (config.seed < 0) {
|
|
67
|
+
config.seed = generateRandomSeed();
|
|
68
|
+
}
|
|
69
|
+
if (typeof overrides.runs == "number") {
|
|
70
|
+
config.runs = overrides.runs;
|
|
71
|
+
}
|
|
72
|
+
config.runsOverrideKind = 0;
|
|
73
|
+
config.runsOverrideValue = 0;
|
|
74
|
+
if (overrides.runsOverride) {
|
|
75
|
+
config.runsOverrideKind = encodeRunsOverrideKind(
|
|
76
|
+
overrides.runsOverride.kind,
|
|
77
|
+
);
|
|
78
|
+
config.runsOverrideValue = overrides.runsOverride.value;
|
|
79
|
+
if (overrides.runsOverride.kind == "set") {
|
|
80
|
+
config.runs = Math.max(1, Math.round(overrides.runsOverride.value));
|
|
40
81
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
config.runsOverrideValue = overrides.runsOverride.value;
|
|
49
|
-
if (overrides.runsOverride.kind == "set") {
|
|
50
|
-
config.runs = Math.max(1, Math.round(overrides.runsOverride.value));
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if (config.target != "bindings") {
|
|
54
|
-
throw new Error(`fuzz target must be "bindings"; received "${config.target}"`);
|
|
55
|
-
}
|
|
56
|
-
return config;
|
|
82
|
+
}
|
|
83
|
+
if (config.target != "bindings") {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`fuzz target must be "bindings"; received "${config.target}"`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return config;
|
|
57
89
|
}
|
|
58
90
|
function generateRandomSeed() {
|
|
59
|
-
|
|
91
|
+
return Math.floor(Math.random() * (MAX_DEFAULT_SEED + 1));
|
|
60
92
|
}
|
|
61
93
|
function encodeRunsOverrideKind(kind) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
94
|
+
switch (kind) {
|
|
95
|
+
case "set":
|
|
96
|
+
return 1;
|
|
97
|
+
case "scale":
|
|
98
|
+
return 2;
|
|
99
|
+
case "add":
|
|
100
|
+
return 3;
|
|
101
|
+
case "percent-add":
|
|
102
|
+
return 4;
|
|
103
|
+
}
|
|
72
104
|
}
|
|
73
|
-
async function runFuzzTarget(
|
|
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
|
-
report = JSON.parse(payload.toString("utf8"));
|
|
126
|
-
reportParseError = null;
|
|
127
|
-
}
|
|
128
|
-
catch (error) {
|
|
129
|
-
reportParseError = String(error);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
try {
|
|
135
|
-
await helper.instantiate(module, {});
|
|
136
|
-
}
|
|
137
|
-
catch (error) {
|
|
138
|
-
const passthrough = captured.restore();
|
|
139
|
-
const crashMessage = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
140
|
-
const crash = persistCrashRecord(config.crashDir, {
|
|
141
|
-
kind: "fuzz",
|
|
142
|
-
file,
|
|
143
|
-
entryKey: buildFuzzCrashEntryKey(file, modeName ?? "default"),
|
|
144
|
-
mode: modeName ?? "default",
|
|
145
|
-
seed: config.seed,
|
|
146
|
-
error: crashMessage,
|
|
147
|
-
stdout: passthrough.stdout,
|
|
148
|
-
stderr: "",
|
|
149
|
-
});
|
|
150
|
-
return {
|
|
151
|
-
file,
|
|
152
|
-
target: path.basename(file),
|
|
153
|
-
modeName: modeName ?? "default",
|
|
154
|
-
runs: config.runs,
|
|
155
|
-
crashes: 1,
|
|
156
|
-
crashFiles: [crash.jsonPath],
|
|
157
|
-
seed: config.seed,
|
|
158
|
-
time: Date.now() - startedAt,
|
|
159
|
-
buildTime,
|
|
160
|
-
buildStartedAt,
|
|
161
|
-
buildFinishedAt,
|
|
162
|
-
fuzzers: [],
|
|
163
|
-
};
|
|
105
|
+
async function runFuzzTarget(
|
|
106
|
+
file,
|
|
107
|
+
outDir,
|
|
108
|
+
duplicateBasenames,
|
|
109
|
+
config,
|
|
110
|
+
fuzzerSelectors,
|
|
111
|
+
buildStartedAt,
|
|
112
|
+
buildFinishedAt,
|
|
113
|
+
buildTime,
|
|
114
|
+
modeName,
|
|
115
|
+
) {
|
|
116
|
+
const startedAt = Date.now();
|
|
117
|
+
const artifact = resolveArtifactFileName(file, duplicateBasenames, modeName);
|
|
118
|
+
const wasmPath = path.resolve(process.cwd(), outDir, artifact);
|
|
119
|
+
const jsPath = resolveBindingsHelperPath(wasmPath);
|
|
120
|
+
const helper = await import(pathToFileURL(jsPath).href + `?t=${Date.now()}`);
|
|
121
|
+
const binary = readFileSync(wasmPath);
|
|
122
|
+
const module = new WebAssembly.Module(binary);
|
|
123
|
+
let report = null;
|
|
124
|
+
let reportParseError = null;
|
|
125
|
+
const reportStream = {
|
|
126
|
+
sawChunkStart: false,
|
|
127
|
+
sawChunkEnd: false,
|
|
128
|
+
chunkCountExpected: 0,
|
|
129
|
+
chunkTotalBytesExpected: 0,
|
|
130
|
+
chunkFramesReceived: 0,
|
|
131
|
+
chunkBytesReceived: 0,
|
|
132
|
+
chunks: [],
|
|
133
|
+
};
|
|
134
|
+
const captured = captureFrames((type, payload, respond) => {
|
|
135
|
+
if (type == 0x02) {
|
|
136
|
+
const event = JSON.parse(payload.toString("utf8"));
|
|
137
|
+
const kind = String(event.kind ?? "");
|
|
138
|
+
if (kind == "fuzz:config") {
|
|
139
|
+
const resolved = config;
|
|
140
|
+
respond(
|
|
141
|
+
`${config.runs}\n${config.seed}\n${resolved.runsOverrideKind ?? 0}\n${resolved.runsOverrideValue ?? 0}`,
|
|
142
|
+
);
|
|
143
|
+
} else if (kind == "report:start") {
|
|
144
|
+
reportStream.sawChunkStart = true;
|
|
145
|
+
reportStream.sawChunkEnd = false;
|
|
146
|
+
reportStream.chunkCountExpected = Number(event.chunkCount ?? 0);
|
|
147
|
+
reportStream.chunkTotalBytesExpected = Number(event.totalBytes ?? 0);
|
|
148
|
+
reportStream.chunkFramesReceived = 0;
|
|
149
|
+
reportStream.chunkBytesReceived = 0;
|
|
150
|
+
reportStream.chunks = [];
|
|
151
|
+
} else if (kind == "report:end") {
|
|
152
|
+
reportStream.sawChunkEnd = true;
|
|
153
|
+
} else {
|
|
154
|
+
respond("");
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
164
157
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
158
|
+
if (type == 0x03) {
|
|
159
|
+
if (reportStream.sawChunkStart && !reportStream.sawChunkEnd) {
|
|
160
|
+
reportStream.chunkFramesReceived++;
|
|
161
|
+
reportStream.chunkBytesReceived += payload.length;
|
|
162
|
+
reportStream.chunks.push(payload.toString("utf8"));
|
|
163
|
+
} else {
|
|
164
|
+
try {
|
|
165
|
+
report = JSON.parse(payload.toString("utf8"));
|
|
166
|
+
reportParseError = null;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
reportParseError = String(error);
|
|
176
169
|
}
|
|
170
|
+
}
|
|
177
171
|
}
|
|
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
|
-
const firstFailureSeed = typeof fuzzer.failures?.[0]?.seed == "number"
|
|
221
|
-
? fuzzer.failures[0].seed
|
|
222
|
-
: config.seed;
|
|
223
|
-
const crash = persistCrashRecord(config.crashDir, {
|
|
224
|
-
kind: "fuzz",
|
|
225
|
-
file,
|
|
226
|
-
entryKey: buildFuzzFailureEntryKey(file, fuzzer.name, modeName ?? "default"),
|
|
227
|
-
mode: modeName ?? "default",
|
|
228
|
-
seed: firstFailureSeed,
|
|
229
|
-
reproCommand: buildFuzzReproCommand(file, firstFailureSeed, modeName ?? "default", fuzzer.selector, 1),
|
|
230
|
-
error: fuzzer.failure?.message ||
|
|
231
|
-
`fuzz failure in ${fuzzer.name} after ${fuzzer.runs} runs`,
|
|
232
|
-
stdout: passthrough.stdout,
|
|
233
|
-
stderr: "",
|
|
234
|
-
failure: fuzzer.failure,
|
|
235
|
-
failures: fuzzer.failures,
|
|
236
|
-
});
|
|
237
|
-
crashFiles.push(crash.jsonPath);
|
|
238
|
-
fuzzer.crashFile = crash.jsonPath;
|
|
172
|
+
});
|
|
173
|
+
try {
|
|
174
|
+
await helper.instantiate(module, {});
|
|
175
|
+
} catch (error) {
|
|
176
|
+
const passthrough = captured.restore();
|
|
177
|
+
const crashMessage =
|
|
178
|
+
error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
179
|
+
const crash = persistCrashRecord(config.crashDir, {
|
|
180
|
+
kind: "fuzz",
|
|
181
|
+
file,
|
|
182
|
+
entryKey: buildFuzzCrashEntryKey(file, modeName ?? "default"),
|
|
183
|
+
mode: modeName ?? "default",
|
|
184
|
+
seed: config.seed,
|
|
185
|
+
error: crashMessage,
|
|
186
|
+
stdout: passthrough.stdout,
|
|
187
|
+
stderr: "",
|
|
188
|
+
});
|
|
189
|
+
return {
|
|
190
|
+
file,
|
|
191
|
+
target: path.basename(file),
|
|
192
|
+
modeName: modeName ?? "default",
|
|
193
|
+
runs: config.runs,
|
|
194
|
+
crashes: 1,
|
|
195
|
+
crashFiles: [crash.jsonPath],
|
|
196
|
+
seed: config.seed,
|
|
197
|
+
time: Date.now() - startedAt,
|
|
198
|
+
buildTime,
|
|
199
|
+
buildStartedAt,
|
|
200
|
+
buildFinishedAt,
|
|
201
|
+
fuzzers: [],
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
const passthrough = captured.restore();
|
|
205
|
+
if (reportStream.sawChunkStart) {
|
|
206
|
+
if (reportStream.sawChunkEnd) {
|
|
207
|
+
const chunkedPayload = reportStream.chunks.join("");
|
|
208
|
+
try {
|
|
209
|
+
report = JSON.parse(chunkedPayload);
|
|
210
|
+
reportParseError = null;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
reportParseError = String(error);
|
|
213
|
+
}
|
|
239
214
|
}
|
|
215
|
+
}
|
|
216
|
+
if (!report?.fuzzers) {
|
|
217
|
+
const diagnostics = [
|
|
218
|
+
`chunked=${reportStream.sawChunkStart ? "yes" : "no"}`,
|
|
219
|
+
`chunkStart=${reportStream.sawChunkStart ? "yes" : "no"}`,
|
|
220
|
+
`chunkEnd=${reportStream.sawChunkEnd ? "yes" : "no"}`,
|
|
221
|
+
`chunkFrames=${reportStream.chunkFramesReceived}`,
|
|
222
|
+
`expectedChunkFrames=${reportStream.chunkCountExpected}`,
|
|
223
|
+
`chunkBytes=${reportStream.chunkBytesReceived}`,
|
|
224
|
+
`expectedChunkBytes=${reportStream.chunkTotalBytesExpected}`,
|
|
225
|
+
].join(", ");
|
|
226
|
+
const crash = persistCrashRecord(config.crashDir, {
|
|
227
|
+
kind: "fuzz",
|
|
228
|
+
file,
|
|
229
|
+
entryKey: buildFuzzCrashEntryKey(file, modeName ?? "default"),
|
|
230
|
+
mode: modeName ?? "default",
|
|
231
|
+
seed: config.seed,
|
|
232
|
+
error: `${reportParseError ? `invalid fuzz report payload: ${reportParseError}` : `missing fuzz report payload from ${path.basename(file)}`} (${diagnostics})`,
|
|
233
|
+
stdout: passthrough.stdout,
|
|
234
|
+
stderr: "",
|
|
235
|
+
});
|
|
240
236
|
return {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
237
|
+
file,
|
|
238
|
+
target: path.basename(file),
|
|
239
|
+
modeName: modeName ?? "default",
|
|
240
|
+
runs: config.runs,
|
|
241
|
+
crashes: 1,
|
|
242
|
+
crashFiles: [crash.jsonPath],
|
|
243
|
+
seed: config.seed,
|
|
244
|
+
time: Date.now() - startedAt,
|
|
245
|
+
buildTime,
|
|
246
|
+
buildStartedAt,
|
|
247
|
+
buildFinishedAt,
|
|
248
|
+
fuzzers: [],
|
|
253
249
|
};
|
|
250
|
+
}
|
|
251
|
+
const crashFiles = [];
|
|
252
|
+
const selectedFuzzers = fuzzerSelectors.length
|
|
253
|
+
? filterSelectedFuzzers(report.fuzzers, fuzzerSelectors, file)
|
|
254
|
+
: report.fuzzers;
|
|
255
|
+
for (const fuzzer of selectedFuzzers) {
|
|
256
|
+
if (fuzzer.failed <= 0 && fuzzer.crashed <= 0) continue;
|
|
257
|
+
const firstFailureSeed =
|
|
258
|
+
typeof fuzzer.failures?.[0]?.seed == "number"
|
|
259
|
+
? fuzzer.failures[0].seed
|
|
260
|
+
: config.seed;
|
|
261
|
+
const crash = persistCrashRecord(config.crashDir, {
|
|
262
|
+
kind: "fuzz",
|
|
263
|
+
file,
|
|
264
|
+
entryKey: buildFuzzFailureEntryKey(
|
|
265
|
+
file,
|
|
266
|
+
fuzzer.name,
|
|
267
|
+
modeName ?? "default",
|
|
268
|
+
),
|
|
269
|
+
mode: modeName ?? "default",
|
|
270
|
+
seed: firstFailureSeed,
|
|
271
|
+
reproCommand: buildFuzzReproCommand(
|
|
272
|
+
file,
|
|
273
|
+
firstFailureSeed,
|
|
274
|
+
modeName ?? "default",
|
|
275
|
+
fuzzer.selector,
|
|
276
|
+
1,
|
|
277
|
+
),
|
|
278
|
+
error:
|
|
279
|
+
fuzzer.failure?.message ||
|
|
280
|
+
`fuzz failure in ${fuzzer.name} after ${fuzzer.runs} runs`,
|
|
281
|
+
stdout: passthrough.stdout,
|
|
282
|
+
stderr: "",
|
|
283
|
+
failure: fuzzer.failure,
|
|
284
|
+
failures: fuzzer.failures,
|
|
285
|
+
});
|
|
286
|
+
crashFiles.push(crash.jsonPath);
|
|
287
|
+
fuzzer.crashFile = crash.jsonPath;
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
file,
|
|
291
|
+
target: path.basename(file),
|
|
292
|
+
modeName: modeName ?? "default",
|
|
293
|
+
runs: selectedFuzzers.reduce((sum, item) => sum + item.runs, 0),
|
|
294
|
+
crashes: selectedFuzzers.reduce((sum, item) => sum + item.crashed, 0),
|
|
295
|
+
crashFiles,
|
|
296
|
+
seed: config.seed,
|
|
297
|
+
time: Date.now() - startedAt,
|
|
298
|
+
buildTime,
|
|
299
|
+
buildStartedAt,
|
|
300
|
+
buildFinishedAt,
|
|
301
|
+
fuzzers: selectedFuzzers,
|
|
302
|
+
};
|
|
254
303
|
}
|
|
255
304
|
function filterSelectedFuzzers(fuzzers, selectors, file) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
for (const match of matches) {
|
|
270
|
-
selected.add(match.selector);
|
|
271
|
-
}
|
|
305
|
+
const annotated = fuzzers.map((fuzzer) => ({
|
|
306
|
+
...fuzzer,
|
|
307
|
+
selector: slugifyFuzzerSelector(fuzzer.name),
|
|
308
|
+
}));
|
|
309
|
+
const selected = new Set();
|
|
310
|
+
for (const selector of selectors) {
|
|
311
|
+
const slug = slugifyFuzzerSelector(selector);
|
|
312
|
+
if (!slug.length) continue;
|
|
313
|
+
const matches = annotated.filter((fuzzer) => fuzzer.selector == slug);
|
|
314
|
+
if (!matches.length) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
`No fuzz targets matched "${selector}" in ${path.basename(file)}.`,
|
|
317
|
+
);
|
|
272
318
|
}
|
|
273
|
-
|
|
319
|
+
for (const match of matches) {
|
|
320
|
+
selected.add(match.selector);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return annotated.filter((fuzzer) => selected.has(fuzzer.selector ?? ""));
|
|
274
324
|
}
|
|
275
325
|
function slugifyFuzzerSelector(value) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
326
|
+
return value
|
|
327
|
+
.trim()
|
|
328
|
+
.toLowerCase()
|
|
329
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
330
|
+
.replace(/^-+|-+$/g, "");
|
|
281
331
|
}
|
|
282
332
|
function buildFuzzReproCommand(file, seed, modeName, fuzzer, runs) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
333
|
+
const modeArg = modeName != "default" ? ` --mode ${modeName}` : "";
|
|
334
|
+
const fuzzerArg = fuzzer?.length ? ` --fuzzer ${fuzzer}` : "";
|
|
335
|
+
const runsArg = typeof runs == "number" ? ` --runs ${runs}` : "";
|
|
336
|
+
return `ast fuzz ${file}${modeArg}${fuzzerArg} --seed ${seed}${runsArg}`;
|
|
287
337
|
}
|
|
288
338
|
function buildFuzzFailureEntryKey(file, name, modeName) {
|
|
289
|
-
|
|
339
|
+
return `${path.basename(file).replace(/\.ts$/, "")}.${sanitizeEntryName(modeName)}.${sanitizeEntryName(name)}`;
|
|
290
340
|
}
|
|
291
341
|
function buildFuzzCrashEntryKey(file, modeName) {
|
|
292
|
-
|
|
342
|
+
return `${path.basename(file).replace(/\.ts$/, "")}.${sanitizeEntryName(modeName)}`;
|
|
293
343
|
}
|
|
294
344
|
function sanitizeEntryName(name) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
345
|
+
return (
|
|
346
|
+
name
|
|
347
|
+
.toLowerCase()
|
|
348
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
349
|
+
.replace(/^-+|-+$/g, "") || "fuzzer"
|
|
350
|
+
);
|
|
299
351
|
}
|
|
300
352
|
function captureFrames(onFrame) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
353
|
+
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
354
|
+
const originalRead =
|
|
355
|
+
typeof process.stdin.read == "function"
|
|
356
|
+
? process.stdin.read.bind(process.stdin)
|
|
357
|
+
: null;
|
|
358
|
+
let buffer = Buffer.alloc(0);
|
|
359
|
+
let passthrough = Buffer.alloc(0);
|
|
360
|
+
let replies = Buffer.alloc(0);
|
|
361
|
+
function encodeReply(body) {
|
|
362
|
+
const payload = Buffer.from(body, "utf8");
|
|
363
|
+
const header = Buffer.alloc(HEADER_SIZE);
|
|
364
|
+
MAGIC.copy(header, 0);
|
|
365
|
+
header.writeUInt8(0x02, 4);
|
|
366
|
+
header.writeUInt32LE(payload.length, 5);
|
|
367
|
+
return Buffer.concat([header, payload]);
|
|
368
|
+
}
|
|
369
|
+
function dequeueReply(length) {
|
|
370
|
+
const available = Math.min(length, replies.length);
|
|
371
|
+
const view = replies.subarray(0, available);
|
|
372
|
+
replies = replies.subarray(available);
|
|
373
|
+
return view.buffer.slice(
|
|
374
|
+
view.byteOffset,
|
|
375
|
+
view.byteOffset + view.byteLength,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
process.stdout.write = (chunk, ...args) => {
|
|
379
|
+
if (!(chunk instanceof ArrayBuffer) && !Buffer.isBuffer(chunk)) {
|
|
380
|
+
return originalWrite(chunk, ...args);
|
|
321
381
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (buffer.length) {
|
|
332
|
-
passthrough = Buffer.concat([passthrough, buffer]);
|
|
333
|
-
originalWrite(buffer);
|
|
334
|
-
buffer = Buffer.alloc(0);
|
|
335
|
-
}
|
|
336
|
-
return true;
|
|
337
|
-
}
|
|
338
|
-
if (index > 0) {
|
|
339
|
-
const raw = buffer.subarray(0, index);
|
|
340
|
-
passthrough = Buffer.concat([passthrough, raw]);
|
|
341
|
-
originalWrite(raw);
|
|
342
|
-
buffer = buffer.subarray(index);
|
|
343
|
-
}
|
|
344
|
-
if (buffer.length < HEADER_SIZE)
|
|
345
|
-
return true;
|
|
346
|
-
const type = buffer.readUInt8(4);
|
|
347
|
-
const length = buffer.readUInt32LE(5);
|
|
348
|
-
const frameSize = HEADER_SIZE + length;
|
|
349
|
-
if (buffer.length < frameSize)
|
|
350
|
-
return true;
|
|
351
|
-
const payload = buffer.subarray(HEADER_SIZE, frameSize);
|
|
352
|
-
buffer = buffer.subarray(frameSize);
|
|
353
|
-
onFrame(type, payload, (body) => {
|
|
354
|
-
replies = Buffer.concat([replies, encodeReply(body)]);
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
process.stdin.read = ((size) => {
|
|
359
|
-
const max = size == null ? 0 : Number(size);
|
|
360
|
-
if (max > 0 && replies.length) {
|
|
361
|
-
return dequeueReply(max);
|
|
362
|
-
}
|
|
363
|
-
if (originalRead) {
|
|
364
|
-
return originalRead(size === null ? undefined : size);
|
|
382
|
+
const incoming = Buffer.from(chunk);
|
|
383
|
+
buffer = Buffer.concat([buffer, incoming]);
|
|
384
|
+
while (true) {
|
|
385
|
+
const index = buffer.indexOf(MAGIC);
|
|
386
|
+
if (index == -1) {
|
|
387
|
+
if (buffer.length) {
|
|
388
|
+
passthrough = Buffer.concat([passthrough, buffer]);
|
|
389
|
+
originalWrite(buffer);
|
|
390
|
+
buffer = Buffer.alloc(0);
|
|
365
391
|
}
|
|
366
|
-
return
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
if (index > 0) {
|
|
395
|
+
const raw = buffer.subarray(0, index);
|
|
396
|
+
passthrough = Buffer.concat([passthrough, raw]);
|
|
397
|
+
originalWrite(raw);
|
|
398
|
+
buffer = buffer.subarray(index);
|
|
399
|
+
}
|
|
400
|
+
if (buffer.length < HEADER_SIZE) return true;
|
|
401
|
+
const type = buffer.readUInt8(4);
|
|
402
|
+
const length = buffer.readUInt32LE(5);
|
|
403
|
+
const frameSize = HEADER_SIZE + length;
|
|
404
|
+
if (buffer.length < frameSize) return true;
|
|
405
|
+
const payload = buffer.subarray(HEADER_SIZE, frameSize);
|
|
406
|
+
buffer = buffer.subarray(frameSize);
|
|
407
|
+
onFrame(type, payload, (body) => {
|
|
408
|
+
replies = Buffer.concat([replies, encodeReply(body)]);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
process.stdin.read = (size) => {
|
|
413
|
+
const max = size == null ? 0 : Number(size);
|
|
414
|
+
if (max > 0 && replies.length) {
|
|
415
|
+
return dequeueReply(max);
|
|
416
|
+
}
|
|
417
|
+
if (originalRead) {
|
|
418
|
+
return originalRead(size === null ? undefined : size);
|
|
419
|
+
}
|
|
420
|
+
return null;
|
|
421
|
+
};
|
|
422
|
+
return {
|
|
423
|
+
restore() {
|
|
424
|
+
process.stdout.write = originalWrite;
|
|
425
|
+
if (originalRead) {
|
|
426
|
+
process.stdin.read = originalRead;
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
stdout: passthrough.toString("utf8"),
|
|
430
|
+
};
|
|
431
|
+
},
|
|
432
|
+
};
|
|
379
433
|
}
|
|
380
434
|
function resolveFuzzInputPatterns(configured, selectors) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
patterns.add(selector);
|
|
435
|
+
const configuredInputs = Array.isArray(configured)
|
|
436
|
+
? configured
|
|
437
|
+
: [configured];
|
|
438
|
+
if (!selectors.length) return configuredInputs;
|
|
439
|
+
const patterns = new Set();
|
|
440
|
+
for (const selector of expandSelectors(selectors)) {
|
|
441
|
+
if (!selector) continue;
|
|
442
|
+
if (isBareSelector(selector)) {
|
|
443
|
+
const base = selector.replace(/\.fuzz\.ts$/, "").replace(/\.ts$/, "");
|
|
444
|
+
for (const configuredInput of configuredInputs) {
|
|
445
|
+
patterns.add(
|
|
446
|
+
path.join(path.dirname(configuredInput), `${base}.fuzz.ts`),
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
continue;
|
|
398
450
|
}
|
|
399
|
-
|
|
451
|
+
patterns.add(selector);
|
|
452
|
+
}
|
|
453
|
+
return [...patterns];
|
|
400
454
|
}
|
|
401
455
|
function resolveArtifactFileName(file, duplicateBasenames, modeName) {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
456
|
+
const base = path
|
|
457
|
+
.basename(file)
|
|
458
|
+
.replace(/\.spec\.ts$/, "")
|
|
459
|
+
.replace(/\.ts$/, "");
|
|
460
|
+
const legacy = !modeName
|
|
461
|
+
? `${path.basename(file).replace(".ts", ".wasm")}`
|
|
462
|
+
: `${base}.${modeName}.bindings.wasm`;
|
|
463
|
+
if (!duplicateBasenames.has(path.basename(file))) {
|
|
464
|
+
return legacy;
|
|
465
|
+
}
|
|
466
|
+
const disambiguator = resolveDisambiguator(file);
|
|
467
|
+
if (!disambiguator.length) {
|
|
468
|
+
return legacy;
|
|
469
|
+
}
|
|
470
|
+
const ext = path.extname(legacy);
|
|
471
|
+
const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
|
|
472
|
+
return `${stem}.${disambiguator}${ext}`;
|
|
419
473
|
}
|
|
420
474
|
function resolveDuplicateBasenames(files) {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
return duplicates;
|
|
475
|
+
const counts = new Map();
|
|
476
|
+
for (const file of files) {
|
|
477
|
+
const base = path.basename(file);
|
|
478
|
+
counts.set(base, (counts.get(base) ?? 0) + 1);
|
|
479
|
+
}
|
|
480
|
+
const duplicates = new Set();
|
|
481
|
+
for (const [base, count] of counts) {
|
|
482
|
+
if (count > 1) duplicates.add(base);
|
|
483
|
+
}
|
|
484
|
+
return duplicates;
|
|
432
485
|
}
|
|
433
486
|
function resolveDisambiguator(file) {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
.replace(/^_+|_+$/g, "");
|
|
487
|
+
const relDir = path.dirname(path.relative(process.cwd(), file));
|
|
488
|
+
if (!relDir.length || relDir == ".") return "";
|
|
489
|
+
return relDir
|
|
490
|
+
.replace(/[\\/]+/g, "__")
|
|
491
|
+
.replace(/[^A-Za-z0-9._-]/g, "_")
|
|
492
|
+
.replace(/^_+|_+$/g, "");
|
|
441
493
|
}
|
|
442
494
|
function resolveBindingsHelperPath(wasmPath) {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
return directPath;
|
|
449
|
-
return bindingsPath;
|
|
495
|
+
const bindingsPath = wasmPath.replace(/\.wasm$/, ".bindings.js");
|
|
496
|
+
if (existsSync(bindingsPath)) return bindingsPath;
|
|
497
|
+
const directPath = wasmPath.replace(/\.wasm$/, ".js");
|
|
498
|
+
if (existsSync(directPath)) return directPath;
|
|
499
|
+
return bindingsPath;
|
|
450
500
|
}
|
|
451
501
|
function expandSelectors(selectors) {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
expanded.push(selector);
|
|
502
|
+
const expanded = [];
|
|
503
|
+
for (const selector of selectors) {
|
|
504
|
+
if (
|
|
505
|
+
selector.includes(",") &&
|
|
506
|
+
!selector.includes("/") &&
|
|
507
|
+
!selector.includes("\\") &&
|
|
508
|
+
!/[*?[\]{}]/.test(selector)
|
|
509
|
+
) {
|
|
510
|
+
for (const token of selector.split(",")) {
|
|
511
|
+
const trimmed = token.trim();
|
|
512
|
+
if (trimmed.length) expanded.push(trimmed);
|
|
513
|
+
}
|
|
514
|
+
continue;
|
|
466
515
|
}
|
|
467
|
-
|
|
516
|
+
expanded.push(selector);
|
|
517
|
+
}
|
|
518
|
+
return expanded;
|
|
468
519
|
}
|
|
469
520
|
function isBareSelector(selector) {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
521
|
+
return (
|
|
522
|
+
!selector.includes("/") &&
|
|
523
|
+
!selector.includes("\\") &&
|
|
524
|
+
!/[*?[\]{}]/.test(selector)
|
|
525
|
+
);
|
|
473
526
|
}
|