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

Fuzz JSPI #7148

Merged
merged 31 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions scripts/clusterfuzz/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,15 @@ def get_js_file_contents(i, output_dir):

print(f'Created {bytes} wasm bytes')

# Some of the time, fuzz JSPI (similar to fuzz_opt.py, see details there).
if system_random.random() < 0.25:
# Prepend the flag to enable JSPI.
js = 'var JSPI = 1;\n\n' + js

# Un-comment the async and await keywords.
js = js.replace('/* async */', 'async')
js = js.replace('/* await */', 'await')

return js


Expand Down
46 changes: 41 additions & 5 deletions scripts/fuzz_opt.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,11 @@ def randomize_fuzz_settings():
if random.random() < 0.5:
GEN_ARGS += ['--enclose-world']

print('randomized settings (NaNs, OOB, legalize):', NANS, OOB, LEGALIZE)
# Test JSPI somewhat rarely, as it may be slower.
global JSPI
JSPI = random.random() < 0.25

print('randomized settings (NaNs, OOB, legalize, JSPI):', NANS, OOB, LEGALIZE, JSPI)


def init_important_initial_contents():
Expand Down Expand Up @@ -758,11 +762,39 @@ def run_d8_js(js, args=[], liftoff=True):
return run_vm(cmd)


FUZZ_SHELL_JS = in_binaryen('scripts', 'fuzz_shell.js')
# For JSPI, we must customize fuzz_shell.js. We do so the first time we need
# it, and save the filename here.
JSPI_JS_FILE = None


def get_fuzz_shell_js():
js = in_binaryen('scripts', 'fuzz_shell.js')

if not JSPI:
# Just use the normal fuzz shell script.
return js

global JSPI_JS_FILE
if JSPI_JS_FILE:
# Use the customized file we've already created.
return JSPI_JS_FILE

JSPI_JS_FILE = os.path.abspath('jspi_fuzz_shell.js')
with open(JSPI_JS_FILE, 'w') as f:
# Enable JSPI.
f.write('var JSPI = 1;\n\n')

# Un-comment the async and await keywords.
with open(js) as g:
code = g.read()
code = code.replace('/* async */', 'async')
code = code.replace('/* await */', 'await')
f.write(code)
return JSPI_JS_FILE


def run_d8_wasm(wasm, liftoff=True, args=[]):
return run_d8_js(FUZZ_SHELL_JS, [wasm] + args, liftoff=liftoff)
return run_d8_js(get_fuzz_shell_js(), [wasm] + args, liftoff=liftoff)


def all_disallowed(features):
Expand Down Expand Up @@ -850,7 +882,7 @@ class D8:
name = 'd8'

def run(self, wasm, extra_d8_flags=[]):
return run_vm([shared.V8, FUZZ_SHELL_JS] + shared.V8_OPTS + get_v8_extra_flags() + extra_d8_flags + ['--', wasm])
return run_vm([shared.V8, get_fuzz_shell_js()] + shared.V8_OPTS + get_v8_extra_flags() + extra_d8_flags + ['--', wasm])

def can_run(self, wasm):
# V8 does not support shared memories when running with
Expand Down Expand Up @@ -1160,7 +1192,7 @@ def fix_number(x):
compare_between_vms(before, interpreter, 'Wasm2JS (vs interpreter)')

def run(self, wasm):
with open(FUZZ_SHELL_JS) as f:
with open(get_fuzz_shell_js()) as f:
wrapper = f.read()
cmd = [in_bin('wasm2js'), wasm, '--emscripten']
# avoid optimizations if we have nans, as we don't handle them with
Expand Down Expand Up @@ -1193,6 +1225,10 @@ def can_run_on_wasm(self, wasm):
# specifically for growth here
if INITIAL_CONTENTS:
return False
# We run in node, which lacks JSPI support, and also we need wasm2js to
# implement wasm suspending using JS async/await.
if JSPI:
return False
return all_disallowed(['exception-handling', 'simd', 'threads', 'bulk-memory', 'nontrapping-float-to-int', 'tail-call', 'sign-ext', 'reference-types', 'multivalue', 'gc', 'multimemory', 'memory64'])


Expand Down
79 changes: 63 additions & 16 deletions scripts/fuzz_shell.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
// This script can be customized by setting the following variables in code that
// runs before this script.
//
// The binary to be run. (If not set, we get the filename from argv and read
// from it.)
var binary;
// A second binary to be linked in and run as well. (Can also be read from
// argv.)
var secondBinary;
// Whether we are fuzzing JSPI. In addition to this being set, the "async" and
// "await" keywords must be taken out of the /* KEYWORD */ comments (which they
// are normally in, so as not to affect normal fuzzing).
var JSPI;

// Shell integration: find argv and set up readBinary().
var argv;
var readBinary;
Expand Down Expand Up @@ -25,9 +39,6 @@ if (typeof process === 'object' && typeof require === 'function') {
};
}

