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.
@@ -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
- export async function fuzz(configPath = DEFAULT_CONFIG_PATH, selectors = [], modeName, overrides = {}) {
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
- if (String(event.kind ?? "") == "fuzz:config") {
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
- report = JSON.parse(payload.toString("utf8"));
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
- for (const fuzzer of report.fuzzers) {
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: report.fuzzers.reduce((sum, item) => sum + item.runs, 0),
177
- crashes: report.fuzzers.reduce((sum, item) => sum + item.crashed, 0),
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: report.fuzzers,
251
+ fuzzers: selectedFuzzers,
185
252
  };
186
253
  }
187
- function buildFuzzReproCommand(file, seed, modeName, runs) {
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 = Number(size ?? 0);
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
  });
@@ -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
  }
@@ -472,7 +472,6 @@ function applyInit(root, target, example, fuzzExample, force) {
472
472
  fuzz: {
473
473
  input: ["assembly/__fuzz__/*.fuzz.ts"],
474
474
  runs: 1000,
475
- seed: 1337,
476
475
  target: "bindings",
477
476
  corpusDir: ".as-test/corpus",
478
477
  crashDir: ".as-test/crashes",