as-test 1.0.11 → 1.0.13
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 +9 -1
- package/README.md +15 -0
- package/as-test.config.schema.json +1 -3
- package/assembly/__fuzz__/seed-perf.fuzz.ts +68 -0
- package/assembly/src/fuzz.ts +156 -55
- package/assembly/util/wipc.ts +30 -1
- package/bin/commands/fuzz-core.js +110 -15
- package/bin/commands/fuzz.js +2 -1
- package/bin/commands/init-core.js +0 -1
- package/bin/commands/run-core.js +321 -7
- package/bin/commands/run.js +2 -1
- package/bin/commands/test.js +3 -1
- package/bin/index.js +102 -29
- package/bin/reporters/default.js +61 -52
- package/bin/types.js +1 -1
- package/bin/util.js +2 -2
- package/package.json +10 -8
- package/transform/lib/builder.js +14 -15
- package/transform/lib/coverage.js +11 -12
- package/transform/lib/index.js +0 -1
- package/transform/lib/linker.js +3 -4
- package/transform/lib/location.js +0 -1
- package/transform/lib/log.js +0 -1
- package/transform/lib/mock.js +15 -9
- package/transform/lib/range.js +0 -1
- package/transform/lib/types.js +0 -1
- package/transform/lib/util.js +0 -1
- package/transform/lib/visitor.js +64 -65
|
@@ -8,7 +8,8 @@ import { persistCrashRecord } from "../crash-store.js";
|
|
|
8
8
|
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(configPath = DEFAULT_CONFIG_PATH, selectors = [], modeName, overrides = {}, fuzzerSelectors = []) {
|
|
12
13
|
const loadedConfig = loadConfig(configPath, false);
|
|
13
14
|
const mode = applyMode(loadedConfig, modeName);
|
|
14
15
|
const config = resolveFuzzConfig(loadedConfig.fuzz, overrides);
|
|
@@ -24,7 +25,7 @@ export async function fuzz(configPath = DEFAULT_CONFIG_PATH, selectors = [], mod
|
|
|
24
25
|
await build(configPath, [file], modeName, { coverage: false }, { target: "bindings", args: ["--use", "AS_TEST_FUZZ=1"], kind: "fuzz" });
|
|
25
26
|
const buildFinishedAt = Date.now();
|
|
26
27
|
const buildTime = buildFinishedAt - buildStartedAt;
|
|
27
|
-
results.push(await runFuzzTarget(file, mode.config.outDir, duplicateBasenames, config, buildStartedAt, buildFinishedAt, buildTime, modeName));
|
|
28
|
+
results.push(await runFuzzTarget(file, mode.config.outDir, duplicateBasenames, config, fuzzerSelectors, buildStartedAt, buildFinishedAt, buildTime, modeName));
|
|
28
29
|
}
|
|
29
30
|
return results;
|
|
30
31
|
}
|
|
@@ -33,6 +34,9 @@ function resolveFuzzConfig(raw, overrides) {
|
|
|
33
34
|
if (typeof overrides.seed == "number") {
|
|
34
35
|
config.seed = overrides.seed;
|
|
35
36
|
}
|
|
37
|
+
else if (config.seed < 0) {
|
|
38
|
+
config.seed = generateRandomSeed();
|
|
39
|
+
}
|
|
36
40
|
if (typeof overrides.runs == "number") {
|
|
37
41
|
config.runs = overrides.runs;
|
|
38
42
|
}
|
|
@@ -50,6 +54,9 @@ function resolveFuzzConfig(raw, overrides) {
|
|
|
50
54
|
}
|
|
51
55
|
return config;
|
|
52
56
|
}
|
|
57
|
+
function generateRandomSeed() {
|
|
58
|
+
return Math.floor(Math.random() * (MAX_DEFAULT_SEED + 1));
|
|
59
|
+
}
|
|
53
60
|
function encodeRunsOverrideKind(kind) {
|
|
54
61
|
switch (kind) {
|
|
55
62
|
case "set":
|
|
@@ -62,7 +69,7 @@ function encodeRunsOverrideKind(kind) {
|
|
|
62
69
|
return 4;
|
|
63
70
|
}
|
|
64
71
|
}
|
|
65
|
-
async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStartedAt, buildFinishedAt, buildTime, modeName) {
|
|
72
|
+
async function runFuzzTarget(file, outDir, duplicateBasenames, config, fuzzerSelectors, buildStartedAt, buildFinishedAt, buildTime, modeName) {
|
|
66
73
|
const startedAt = Date.now();
|
|
67
74
|
const artifact = resolveArtifactFileName(file, duplicateBasenames, modeName);
|
|
68
75
|
const wasmPath = path.resolve(process.cwd(), outDir, artifact);
|
|
@@ -71,20 +78,56 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStar
|
|
|
71
78
|
const binary = readFileSync(wasmPath);
|
|
72
79
|
const module = new WebAssembly.Module(binary);
|
|
73
80
|
let report = null;
|
|
81
|
+
let reportParseError = null;
|
|
82
|
+
const reportStream = {
|
|
83
|
+
sawChunkStart: false,
|
|
84
|
+
sawChunkEnd: false,
|
|
85
|
+
chunkCountExpected: 0,
|
|
86
|
+
chunkTotalBytesExpected: 0,
|
|
87
|
+
chunkFramesReceived: 0,
|
|
88
|
+
chunkBytesReceived: 0,
|
|
89
|
+
chunks: [],
|
|
90
|
+
};
|
|
74
91
|
const captured = captureFrames((type, payload, respond) => {
|
|
75
92
|
if (type == 0x02) {
|
|
76
93
|
const event = JSON.parse(payload.toString("utf8"));
|
|
77
|
-
|
|
94
|
+
const kind = String(event.kind ?? "");
|
|
95
|
+
if (kind == "fuzz:config") {
|
|
78
96
|
const resolved = config;
|
|
79
97
|
respond(`${config.runs}\n${config.seed}\n${resolved.runsOverrideKind ?? 0}\n${resolved.runsOverrideValue ?? 0}`);
|
|
80
98
|
}
|
|
99
|
+
else if (kind == "report:start") {
|
|
100
|
+
reportStream.sawChunkStart = true;
|
|
101
|
+
reportStream.sawChunkEnd = false;
|
|
102
|
+
reportStream.chunkCountExpected = Number(event.chunkCount ?? 0);
|
|
103
|
+
reportStream.chunkTotalBytesExpected = Number(event.totalBytes ?? 0);
|
|
104
|
+
reportStream.chunkFramesReceived = 0;
|
|
105
|
+
reportStream.chunkBytesReceived = 0;
|
|
106
|
+
reportStream.chunks = [];
|
|
107
|
+
}
|
|
108
|
+
else if (kind == "report:end") {
|
|
109
|
+
reportStream.sawChunkEnd = true;
|
|
110
|
+
}
|
|
81
111
|
else {
|
|
82
112
|
respond("");
|
|
83
113
|
}
|
|
84
114
|
return;
|
|
85
115
|
}
|
|
86
116
|
if (type == 0x03) {
|
|
87
|
-
|
|
117
|
+
if (reportStream.sawChunkStart && !reportStream.sawChunkEnd) {
|
|
118
|
+
reportStream.chunkFramesReceived++;
|
|
119
|
+
reportStream.chunkBytesReceived += payload.length;
|
|
120
|
+
reportStream.chunks.push(payload.toString("utf8"));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
try {
|
|
124
|
+
report = JSON.parse(payload.toString("utf8"));
|
|
125
|
+
reportParseError = null;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
reportParseError = String(error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
88
131
|
}
|
|
89
132
|
});
|
|
90
133
|
try {
|
|
@@ -119,14 +162,35 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStar
|
|
|
119
162
|
};
|
|
120
163
|
}
|
|
121
164
|
const passthrough = captured.restore();
|
|
165
|
+
if (reportStream.sawChunkStart) {
|
|
166
|
+
if (reportStream.sawChunkEnd) {
|
|
167
|
+
const chunkedPayload = reportStream.chunks.join("");
|
|
168
|
+
try {
|
|
169
|
+
report = JSON.parse(chunkedPayload);
|
|
170
|
+
reportParseError = null;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
reportParseError = String(error);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
122
177
|
if (!report?.fuzzers) {
|
|
178
|
+
const diagnostics = [
|
|
179
|
+
`chunked=${reportStream.sawChunkStart ? "yes" : "no"}`,
|
|
180
|
+
`chunkStart=${reportStream.sawChunkStart ? "yes" : "no"}`,
|
|
181
|
+
`chunkEnd=${reportStream.sawChunkEnd ? "yes" : "no"}`,
|
|
182
|
+
`chunkFrames=${reportStream.chunkFramesReceived}`,
|
|
183
|
+
`expectedChunkFrames=${reportStream.chunkCountExpected}`,
|
|
184
|
+
`chunkBytes=${reportStream.chunkBytesReceived}`,
|
|
185
|
+
`expectedChunkBytes=${reportStream.chunkTotalBytesExpected}`,
|
|
186
|
+
].join(", ");
|
|
123
187
|
const crash = persistCrashRecord(config.crashDir, {
|
|
124
188
|
kind: "fuzz",
|
|
125
189
|
file,
|
|
126
190
|
entryKey: buildFuzzCrashEntryKey(file, modeName ?? "default"),
|
|
127
191
|
mode: modeName ?? "default",
|
|
128
192
|
seed: config.seed,
|
|
129
|
-
error: `missing fuzz report payload from ${path.basename(file)}`,
|
|
193
|
+
error: `${reportParseError ? `invalid fuzz report payload: ${reportParseError}` : `missing fuzz report payload from ${path.basename(file)}`} (${diagnostics})`,
|
|
130
194
|
stdout: passthrough.stdout,
|
|
131
195
|
stderr: "",
|
|
132
196
|
});
|
|
@@ -146,7 +210,10 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStar
|
|
|
146
210
|
};
|
|
147
211
|
}
|
|
148
212
|
const crashFiles = [];
|
|
149
|
-
|
|
213
|
+
const selectedFuzzers = fuzzerSelectors.length
|
|
214
|
+
? filterSelectedFuzzers(report.fuzzers, fuzzerSelectors, file)
|
|
215
|
+
: report.fuzzers;
|
|
216
|
+
for (const fuzzer of selectedFuzzers) {
|
|
150
217
|
if (fuzzer.failed <= 0 && fuzzer.crashed <= 0)
|
|
151
218
|
continue;
|
|
152
219
|
const firstFailureSeed = typeof fuzzer.failures?.[0]?.seed == "number"
|
|
@@ -158,7 +225,7 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStar
|
|
|
158
225
|
entryKey: buildFuzzFailureEntryKey(file, fuzzer.name, modeName ?? "default"),
|
|
159
226
|
mode: modeName ?? "default",
|
|
160
227
|
seed: firstFailureSeed,
|
|
161
|
-
reproCommand: buildFuzzReproCommand(file, firstFailureSeed, modeName ?? "default", 1),
|
|
228
|
+
reproCommand: buildFuzzReproCommand(file, firstFailureSeed, modeName ?? "default", fuzzer.selector, 1),
|
|
162
229
|
error: fuzzer.failure?.message ||
|
|
163
230
|
`fuzz failure in ${fuzzer.name} after ${fuzzer.runs} runs`,
|
|
164
231
|
stdout: passthrough.stdout,
|
|
@@ -173,21 +240,49 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStar
|
|
|
173
240
|
file,
|
|
174
241
|
target: path.basename(file),
|
|
175
242
|
modeName: modeName ?? "default",
|
|
176
|
-
runs:
|
|
177
|
-
crashes:
|
|
243
|
+
runs: selectedFuzzers.reduce((sum, item) => sum + item.runs, 0),
|
|
244
|
+
crashes: selectedFuzzers.reduce((sum, item) => sum + item.crashed, 0),
|
|
178
245
|
crashFiles,
|
|
179
246
|
seed: config.seed,
|
|
180
247
|
time: Date.now() - startedAt,
|
|
181
248
|
buildTime,
|
|
182
249
|
buildStartedAt,
|
|
183
250
|
buildFinishedAt,
|
|
184
|
-
fuzzers:
|
|
251
|
+
fuzzers: selectedFuzzers,
|
|
185
252
|
};
|
|
186
253
|
}
|
|
187
|
-
function
|
|
254
|
+
function filterSelectedFuzzers(fuzzers, selectors, file) {
|
|
255
|
+
const annotated = fuzzers.map((fuzzer) => ({
|
|
256
|
+
...fuzzer,
|
|
257
|
+
selector: slugifyFuzzerSelector(fuzzer.name),
|
|
258
|
+
}));
|
|
259
|
+
const selected = new Set();
|
|
260
|
+
for (const selector of selectors) {
|
|
261
|
+
const slug = slugifyFuzzerSelector(selector);
|
|
262
|
+
if (!slug.length)
|
|
263
|
+
continue;
|
|
264
|
+
const matches = annotated.filter((fuzzer) => fuzzer.selector == slug);
|
|
265
|
+
if (!matches.length) {
|
|
266
|
+
throw new Error(`No fuzz targets matched "${selector}" in ${path.basename(file)}.`);
|
|
267
|
+
}
|
|
268
|
+
for (const match of matches) {
|
|
269
|
+
selected.add(match.selector);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return annotated.filter((fuzzer) => selected.has(fuzzer.selector ?? ""));
|
|
273
|
+
}
|
|
274
|
+
function slugifyFuzzerSelector(value) {
|
|
275
|
+
return value
|
|
276
|
+
.trim()
|
|
277
|
+
.toLowerCase()
|
|
278
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
279
|
+
.replace(/^-+|-+$/g, "");
|
|
280
|
+
}
|
|
281
|
+
function buildFuzzReproCommand(file, seed, modeName, fuzzer, runs) {
|
|
188
282
|
const modeArg = modeName != "default" ? ` --mode ${modeName}` : "";
|
|
283
|
+
const fuzzerArg = fuzzer?.length ? ` --fuzzer ${fuzzer}` : "";
|
|
189
284
|
const runsArg = typeof runs == "number" ? ` --runs ${runs}` : "";
|
|
190
|
-
return `ast fuzz ${file}${modeArg} --seed ${seed}${runsArg}`;
|
|
285
|
+
return `ast fuzz ${file}${modeArg}${fuzzerArg} --seed ${seed}${runsArg}`;
|
|
191
286
|
}
|
|
192
287
|
function buildFuzzFailureEntryKey(file, name, modeName) {
|
|
193
288
|
return `${path.basename(file).replace(/\.ts$/, "")}.${sanitizeEntryName(modeName)}.${sanitizeEntryName(name)}`;
|
|
@@ -260,12 +355,12 @@ function captureFrames(onFrame) {
|
|
|
260
355
|
}
|
|
261
356
|
});
|
|
262
357
|
process.stdin.read = ((size) => {
|
|
263
|
-
const max =
|
|
358
|
+
const max = size == null ? 0 : Number(size);
|
|
264
359
|
if (max > 0 && replies.length) {
|
|
265
360
|
return dequeueReply(max);
|
|
266
361
|
}
|
|
267
362
|
if (originalRead) {
|
|
268
|
-
return originalRead(size);
|
|
363
|
+
return originalRead(size === null ? undefined : size);
|
|
269
364
|
}
|
|
270
365
|
return null;
|
|
271
366
|
});
|
package/bin/commands/fuzz.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export async function executeFuzzCommand(rawArgs, configPath, selectedModes, deps) {
|
|
2
2
|
const commandArgs = deps.resolveCommandArgs(rawArgs, "fuzz");
|
|
3
|
+
const fuzzerSelectors = deps.resolveFuzzerSelectors(rawArgs, "fuzz");
|
|
3
4
|
const listFlags = deps.resolveListFlags(rawArgs, "fuzz");
|
|
4
5
|
const modeTargets = deps.resolveExecutionModes(configPath, selectedModes);
|
|
5
6
|
if (listFlags.list || listFlags.listModes) {
|
|
6
7
|
await deps.listExecutionPlan("fuzz", configPath, commandArgs, modeTargets, listFlags);
|
|
7
8
|
return;
|
|
8
9
|
}
|
|
9
|
-
await deps.runFuzzModes(configPath, commandArgs, modeTargets, rawArgs);
|
|
10
|
+
await deps.runFuzzModes(configPath, commandArgs, fuzzerSelectors, modeTargets, rawArgs);
|
|
10
11
|
}
|