diff --git a/scripts/fuzz_shell.js b/scripts/fuzz_shell.js index 1856c815b3e..252ee5629dc 100644 --- a/scripts/fuzz_shell.js +++ b/scripts/fuzz_shell.js @@ -224,6 +224,34 @@ function toAddressType(table, index) { return index; } +// Simple deterministic hashing, on an unsigned 32-bit seed. See e.g. +// https://www.boost.org/doc/libs/1_55_0/doc/html/hash/reference.html#boost.hash_combine +var hashSeed; + +function hasHashSeed() { + return hashSeed !== undefined; +} + +function hashCombine(value) { + // hashSeed must be set before we do anything. + assert(hasHashSeed()); + + hashSeed ^= value + 0x9e3779b9 + (hashSeed << 6) + (hashSeed >>> 2); + return hashSeed >>> 0; +} + +// Get a random 32-bit number. This is like hashCombine but does not take a +// parameter. +function randomBits() { + return hashCombine(-1); +} + +// Return true with probability 1 in n. E.g. oneIn(3) returns false 2/3 of the +// time, and true 1/3 of the time. +function oneIn(n) { + return (randomBits() % n) == 0; +} + // Set up the imports. var tempRet0; var imports = { @@ -274,7 +302,10 @@ var imports = { // Sleep a given amount of ms (when JSPI) and return a given id after that. 'sleep': (ms, id) => { - if (!JSPI) { + // Also avoid sleeping even in JSPI mode, rarely, just to add variety + // here. Only do this when we have a hash seed, that is, when we are + // allowing randomness. + if (!JSPI || (hasHashSeed() && oneIn(10))) { return id; } return new Promise((resolve, reject) => { @@ -371,13 +402,6 @@ function build(binary) { } } -// Simple deterministic hashing, on an unsigned 32-bit seed. See e.g. -// https://www.boost.org/doc/libs/1_55_0/doc/html/hash/reference.html#boost.hash_combine -function hashCombine(seed, value) { - seed ^= value + 0x9e3779b9 + (seed << 6) + (seed >>> 2); - return seed >>> 0; -} - // Run the code by calling exports. The optional |ordering| parameter indicates // howe we should order the calls to the exports: if it is not provided, we call // them in the natural order, which allows our output to be compared to other @@ -385,6 +409,8 @@ function hashCombine(seed, value) { // provided, it is a random seed we use to make deterministic choices on // the order of calls. /* async */ function callExports(ordering) { + hashSeed = ordering; + // Call the exports we were told, or if we were not given an explicit list, // call them all. let relevantExports = exportsToCall || exportList; @@ -425,13 +451,12 @@ function hashCombine(seed, value) { task = tasks.pop(); } else { // Pick a random task. - ordering = hashCombine(ordering, tasks.length); - let i = ordering % tasks.length; + let i = hashCombine(tasks.length) % tasks.length; task = tasks.splice(i, 1)[0]; } // Execute the task. - console.log('[fuzz-exec] calling ' + task.name); + console.log(`[fuzz-exec] calling ${task.name}${task.deferred ? ' (after defer)' : ''}`); let result; try { result = task.func(); @@ -451,10 +476,7 @@ function hashCombine(seed, value) { // 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) { + if (randomBits() & 1) { // Defer it for later. Reuse the existing task for simplicity. console.log(`(jspi: defer ${task.name})`); task.func = /* async */ () => { diff --git a/test/lit/d8/fuzz_shell_sleep.wast b/test/lit/d8/fuzz_shell_sleep.wast new file mode 100644 index 00000000000..40bbb6e8289 --- /dev/null +++ b/test/lit/d8/fuzz_shell_sleep.wast @@ -0,0 +1,72 @@ +(module + (import "fuzzing-support" "sleep" (func $sleep (param i32 i32) (result i32))) + + (func $func1 (export "func1") (result i32) + (call $sleep + (i32.const 0) ;; ms (d8 always sleeps 0 anyhow) + (i32.const 1) ;; id + ) + ) + + (func $func2 (export "func2") (result i32) + (call $sleep + (i32.const 0) + (i32.const 2) + ) + ) + + (func $func3 (export "func3") (result i32) + (call $sleep + (i32.const 0) + (i32.const 3) + ) + ) + + (func $func4 (export "func4") (result i32) + (call $sleep + (i32.const 0) + (i32.const 4) + ) + ) + + (func $func5 (export "func5") (result i32) + (call $sleep + (i32.const 0) + (i32.const 5) + ) + ) +) + +;; See fuzz_shell_jspi.wast for how the following works. +;; RUN: echo "JSPI = 1;" > %t.0.js +;; 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.0.js + +;; Replace the callExports() at the end with a call that has a random seed. +;; RUN: cat %t.0.js | node -e "process.stdout.write(require('fs').readFileSync(0, 'utf-8').replace('callExports()', 'callExports(66)'))" > %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 +;; +;; We should see a few cases that avoid sleeping: func2, func3, and func4 all +;; return a result immediately, showing they do not sleep. (Note though that +;; func2 is more because we do not have a toplevel await, see comment in +;; fuzz_shell_jspi.wast.) +;; +;; CHECK: [fuzz-exec] calling func2 +;; CHECK: [fuzz-exec] note result: func2 => 2 +;; CHECK: [fuzz-exec] calling func1 +;; CHECK: (jspi: defer func1) +;; CHECK: [fuzz-exec] calling func3 +;; CHECK: [fuzz-exec] note result: func3 => 3 +;; CHECK: [fuzz-exec] calling func1 (after defer) +;; CHECK: (jspi: finish func1) +;; CHECK: [fuzz-exec] note result: func1 => 1 +;; CHECK: [fuzz-exec] calling func5 +;; CHECK: (jspi: defer func5) +;; CHECK: [fuzz-exec] calling func4 +;; CHECK: [fuzz-exec] note result: func4 => 4 +;; CHECK: [fuzz-exec] calling func5 (after defer) +;; CHECK: (jspi: finish func5) +;; CHECK: [fuzz-exec] note result: func5 => 5 +