Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSPI Fuzzing: Interleave executions #7226

Merged
merged 55 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from 53 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
93c4196
star
kripken Jan 10, 2025
e220432
work
kripken Jan 10, 2025
211d585
chak
kripken Jan 10, 2025
9610d85
fixes
kripken Jan 10, 2025
ade1ebe
fixes
kripken Jan 10, 2025
bf5d537
fix
kripken Jan 10, 2025
8b25475
temp
kripken Jan 10, 2025
5514a2c
fix
kripken Jan 10, 2025
96df913
sad
kripken Jan 10, 2025
02b6bfd
fix
kripken Jan 10, 2025
824908c
work
kripken Jan 10, 2025
ef26967
work
kripken Jan 10, 2025
1ef833a
test
kripken Jan 10, 2025
70046a7
work
kripken Jan 10, 2025
ba381d4
test
kripken Jan 11, 2025
adc74f2
moof
kripken Jan 11, 2025
5fbb4d5
todo
kripken Jan 11, 2025
5af6120
start
kripken Jan 11, 2025
9e68a3b
test
kripken Jan 11, 2025
ed01751
test
kripken Jan 11, 2025
d01a605
fix?
kripken Jan 16, 2025
0f3b1dd
Merge remote-tracking branch 'origin/main' into d8.test
kripken Jan 16, 2025
bfb4d38
coverage run too
kripken Jan 16, 2025
c053377
Merge remote-tracking branch 'origin/main' into d8.test
kripken Jan 16, 2025
8d1a559
no errors on invalid yml, very sad
kripken Jan 16, 2025
94d9d01
Merge branch 'd8.test' into fuzz.interleave.jspi
kripken Jan 16, 2025
d806a95
jsvu needs bash
kripken Jan 17, 2025
6931fd5
Merge remote-tracking branch 'myself/d8.test' into fuzz.interleave.jspi
kripken Jan 17, 2025
7b00d01
DEBUG-REVERTME
kripken Jan 17, 2025
5bf3756
work around alpine testing REVERTME
kripken Jan 17, 2025
0d7668a
Revert "work around alpine testing REVERTME"
kripken Jan 17, 2025
e7c55e3
Revert "DEBUG-REVERTME"
kripken Jan 17, 2025
b793595
try to delete rather than skip
kripken Jan 17, 2025
a44418f
undo.moar
kripken Jan 17, 2025
d479e99
try
kripken Jan 17, 2025
14816ee
finish
kripken Jan 17, 2025
48e9392
apply to release too
kripken Jan 17, 2025
686d39b
Merge branch 'd8.test' into fuzz.interleave.jspi
kripken Jan 17, 2025
ea31e3c
Merge remote-tracking branch 'myself/fuzz.interleave.jspi' into fuzz.…
kripken Jan 17, 2025
09badf2
undo
kripken Jan 17, 2025
686403f
Merge remote-tracking branch 'origin/main' into fuzz.interleave.jspi
kripken Jan 17, 2025
b7abf06
Merge remote-tracking branch 'origin/main' into fuzz.interleave.jspi
kripken Jan 17, 2025
13a4a4d
fix test
kripken Jan 18, 2025
3c246b9
try to fix windows
kripken Jan 18, 2025
663a667
DEBUG.WINDOWS.1
kripken Jan 21, 2025
110069b
DEBUG.WINDOWS.2
kripken Jan 21, 2025
3a57dad
DEBUG.WINDOWS.3
kripken Jan 21, 2025
d570dcc
DEBUG.WINDOWS.4
kripken Jan 21, 2025
639c964
UNDO
kripken Jan 21, 2025
f4d4311
clean
kripken Jan 21, 2025
9cca44c
split
kripken Jan 22, 2025
3bfdbe5
split
kripken Jan 22, 2025
15fa8fe
simpl
kripken Jan 22, 2025
2daccd2
add chaining todo
kripken Jan 23, 2025
1dd5feb
add chaining todo
kripken Jan 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 70 additions & 27 deletions scripts/fuzz_shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -423,16 +408,74 @@ 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 (which would be pointless), and we
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferring a task more than once would be interesting if it were deferred via chaining to another promise. And it would be even more interesting if it were "chained" to another promise by being returned from a future JSPI import call.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, interesting, what do you mean by "chaining" here? Is there an API for that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var newPromise = oldPromise.then((x) => { return x; }); or similar ways of wrapping the contents of the old promise in a new promise that will only be resolved after the old promise.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think that would test anything on the wasm side? I'd guess that just defers any wasm work further? I don't really know the VM side though.

As for chaining to a future JSPI call, that does happen already, if there is this in the wasm:

sleep();
sleep();
sleep();

Each one sleeps, then we continue, then we sleep again etc., which I think achieves the same?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, maybe chaining in pure JS wouldn't add much. I think chaining through future JSPI calls might still be interesting because it would mix up the temporal order of tasks. The common case is that a task will not be resumed until a subsequently started task is completed, as in the sequence of sleeps above, but it is also interesting to test tasks that will not be resumed until prior tasks are completed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a todo.

// also only defer a promise (which we check for using .then).
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 */ () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is all in if (JSPI), do the async and await need to be comments?

Copy link
Member Author

@kripken kripken Jan 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My (perhaps overly paranoid) approach was that when JSPI is off, we don't want the JS VM to see any async stuff, which could affect codegen. Like maybe they lay out the stack or basic blocks differently in async functions?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would hope these isolated awaits/asyncs would not affect other code. Anway, I'm fine with leaving them since it's consistent with all the other uses.

console.log(`(jspi: finish ${task.name})`);
return /* await */ result;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this await be in a try/catch as well? If the suspended Wasm ends up throwing an error, that error would propagate out here IIUC.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the try-catch on 468 is enough? This code ends up run in that try-catch, so yes, an exception would propagate, but just to that try-catch, where it is caught and handled. (Note that we can't really add any handling for it here - all we could do is log and rethrow?)

};
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));
}
}
}
Expand Down
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
Expand Up @@ -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
Expand Down
Loading