Skip to content

Commit

Permalink
JSPI Fuzzing: Interleave executions (#7226)
Browse files Browse the repository at this point in the history
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.
kripken authored Jan 24, 2025

Verified

This commit was signed with the committer’s verified signature.
evenyag Yingwen
1 parent ee0191a commit bb876a5
Showing 3 changed files with 156 additions and 28 deletions.
101 changes: 74 additions & 27 deletions scripts/fuzz_shell.js
Original file line number Diff line number Diff line change
@@ -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;

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;
}
}
let relevantExports = exportsToCall || exportList;

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));
}
}
}
81 changes: 81 additions & 0 deletions test/lit/d8/fuzz_shell_jspi.wast
Original file line number Diff line number Diff line change
@@ -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

2 changes: 1 addition & 1 deletion test/lit/node/fuzz_shell_orders.wast
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit bb876a5

Please sign in to comment.