Skip to content

Commit bb876a5

Browse files
authored
JSPI Fuzzing: Interleave executions (WebAssembly#7226)
Rather than always do await export() now we might stash the Promise on the side, and execute it later, after other stacks are executed and perhaps also saved. To do this, rewrite the logic for calling the exports in a more flexible manner. (That required altering the random seed in fuzz_shell_orders.wast, to preserve the current order it was emitting.) We do not fuzz with top-level await, so the output here looks a bit out of order, but it does still end up with interleaved executions, which I think is useful for fuzzing.
1 parent ee0191a commit bb876a5

File tree

3 files changed

+156
-28
lines changed

3 files changed

+156
-28
lines changed

scripts/fuzz_shell.js

Lines changed: 74 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -387,27 +387,12 @@ function hashCombine(seed, value) {
387387
/* async */ function callExports(ordering) {
388388
// Call the exports we were told, or if we were not given an explicit list,
389389
// call them all.
390-
var relevantExports = exportsToCall || exportList;
391-
392-
if (ordering !== undefined) {
393-
// Copy the list, and sort it in the simple Fisher-Yates manner.
394-
// https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm
395-
relevantExports = relevantExports.slice(0);
396-
for (var i = 0; i < relevantExports.length - 1; i++) {
397-
// Pick the index of the item to place at index |i|.
398-
ordering = hashCombine(ordering, i);
399-
// The number of items to pick from begins at the full length, then
400-
// decreases with i.
401-
var j = i + (ordering % (relevantExports.length - i));
402-
// Swap the item over here.
403-
var t = relevantExports[j];
404-
relevantExports[j] = relevantExports[i];
405-
relevantExports[i] = t;
406-
}
407-
}
390+
let relevantExports = exportsToCall || exportList;
408391

409-
for (var e of relevantExports) {
410-
var name, value;
392+
// Build the list of call tasks to run, one for each relevant export.
393+
let tasks = [];
394+
for (let e of relevantExports) {
395+
let name, value;
411396
if (typeof e === 'string') {
412397
// We are given a string name to call. Look it up in the global namespace.
413398
name = e;
@@ -423,16 +408,78 @@ function hashCombine(seed, value) {
423408
continue;
424409
}
425410

411+
// A task is a name + a function to call. For an export, the function is
412+
// simply a call of the export.
413+
tasks.push({ name: name, func: /* async */ () => callFunc(value) });
414+
}
415+
416+
// Reverse the array, so the first task is at the end, for efficient
417+
// popping in the common case.
418+
tasks.reverse();
419+
420+
// Execute tasks while they remain.
421+
while (tasks.length) {
422+
let task;
423+
if (ordering === undefined) {
424+
// Use the natural order.
425+
task = tasks.pop();
426+
} else {
427+
// Pick a random task.
428+
ordering = hashCombine(ordering, tasks.length);
429+
let i = ordering % tasks.length;
430+
task = tasks.splice(i, 1)[0];
431+
}
432+
433+
// Execute the task.
434+
console.log('[fuzz-exec] calling ' + task.name);
435+
let result;
426436
try {
427-
console.log('[fuzz-exec] calling ' + name);
428-
// TODO: Based on |ordering|, do not always await, leaving a promise
429-
// for later, so we interleave stacks.
430-
var result = /* await */ callFunc(value);
431-
if (typeof result !== 'undefined') {
432-
console.log('[fuzz-exec] note result: ' + name + ' => ' + printed(result));
433-
}
437+
result = task.func();
434438
} catch (e) {
435439
console.log('exception thrown: ' + e);
440+
continue;
441+
}
442+
443+
if (JSPI) {
444+
// When we are changing up the order, in JSPI we can also leave some
445+
// promises unresolved until later, which lets us interleave them. Note we
446+
// never defer a task more than once, and we only defer a promise (which
447+
// we check for using .then).
448+
// TODO: Deferring more than once may make sense, by chaining promises in
449+
// JS (that would not add wasm execution in the middle, but might
450+
// find JS issues in principle). We could also link promises by
451+
// depending on each other, ensuring certain orders of execution.
452+
if (ordering !== undefined && !task.deferred && result &&
453+
typeof result == 'object' && typeof result.then === 'function') {
454+
// Hash with -1 here, just to get something different than the hashing a
455+
// few lines above.
456+
ordering = hashCombine(ordering, -1);
457+
if (ordering & 1) {
458+
// Defer it for later. Reuse the existing task for simplicity.
459+
console.log(`(jspi: defer ${task.name})`);
460+
task.func = /* async */ () => {
461+
console.log(`(jspi: finish ${task.name})`);
462+
return /* await */ result;
463+
};
464+
task.deferred = true;
465+
tasks.push(task);
466+
continue;
467+
}
468+
// Otherwise, continue down.
469+
}
470+
471+
// Await it right now.
472+
try {
473+
result = /* await */ result;
474+
} catch (e) {
475+
console.log('exception thrown: ' + e);
476+
continue;
477+
}
478+
}
479+
480+
// Log the result.
481+
if (typeof result !== 'undefined') {
482+
console.log('[fuzz-exec] note result: ' + task.name + ' => ' + printed(result));
436483
}
437484
}
438485
}

test/lit/d8/fuzz_shell_jspi.wast

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
(module
2+
(import "fuzzing-support" "log-i32" (func $log (param i32)))
3+
4+
(func $a (export "a") (result i32)
5+
(i32.const 10)
6+
)
7+
8+
(func $b (export "b") (result i32)
9+
(i32.const 20)
10+
)
11+
12+
(func $c (export "c") (result i32)
13+
(i32.const 30)
14+
)
15+
16+
(func $d (export "d") (result i32)
17+
(i32.const 40)
18+
)
19+
20+
(func $e (export "e") (result i32)
21+
(i32.const 50)
22+
)
23+
)
24+
25+
;; Apply JSPI: first, prepend JSPI = 1.
26+
27+
;; RUN: echo "JSPI = 1;" > %t.js
28+
29+
;; Second, remove comments around async and await: feed fuzz_shell.js into node
30+
;; as stdin, so all node needs to do is read stdin, do the replacements, and
31+
;; write to stdout.
32+
33+
;; RUN: cat %S/../../../scripts/fuzz_shell.js | node -e "process.stdout.write(require('fs').readFileSync(0, 'utf-8').replace(/[/][*] async [*][/]/g, 'async').replace(/[/][*] await [*][/]/g, 'await'))" >> %t.js
34+
35+
;; Append another run with a random seed, so we reorder and delay execution.
36+
;; RUN: echo "callExports(42);" >> %t.js
37+
38+
;; Run that JS shell with our wasm.
39+
;; RUN: wasm-opt %s -o %t.wasm -q
40+
;; RUN: v8 --wasm-staging %t.js -- %t.wasm | filecheck %s
41+
;;
42+
;; The output here looks a little out of order, in particular because we do not
43+
;; |await| the toplevel callExports() calls. That |await| is only valid if we
44+
;; pass --module, which we do not fuzz with. As a result, the first await
45+
;; operation in the first callExports() leaves that function and continues to
46+
;; the next, but we do get around to executing all the things we need. In
47+
;; particular, the output here should contain two "node result" lines for each
48+
;; of the 5 functions (one from each callExports()). The important thing is that
49+
;; we get a random-like ordering, which includes some defers (each of which has
50+
;; a later finish), showing that we interleave stacks.
51+
;;
52+
;; CHECK: [fuzz-exec] calling a
53+
;; CHECK: [fuzz-exec] calling b
54+
;; CHECK: [fuzz-exec] note result: a => 10
55+
;; CHECK: [fuzz-exec] calling b
56+
;; CHECK: [fuzz-exec] note result: b => 20
57+
;; CHECK: [fuzz-exec] calling a
58+
;; CHECK: (jspi: defer a)
59+
;; CHECK: [fuzz-exec] calling d
60+
;; CHECK: (jspi: defer d)
61+
;; CHECK: [fuzz-exec] calling e
62+
;; CHECK: [fuzz-exec] note result: b => 20
63+
;; CHECK: [fuzz-exec] calling c
64+
;; CHECK: [fuzz-exec] note result: e => 50
65+
;; CHECK: [fuzz-exec] calling c
66+
;; CHECK: (jspi: defer c)
67+
;; CHECK: [fuzz-exec] calling c
68+
;; CHECK: (jspi: finish c)
69+
;; CHECK: [fuzz-exec] note result: c => 30
70+
;; CHECK: [fuzz-exec] calling d
71+
;; CHECK: [fuzz-exec] note result: c => 30
72+
;; CHECK: [fuzz-exec] calling d
73+
;; CHECK: (jspi: finish d)
74+
;; CHECK: [fuzz-exec] note result: d => 40
75+
;; CHECK: [fuzz-exec] calling e
76+
;; CHECK: [fuzz-exec] note result: d => 40
77+
;; CHECK: [fuzz-exec] calling a
78+
;; CHECK: (jspi: finish a)
79+
;; CHECK: [fuzz-exec] note result: a => 10
80+
;; CHECK: [fuzz-exec] note result: e => 50
81+

test/lit/node/fuzz_shell_orders.wast

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
;; Append another run with a seed that leads to a different order
3333
;;
3434
;; RUN: cp %S/../../../scripts/fuzz_shell.js %t.js
35-
;; RUN: echo "callExports(1337);" >> %t.js
35+
;; RUN: echo "callExports(34);" >> %t.js
3636
;; RUN: node %t.js %t.wasm | filecheck %s --check-prefix=APPENDED
3737
;;
3838
;; The original order: a,b,c

0 commit comments

Comments
 (0)