// The binary to be run. This may be set already (by code that runs before this
// script), and if not, we get the filename from argv.
var binary;
if (!binary) {
binary = readBinary(argv[0]);
}
Expand All @@ -43,7 +54,6 @@ if (argv.length > 0 && argv[argv.length - 1].startsWith('exports:')) {

// If a second parameter is given, it is a second binary that we will link in
// with it.
var secondBinary;
if (argv[1]) {
secondBinary = readBinary(argv[1]);
}
Expand Down Expand Up @@ -163,9 +173,9 @@ function callFunc(func) {
// Calls a given function in a try-catch, swallowing JS exceptions, and return 1
// if we did in fact swallow an exception. Wasm traps are not swallowed (see
// details below).
function tryCall(func) {
/* async */ function tryCall(func) {
try {
func();
/* await */ func();
return 0;
} catch (e) {
// We only want to catch exceptions, not wasm traps: traps should still
Expand Down Expand Up @@ -243,19 +253,39 @@ var imports = {
},

// Export operations.
'call-export': (index) => {
callFunc(exportList[index].value);
'call-export': /* async */ (index) => {
/* await */ callFunc(exportList[index].value);
},
'call-export-catch': (index) => {
return tryCall(() => callFunc(exportList[index].value));
'call-export-catch': /* async */ (index) => {
return tryCall(/* async */ () => /* await */ callFunc(exportList[index].value));
},

// Funcref operations.
'call-ref': (ref) => {
callFunc(ref);
'call-ref': /* async */ (ref) => {
// This is a direct function reference, and just like an export, it must
// be wrapped for JSPI.
ref = wrapExportForJSPI(ref);
/* await */ callFunc(ref);
},
'call-ref-catch': /* async */ (ref) => {
ref = wrapExportForJSPI(ref);
return tryCall(/* async */ () => /* await */ callFunc(ref));
},
'call-ref-catch': (ref) => {
return tryCall(() => callFunc(ref));

// Sleep a given amount of ms (when JSPI) and return a given id after that.
'sleep': (ms, id) => {
if (!JSPI) {
return id;
}
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(id);
}, 0); // TODO: Use the ms in some reasonable, deterministic manner.
// Rather than actually setTimeout on them we could manage
// a queue of pending sleeps manually, and order them based
// on the "ms" (which would not be literal ms, but just
// how many time units to wait).
});
},
},
// Emscripten support.
Expand All @@ -274,6 +304,22 @@ if (typeof WebAssembly.Tag !== 'undefined') {
};
}

// If JSPI is available, wrap the imports and exports.
if (JSPI) {
for (var name of ['sleep', 'call-export', 'call-export-catch', 'call-ref',
'call-ref-catch']) {
imports['fuzzing-support'][name] =
new WebAssembly.Suspending(imports['fuzzing-support'][name]);
}
}

function wrapExportForJSPI(value) {
if (JSPI && typeof value === 'function') {
value = WebAssembly.promising(value);
}
return value;
}

// If a second binary will be linked in then set up the imports for
// placeholders. Any import like (import "placeholder" "0" (func .. will be
// provided by the secondary module, and must be called using an indirection.
Expand Down Expand Up @@ -312,13 +358,14 @@ function build(binary) {
// keep the ability to call anything that was ever exported.)
for (var key in instance.exports) {
var value = instance.exports[key];
value = wrapExportForJSPI(value);
exports[key] = value;
exportList.push({ name: key, value: value });
}
}

// Run the code by calling exports.
function callExports() {
/* async */ function callExports() {
// Call the exports we were told, or if we were not given an explicit list,
// call them all.
var relevantExports = exportsToCall || exportList;
Expand All @@ -342,7 +389,7 @@ function callExports() {

try {
console.log('[fuzz-exec] calling ' + name);
var result = callFunc(value);
var result = /* await */ callFunc(value);
if (typeof result !== 'undefined') {
console.log('[fuzz-exec] note result: ' + name + ' => ' + printed(result));
}
Expand Down
3 changes: 3 additions & 0 deletions src/tools/execution-results.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ struct LoggingExternalInterface : public ShellExternalInterface {
} catch (const WasmException& e) {
return {Literal(int32_t(1))};
}
} else if (import->base == "sleep") {
// Do not actually sleep, just return the id.
return {arguments[1]};
} else {
WASM_UNREACHABLE("unknown fuzzer import");
}
Expand Down
3 changes: 3 additions & 0 deletions src/tools/fuzzing.h
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ class TranslateToFuzzReader {
Name callExportCatchImportName;
Name callRefImportName;
Name callRefCatchImportName;
Name sleepImportName;

std::unordered_map<Type, std::vector<Name>> globalsByType;
std::unordered_map<Type, std::vector<Name>> mutableGlobalsByType;
Expand Down Expand Up @@ -238,6 +239,7 @@ class TranslateToFuzzReader {
void addImportCallingSupport();
void addImportThrowingSupport();
void addImportTableSupport();
void addImportSleepSupport();
void addHashMemorySupport();

// Special expression makers
Expand All @@ -249,6 +251,7 @@ class TranslateToFuzzReader {
// Call either an export or a ref. We do this from a single function to better
// control the frequency of each.
Expression* makeImportCallCode(Type type);
Expression* makeImportSleep(Type type);
Expression* makeMemoryHashLogging();

// Function creation
Expand Down
29 changes: 29 additions & 0 deletions src/tools/fuzzing/fuzzing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ void TranslateToFuzzReader::build() {
}
addImportLoggingSupport();
addImportCallingSupport();
addImportSleepSupport();
modifyInitialFunctions();
// keep adding functions until we run out of input
while (!random.finished()) {
Expand Down Expand Up @@ -909,6 +910,24 @@ void TranslateToFuzzReader::addImportTableSupport() {
}
}

void TranslateToFuzzReader::addImportSleepSupport() {
if (!oneIn(4)) {
// Fuzz this somewhat rarely, as it may be slow.
return;
}

// An import that sleeps for a given number of milliseconds, and also receives
// an integer id. It returns that integer id (useful for tracking separate
// sleeps).
sleepImportName = Names::getValidFunctionName(wasm, "sleep");
auto func = std::make_unique<Function>();
func->name = sleepImportName;
func->module = "fuzzing-support";
func->base = "sleep";
func->type = Signature({Type::i32, Type::i32}, Type::i32);
wasm.addFunction(std::move(func));
}

void TranslateToFuzzReader::addHashMemorySupport() {
// Add memory hasher helper (for the hash, see hash.h). The function looks
// like:
Expand Down Expand Up @@ -1090,6 +1109,13 @@ Expression* TranslateToFuzzReader::makeImportCallCode(Type type) {
return builder.makeCall(exportTarget, {index}, type);
}

Expression* TranslateToFuzzReader::makeImportSleep(Type type) {
// Sleep for some ms, and return a given id.
auto* ms = make(Type::i32);
auto id = make(Type::i32);
return builder.makeCall(sleepImportName, {ms, id}, Type::i32);
}

Expression* TranslateToFuzzReader::makeMemoryHashLogging() {
auto* hash = builder.makeCall(std::string("hashMemory"), {}, Type::i32);
return builder.makeCall(logImportNames[Type::i32], {hash}, Type::none);
Expand Down Expand Up @@ -1768,6 +1794,9 @@ Expression* TranslateToFuzzReader::_makeConcrete(Type type) {
if (callExportCatchImportName || callRefCatchImportName) {
options.add(FeatureSet::MVP, &Self::makeImportCallCode);
}
if (sleepImportName) {
options.add(FeatureSet::MVP, &Self::makeImportSleep);
}
options.add(FeatureSet::ReferenceTypes, &Self::makeRefIsNull);
options.add(FeatureSet::ReferenceTypes | FeatureSet::GC,
&Self::makeRefEq,
Expand Down
19 changes: 18 additions & 1 deletion test/lit/exec/fuzzing-api.wast
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
(import "fuzzing-support" "call-ref" (func $call.ref (param funcref)))
(import "fuzzing-support" "call-ref-catch" (func $call.ref.catch (param funcref) (result i32)))

(import "fuzzing-support" "sleep" (func $sleep (param i32 i32) (result i32)))

(table $table 10 20 funcref)

;; Note that the exported table appears first here, but in the binary and in
Expand Down Expand Up @@ -284,7 +286,6 @@

;; CHECK: [fuzz-exec] calling ref.calling.trap
;; CHECK-NEXT: [trap unreachable]
;; CHECK-NEXT: warning: no passes specified, not doing any work
(func $ref.calling.trap (export "ref.calling.trap")
;; We try to catch an exception here, but the target function traps, which is
;; not something we can catch. We will trap here, and not log at all.
Expand All @@ -294,6 +295,18 @@
)
)
)

;; CHECK: [fuzz-exec] calling do-sleep
;; CHECK-NEXT: [fuzz-exec] note result: do-sleep => 42
;; CHECK-NEXT: warning: no passes specified, not doing any work
(func $do-sleep (export "do-sleep") (result i32)
(call $sleep
;; A ridiculous amount of ms, but in the interpreter it is ignored anyhow.
(i32.const -1)
;; An id, that is returned back to us.
(i32.const 42)
)
)
)
;; CHECK: [fuzz-exec] calling logging
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
Expand Down Expand Up @@ -354,6 +367,10 @@

;; CHECK: [fuzz-exec] calling ref.calling.trap
;; CHECK-NEXT: [trap unreachable]

;; CHECK: [fuzz-exec] calling do-sleep
;; CHECK-NEXT: [fuzz-exec] note result: do-sleep => 42
;; CHECK-NEXT: [fuzz-exec] comparing do-sleep
;; CHECK-NEXT: [fuzz-exec] comparing export.calling
;; CHECK-NEXT: [fuzz-exec] comparing export.calling.catching
;; CHECK-NEXT: [fuzz-exec] comparing logging
Expand Down
Loading
Loading