JSPI Fuzzing: Interleave executions (#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.
This commit is contained in:
Alon Zakai
2025-01-24 11:33:09 -08:00
committed by GitHub
parent ee0191a37c
commit bb876a5805
3 changed files with 156 additions and 28 deletions

View File

@ -387,27 +387,12 @@ function hashCombine(seed, value) {
/* async */ function callExports(ordering) {
// Call the exports we were told, or if we were not given an explicit list,
// call them all.
var relevantExports = exportsToCall || exportList;
let relevantExports = exportsToCall || exportList;
if (ordering !== undefined) {
// Copy the list, and sort it in the simple Fisher-Yates manner.
// https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm
relevantExports = relevantExports.slice(0);
for (var i = 0; i < relevantExports.length - 1; i++) {
// Pick the index of the item to place at index |i|.
ordering = hashCombine(ordering, i);
// The number of items to pick from begins at the full length, then
// decreases with i.
var j = i + (ordering % (relevantExports.length - i));
// Swap the item over here.
var t = relevantExports[j];
relevantExports[j] = relevantExports[i];
relevantExports[i] = t;
}
}
for (var e of relevantExports) {
var name, value;
// Build the list of call tasks to run, one for each relevant export.
let tasks = [];
for (let e of relevantExports) {
let name, value;
if (typeof e === 'string') {
// We are given a string name to call. Look it up in the global namespace.
name = e;
@ -423,16 +408,78 @@ function hashCombine(seed, value) {
continue;
}
// A task is a name + a function to call. For an export, the function is
// simply a call of the export.
tasks.push({ name: name, func: /* async */ () => callFunc(value) });
}
// Reverse the array, so the first task is at the end, for efficient
// popping in the common case.
tasks.reverse();
// Execute tasks while they remain.
while (tasks.length) {
let task;
if (ordering === undefined) {
// Use the natural order.
task = tasks.pop();
} else {
// Pick a random task.
ordering = hashCombine(ordering, tasks.length);
let i = ordering % tasks.length;
task = tasks.splice(i, 1)[0];
}
// Execute the task.
console.log('[fuzz-exec] calling ' + task.name);
let result;
try {
console.log('[fuzz-exec] calling ' + name);
// TODO: Based on |ordering|, do not always await, leaving a promise
// for later, so we interleave stacks.
var result = /* await */ callFunc(value);
if (typeof result !== 'undefined') {
console.log('[fuzz-exec] note result: ' + name + ' => ' + printed(result));
}
result = task.func();
} catch (e) {
console.log('exception thrown: ' + e);
continue;
}
if (JSPI) {
// When we are changing up the order, in JSPI we can also leave some
// promises unresolved until later, which lets us interleave them. Note we
// never defer a task more than once, and we only defer a promise (which
// we check for using .then).
// TODO: Deferring more than once may make sense, by chaining promises in
// JS (that would not add wasm execution in the middle, but might
// find JS issues in principle). We could also link promises by
// depending on each other, ensuring certain orders of execution.
if (ordering !== undefined && !task.deferred && result &&
typeof result == 'object' && typeof result.then === 'function') {
// Hash with -1 here, just to get something different than the hashing a
// few lines above.
ordering = hashCombine(ordering, -1);
if (ordering & 1) {
// Defer it for later. Reuse the existing task for simplicity.
console.log(`(jspi: defer ${task.name})`);
task.func = /* async */ () => {
console.log(`(jspi: finish ${task.name})`);
return /* await */ result;
};
task.deferred = true;
tasks.push(task);
continue;
}
// Otherwise, continue down.
}
// Await it right now.
try {
result = /* await */ result;
} catch (e) {
console.log('exception thrown: ' + e);
continue;
}
}
// Log the result.
if (typeof result !== 'undefined') {
console.log('[fuzz-exec] note result: ' + task.name + ' => ' + printed(result));
}
}
}

View File

@ -0,0 +1,81 @@
(module
(import "fuzzing-support" "log-i32" (func $log (param i32)))
(func $a (export "a") (result i32)
(i32.const 10)
)
(func $b (export "b") (result i32)
(i32.const 20)
)
(func $c (export "c") (result i32)
(i32.const 30)
)
(func $d (export "d") (result i32)
(i32.const 40)
)
(func $e (export "e") (result i32)
(i32.const 50)
)
)
;; Apply JSPI: first, prepend JSPI = 1.
;; RUN: echo "JSPI = 1;" > %t.js
;; Second, remove comments around async and await: feed fuzz_shell.js into node
;; as stdin, so all node needs to do is read stdin, do the replacements, and
;; write to stdout.
;; 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
;; Append another run with a random seed, so we reorder and delay execution.
;; RUN: echo "callExports(42);" >> %t.js
;; Run that JS shell with our wasm.
;; RUN: wasm-opt %s -o %t.wasm -q
;; RUN: v8 --wasm-staging %t.js -- %t.wasm | filecheck %s
;;
;; The output here looks a little out of order, in particular because we do not
;; |await| the toplevel callExports() calls. That |await| is only valid if we
;; pass --module, which we do not fuzz with. As a result, the first await
;; operation in the first callExports() leaves that function and continues to
;; the next, but we do get around to executing all the things we need. In
;; particular, the output here should contain two "node result" lines for each
;; of the 5 functions (one from each callExports()). The important thing is that
;; we get a random-like ordering, which includes some defers (each of which has
;; a later finish), showing that we interleave stacks.
;;
;; CHECK: [fuzz-exec] calling a
;; CHECK: [fuzz-exec] calling b
;; CHECK: [fuzz-exec] note result: a => 10
;; CHECK: [fuzz-exec] calling b
;; CHECK: [fuzz-exec] note result: b => 20
;; CHECK: [fuzz-exec] calling a
;; CHECK: (jspi: defer a)
;; CHECK: [fuzz-exec] calling d
;; CHECK: (jspi: defer d)
;; CHECK: [fuzz-exec] calling e
;; CHECK: [fuzz-exec] note result: b => 20
;; CHECK: [fuzz-exec] calling c
;; CHECK: [fuzz-exec] note result: e => 50
;; CHECK: [fuzz-exec] calling c
;; CHECK: (jspi: defer c)
;; CHECK: [fuzz-exec] calling c
;; CHECK: (jspi: finish c)
;; CHECK: [fuzz-exec] note result: c => 30
;; CHECK: [fuzz-exec] calling d
;; CHECK: [fuzz-exec] note result: c => 30
;; CHECK: [fuzz-exec] calling d
;; CHECK: (jspi: finish d)
;; CHECK: [fuzz-exec] note result: d => 40
;; CHECK: [fuzz-exec] calling e
;; CHECK: [fuzz-exec] note result: d => 40
;; CHECK: [fuzz-exec] calling a
;; CHECK: (jspi: finish a)
;; CHECK: [fuzz-exec] note result: a => 10
;; CHECK: [fuzz-exec] note result: e => 50

View File

@ -32,7 +32,7 @@
;; Append another run with a seed that leads to a different order
;;
;; RUN: cp %S/../../../scripts/fuzz_shell.js %t.js
;; RUN: echo "callExports(1337);" >> %t.js
;; RUN: echo "callExports(34);" >> %t.js
;; RUN: node %t.js %t.wasm | filecheck %s --check-prefix=APPENDED
;;
;; The original order: a,b,c