-
Notifications
You must be signed in to change notification settings - Fork 756
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
Changes from 53 commits
93c4196
e220432
211d585
9610d85
ade1ebe
bf5d537
8b25475
5514a2c
96df913
02b6bfd
824908c
ef26967
1ef833a
70046a7
ba381d4
adc74f2
5fbb4d5
5af6120
9e68a3b
ed01751
d01a605
0f3b1dd
bfb4d38
c053377
8d1a559
94d9d01
d806a95
6931fd5
7b00d01
5bf3756
0d7668a
e7c55e3
b793595
a44418f
d479e99
14816ee
48e9392
686d39b
ea31e3c
09badf2
686403f
b7abf06
13a4a4d
3c246b9
663a667
110069b
3a57dad
d570dcc
639c964
f4d4311
9cca44c
3bfdbe5
15fa8fe
2daccd2
1dd5feb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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,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 | ||
// 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 */ () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this is all in There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
} | ||
} | ||
} | ||
|
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 | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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:
Each one sleeps, then we continue, then we sleep again etc., which I think achieves the same?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a todo.