diff options
-rw-r--r-- | src/passes/Asyncify.cpp | 113 | ||||
-rw-r--r-- | src/wasm-binary.h | 1 | ||||
-rw-r--r-- | src/wasm/wasm-binary.cpp | 8 | ||||
-rw-r--r-- | test/passes/asyncify_pass-arg=asyncify-blacklist@foo,bar.txt | 285 | ||||
-rw-r--r-- | test/passes/asyncify_pass-arg=asyncify-blacklist@foo,bar.wast | 20 | ||||
-rw-r--r-- | test/passes/asyncify_pass-arg=asyncify-whitelist@foo,bar.txt | 285 | ||||
-rw-r--r-- | test/passes/asyncify_pass-arg=asyncify-whitelist@foo,bar.wast | 20 | ||||
-rw-r--r-- | test/unit/test_asyncify.py | 21 |
8 files changed, 733 insertions, 20 deletions
diff --git a/src/passes/Asyncify.cpp b/src/passes/Asyncify.cpp index 98b4425b1..7d6dd9f76 100644 --- a/src/passes/Asyncify.cpp +++ b/src/passes/Asyncify.cpp @@ -212,6 +212,24 @@ // If that is true for your codebase, then this can be extremely useful // as otherwise it looks like any indirect call can go to a lot of places. // +// --pass-arg=asyncify-blacklist@name1,name2,name3 +// +// If the blacklist is provided, then the functions in it will not be +// instrumented even if it looks like they need to. This can be useful +// if you know things the whole-program analysis doesn't, like if you +// know certain indirect calls are safe and won't unwind. But if you +// get the list wrong things will break (and in a production build user +// input might reach code paths you missed during testing, so it's hard +// to know you got this right), so this is not recommended unless you +// really know what are doing, and need to optimize every bit of speed +// and size. +// +// --pass-arg=asyncify-whitelist@name1,name2,name3 +// +// If the whitelist is provided, then only the functions in the list +// will be instrumented. Like the blacklist, getting this wrong will +// break your application. +// #include "ir/effects.h" #include "ir/literal-utils.h" @@ -327,9 +345,15 @@ class ModuleAnalyzer { public: ModuleAnalyzer(Module& module, std::function<bool(Name, Name)> canImportChangeState, - bool canIndirectChangeState) + bool canIndirectChangeState, + const String::Split& blacklistInput, + const String::Split& whitelistInput) : module(module), canIndirectChangeState(canIndirectChangeState), globals(module) { + + blacklist.insert(blacklistInput.begin(), blacklistInput.end()); + whitelist.insert(whitelistInput.begin(), whitelistInput.end()); + // Scan to see which functions can directly change the state. // Also handle the asyncify imports, removing them (as we will implement // them later), and replace calls to them with calls to the later proper @@ -388,11 +412,13 @@ public: } Info* info; Module* module; + ModuleAnalyzer* analyzer; bool canIndirectChangeState; }; Walker walker; walker.info = &info; walker.module = &module; + walker.analyzer = this; walker.canIndirectChangeState = canIndirectChangeState; walker.walk(func->body); @@ -406,6 +432,13 @@ public: map.swap(scanner.map); + // Functions in the blacklist are assumed to not change the state. + for (auto& name : blacklist) { + if (auto* func = module.getFunctionOrNull(name)) { + map[func].canChangeState = false; + } + } + // Remove the asyncify imports, if any. for (auto& pair : map) { auto* func = pair.first; @@ -435,12 +468,22 @@ public: while (!work.empty()) { auto* func = work.pop(); for (auto* caller : map[func].calledBy) { - if (!map[caller].canChangeState && !map[caller].isBottomMostRuntime) { + if (!map[caller].canChangeState && !map[caller].isBottomMostRuntime && + !blacklist.count(caller->name)) { map[caller].canChangeState = true; work.push(caller); } } } + + if (!whitelist.empty()) { + // Only the functions in the whitelist can change the state. + for (auto& func : module.functions) { + if (!func->imported()) { + map[func.get()].canChangeState = whitelist.count(func->name) > 0; + } + } + } } bool needsInstrumentation(Function* func) { @@ -481,6 +524,7 @@ public: // TODO optimize the other case, at least by type } Module* module; + ModuleAnalyzer* analyzer; Map* map; bool canIndirectChangeState; bool canChangeState = false; @@ -488,6 +532,7 @@ public: }; Walker walker; walker.module = &module; + walker.analyzer = this; walker.map = ↦ walker.canIndirectChangeState = canIndirectChangeState; walker.walk(curr); @@ -498,6 +543,8 @@ public: } GlobalHelper globals; + std::set<Name> blacklist; + std::set<Name> whitelist; }; // Checks if something performs a call: either a direct or indirect call, @@ -987,23 +1034,57 @@ struct Asyncify : public Pass { String::Split listedImports(stateChangingImports, ","); auto ignoreIndirect = runner->options.getArgumentOrDefault("asyncify-ignore-indirect", ""); + String::Split blacklist( + runner->options.getArgumentOrDefault("asyncify-blacklist", ""), ","); + String::Split whitelist( + runner->options.getArgumentOrDefault("asyncify-whitelist", ""), ","); + + // The lists contain human-readable strings. Turn them into the internal + // escaped names for later comparisons + auto processList = [module](String::Split& list, const std::string& which) { + for (auto& name : list) { + auto escaped = WasmBinaryBuilder::escape(name); + auto* func = module->getFunctionOrNull(escaped); + if (!func) { + std::cerr << "warning: Asyncify " << which + << "list contained a non-existing function name: " << name + << " (" << escaped << ")\n"; + } else if (func->imported()) { + Fatal() << "Asyncify " << which + << "list contained an imported function name (use the import " + "list for imports): " + << name << '\n'; + } + name = escaped.str; + } + }; + processList(blacklist, "black"); + processList(whitelist, "white"); + + if (!blacklist.empty() && !whitelist.empty()) { + Fatal() << "It makes no sense to use both a blacklist and a whitelist " + "with asyncify."; + } + + auto canImportChangeState = [&](Name module, Name base) { + if (allImportsCanChangeState) { + return true; + } + std::string full = std::string(module.str) + '.' + base.str; + for (auto& listedImport : listedImports) { + if (String::wildcardMatch(listedImport, full)) { + return true; + } + } + return false; + }; // Scan the module. ModuleAnalyzer analyzer(*module, - [&](Name module, Name base) { - if (allImportsCanChangeState) { - return true; - } - std::string full = - std::string(module.str) + '.' + base.str; - for (auto& listedImport : listedImports) { - if (String::wildcardMatch(listedImport, full)) { - return true; - } - } - return false; - }, - ignoreIndirect == ""); + canImportChangeState, + ignoreIndirect == "", + blacklist, + whitelist); // Add necessary globals before we emit code to use them. addGlobals(module); diff --git a/src/wasm-binary.h b/src/wasm-binary.h index e1eb5733c..f58a10cf7 100644 --- a/src/wasm-binary.h +++ b/src/wasm-binary.h @@ -1174,6 +1174,7 @@ public: void readEvents(); + static Name escape(Name name); void readNames(size_t); void readFeatures(size_t); diff --git a/src/wasm/wasm-binary.cpp b/src/wasm/wasm-binary.cpp index 3d9a1ba9b..039cef5e5 100644 --- a/src/wasm/wasm-binary.cpp +++ b/src/wasm/wasm-binary.cpp @@ -2054,13 +2054,13 @@ static char formatNibble(int nibble) { return nibble < 10 ? '0' + nibble : 'a' - 10 + nibble; } -static void escapeName(Name& name) { +Name WasmBinaryBuilder::escape(Name name) { bool allIdChars = true; for (const char* p = name.str; allIdChars && *p; p++) { allIdChars = isIdChar(*p); } if (allIdChars) { - return; + return name; } // encode name, if at least one non-idchar (per WebAssembly spec) was found std::string escaped; @@ -2075,7 +2075,7 @@ static void escapeName(Name& name) { escaped.push_back(formatNibble(ch >> 4)); escaped.push_back(formatNibble(ch & 15)); } - name = escaped; + return escaped; } void WasmBinaryBuilder::readNames(size_t payloadLen) { @@ -2098,7 +2098,7 @@ void WasmBinaryBuilder::readNames(size_t payloadLen) { for (size_t i = 0; i < num; i++) { auto index = getU32LEB(); auto rawName = getInlineString(); - escapeName(rawName); + rawName = escape(rawName); auto name = rawName; // De-duplicate names by appending .1, .2, etc. for (int i = 1; !usedNames.insert(name).second; ++i) { diff --git a/test/passes/asyncify_pass-arg=asyncify-blacklist@foo,bar.txt b/test/passes/asyncify_pass-arg=asyncify-blacklist@foo,bar.txt new file mode 100644 index 000000000..2c7ad5fe0 --- /dev/null +++ b/test/passes/asyncify_pass-arg=asyncify-blacklist@foo,bar.txt @@ -0,0 +1,285 @@ +(module + (type $FUNCSIG$v (func)) + (import "env" "import" (func $import)) + (memory $0 1 2) + (global $__asyncify_state (mut i32) (i32.const 0)) + (global $__asyncify_data (mut i32) (i32.const 0)) + (export "asyncify_start_unwind" (func $asyncify_start_unwind)) + (export "asyncify_stop_unwind" (func $asyncify_stop_unwind)) + (export "asyncify_start_rewind" (func $asyncify_start_rewind)) + (export "asyncify_stop_rewind" (func $asyncify_stop_rewind)) + (func $foo (; 1 ;) (type $FUNCSIG$v) + (call $import) + (nop) + ) + (func $bar (; 2 ;) (type $FUNCSIG$v) + (call $import) + (nop) + ) + (func $baz (; 3 ;) (type $FUNCSIG$v) + (local $0 i32) + (local $1 i32) + (if + (i32.eq + (global.get $__asyncify_state) + (i32.const 2) + ) + (nop) + ) + (local.set $0 + (block $__asyncify_unwind (result i32) + (block + (block + (if + (i32.eq + (global.get $__asyncify_state) + (i32.const 2) + ) + (block + (i32.store + (global.get $__asyncify_data) + (i32.add + (i32.load + (global.get $__asyncify_data) + ) + (i32.const -4) + ) + ) + (local.set $1 + (i32.load + (i32.load + (global.get $__asyncify_data) + ) + ) + ) + ) + ) + (block + (if + (if (result i32) + (i32.eq + (global.get $__asyncify_state) + (i32.const 0) + ) + (i32.const 1) + (i32.eq + (local.get $1) + (i32.const 0) + ) + ) + (block + (call $import) + (if + (i32.eq + (global.get $__asyncify_state) + (i32.const 1) + ) + (br $__asyncify_unwind + (i32.const 0) + ) + ) + ) + ) + (if + (i32.eq + (global.get $__asyncify_state) + (i32.const 0) + ) + (nop) + ) + ) + ) + (return) + ) + ) + ) + (block + (i32.store + (i32.load + (global.get $__asyncify_data) + ) + (local.get $0) + ) + (i32.store + (global.get $__asyncify_data) + (i32.add + (i32.load + (global.get $__asyncify_data) + ) + (i32.const 4) + ) + ) + ) + (nop) + ) + (func $other1 (; 4 ;) (type $FUNCSIG$v) + (call $foo) + (nop) + ) + (func $other2 (; 5 ;) (type $FUNCSIG$v) + (local $0 i32) + (local $1 i32) + (if + (i32.eq + (global.get $__asyncify_state) + (i32.const 2) + ) + (nop) + ) + (local.set $0 + (block $__asyncify_unwind (result i32) + (block + (block + (if + (i32.eq + (global.get $__asyncify_state) + (i32.const 2) + ) + (block + (i32.store + (global.get $__asyncify_data) + (i32.add + (i32.load + (global.get $__asyncify_data) + ) + (i32.const -4) + ) + ) + (local.set $1 + (i32.load + (i32.load + (global.get $__asyncify_data) + ) + ) + ) + ) + ) + (block + (if + (if (result i32) + (i32.eq + (global.get $__asyncify_state) + (i32.const 0) + ) + (i32.const 1) + (i32.eq + (local.get $1) + (i32.const 0) + ) + ) + (block + (call $baz) + (if + (i32.eq + (global.get $__asyncify_state) + (i32.const 1) + ) + (br $__asyncify_unwind + (i32.const 0) + ) + ) + ) + ) + (if + (i32.eq + (global.get $__asyncify_state) + (i32.const 0) + ) + (nop) + ) + ) + ) + (return) + ) + ) + ) + (block + (i32.store + (i32.load + (global.get $__asyncify_data) + ) + (local.get $0) + ) + (i32.store + (global.get $__asyncify_data) + (i32.add + (i32.load + (global.get $__asyncify_data) + ) + (i32.const 4) + ) + ) + ) + (nop) + ) + (func $asyncify_start_unwind (; 6 ;) (param $0 i32) + (global.set $__asyncify_state + (i32.const 1) + ) + (global.set $__asyncify_data + (local.get $0) + ) + (if + (i32.gt_u + (i32.load + (global.get $__asyncify_data) + ) + (i32.load offset=4 + (global.get $__asyncify_data) + ) + ) + (unreachable) + ) + ) + (func $asyncify_stop_unwind (; 7 ;) + (global.set $__asyncify_state + (i32.const 0) + ) + (if + (i32.gt_u + (i32.load + (global.get $__asyncify_data) + ) + (i32.load offset=4 + (global.get $__asyncify_data) + ) + ) + (unreachable) + ) + ) + (func $asyncify_start_rewind (; 8 ;) (param $0 i32) + (global.set $__asyncify_state + (i32.const 2) + ) + (global.set $__asyncify_data + (local.get $0) + ) + (if + (i32.gt_u + (i32.load + (global.get $__asyncify_data) + ) + (i32.load offset=4 + (global.get $__asyncify_data) + ) + ) + (unreachable) + ) + ) + (func $asyncify_stop_rewind (; 9 ;) + (global.set $__asyncify_state + (i32.const 0) + ) + (if + (i32.gt_u + (i32.load + (global.get $__asyncify_data) + ) + (i32.load offset=4 + (global.get $__asyncify_data) + ) + ) + (unreachable) + ) + ) +) diff --git a/test/passes/asyncify_pass-arg=asyncify-blacklist@foo,bar.wast b/test/passes/asyncify_pass-arg=asyncify-blacklist@foo,bar.wast new file mode 100644 index 000000000..8496e7ee2 --- /dev/null +++ b/test/passes/asyncify_pass-arg=asyncify-blacklist@foo,bar.wast @@ -0,0 +1,20 @@ +(module + (memory 1 2) + (import "env" "import" (func $import)) + (func $foo + (call $import) + ) + (func $bar + (call $import) + ) + (func $baz + (call $import) + ) + (func $other1 + (call $foo) + ) + (func $other2 + (call $baz) + ) +) + diff --git a/test/passes/asyncify_pass-arg=asyncify-whitelist@foo,bar.txt b/test/passes/asyncify_pass-arg=asyncify-whitelist@foo,bar.txt new file mode 100644 index 000000000..e69bc19a7 --- /dev/null +++ b/test/passes/asyncify_pass-arg=asyncify-whitelist@foo,bar.txt @@ -0,0 +1,285 @@ +(module + (type $FUNCSIG$v (func)) + (import "env" "import" (func $import)) + (memory $0 1 2) + (global $__asyncify_state (mut i32) (i32.const 0)) + (global $__asyncify_data (mut i32) (i32.const 0)) + (export "asyncify_start_unwind" (func $asyncify_start_unwind)) + (export "asyncify_stop_unwind" (func $asyncify_stop_unwind)) + (export "asyncify_start_rewind" (func $asyncify_start_rewind)) + (export "asyncify_stop_rewind" (func $asyncify_stop_rewind)) + (func $foo (; 1 ;) (type $FUNCSIG$v) + (local $0 i32) + (local $1 i32) + (if + (i32.eq + (global.get $__asyncify_state) + (i32.const 2) + ) + (nop) + ) + (local.set $0 + (block $__asyncify_unwind (result i32) + (block + (block + (if + (i32.eq + (global.get $__asyncify_state) + (i32.const 2) + ) + (block + (i32.store + (global.get $__asyncify_data) + (i32.add + (i32.load + (global.get $__asyncify_data) + ) + (i32.const -4) + ) + ) + (local.set $1 + (i32.load + (i32.load + (global.get $__asyncify_data) + ) + ) + ) + ) + ) + (block + (if + (if (result i32) + (i32.eq + (global.get $__asyncify_state) + (i32.const 0) + ) + (i32.const 1) + (i32.eq + (local.get $1) + (i32.const 0) + ) + ) + (block + (call $import) + (if + (i32.eq + (global.get $__asyncify_state) + (i32.const 1) + ) + (br $__asyncify_unwind + (i32.const 0) + ) + ) + ) + ) + (if + (i32.eq + (global.get $__asyncify_state) + (i32.const 0) + ) + (nop) + ) + ) + ) + (return) + ) + ) + ) + (block + (i32.store + (i32.load + (global.get $__asyncify_data) + ) + (local.get $0) + ) + (i32.store + (global.get $__asyncify_data) + (i32.add + (i32.load + (global.get $__asyncify_data) + ) + (i32.const 4) + ) + ) + ) + (nop) + ) + (func $bar (; 2 ;) (type $FUNCSIG$v) + (local $0 i32) + (local $1 i32) + (if + (i32.eq + (global.get $__asyncify_state) + (i32.const 2) + ) + (nop) + ) + (local.set $0 + (block $__asyncify_unwind (result i32) + (block + (block + (if + (i32.eq + (global.get $__asyncify_state) + (i32.const 2) + ) + (block + (i32.store + (global.get $__asyncify_data) + (i32.add + (i32.load + (global.get $__asyncify_data) + ) + (i32.const -4) + ) + ) + (local.set $1 + (i32.load + (i32.load + (global.get $__asyncify_data) + ) + ) + ) + ) + ) + (block + (if + (if (result i32) + (i32.eq + (global.get $__asyncify_state) + (i32.const 0) + ) + (i32.const 1) + (i32.eq + (local.get $1) + (i32.const 0) + ) + ) + (block + (call $import) + (if + (i32.eq + (global.get $__asyncify_state) + (i32.const 1) + ) + (br $__asyncify_unwind + (i32.const 0) + ) + ) + ) + ) + (if + (i32.eq + (global.get $__asyncify_state) + (i32.const 0) + ) + (nop) + ) + ) + ) + (return) + ) + ) + ) + (block + (i32.store + (i32.load + (global.get $__asyncify_data) + ) + (local.get $0) + ) + (i32.store + (global.get $__asyncify_data) + (i32.add + (i32.load + (global.get $__asyncify_data) + ) + (i32.const 4) + ) + ) + ) + (nop) + ) + (func $baz (; 3 ;) (type $FUNCSIG$v) + (call $import) + (nop) + ) + (func $other1 (; 4 ;) (type $FUNCSIG$v) + (call $foo) + (nop) + ) + (func $other2 (; 5 ;) (type $FUNCSIG$v) + (call $baz) + (nop) + ) + (func $asyncify_start_unwind (; 6 ;) (param $0 i32) + (global.set $__asyncify_state + (i32.const 1) + ) + (global.set $__asyncify_data + (local.get $0) + ) + (if + (i32.gt_u + (i32.load + (global.get $__asyncify_data) + ) + (i32.load offset=4 + (global.get $__asyncify_data) + ) + ) + (unreachable) + ) + ) + (func $asyncify_stop_unwind (; 7 ;) + (global.set $__asyncify_state + (i32.const 0) + ) + (if + (i32.gt_u + (i32.load + (global.get $__asyncify_data) + ) + (i32.load offset=4 + (global.get $__asyncify_data) + ) + ) + (unreachable) + ) + ) + (func $asyncify_start_rewind (; 8 ;) (param $0 i32) + (global.set $__asyncify_state + (i32.const 2) + ) + (global.set $__asyncify_data + (local.get $0) + ) + (if + (i32.gt_u + (i32.load + (global.get $__asyncify_data) + ) + (i32.load offset=4 + (global.get $__asyncify_data) + ) + ) + (unreachable) + ) + ) + (func $asyncify_stop_rewind (; 9 ;) + (global.set $__asyncify_state + (i32.const 0) + ) + (if + (i32.gt_u + (i32.load + (global.get $__asyncify_data) + ) + (i32.load offset=4 + (global.get $__asyncify_data) + ) + ) + (unreachable) + ) + ) +) diff --git a/test/passes/asyncify_pass-arg=asyncify-whitelist@foo,bar.wast b/test/passes/asyncify_pass-arg=asyncify-whitelist@foo,bar.wast new file mode 100644 index 000000000..355ca1b11 --- /dev/null +++ b/test/passes/asyncify_pass-arg=asyncify-whitelist@foo,bar.wast @@ -0,0 +1,20 @@ +(module + (memory 1 2) + (import "env" "import" (func $import)) + (func $foo + (call $import) + ) + (func $bar + (call $import) + ) + (func $baz + (call $import) + ) + (func $other1 + (call $foo) ;; even though we call foo, we are not in the whitelist, so do not instrument us + ) + (func $other2 + (call $baz) + ) +) + diff --git a/test/unit/test_asyncify.py b/test/unit/test_asyncify.py index 479d24cab..40ee160d2 100644 --- a/test/unit/test_asyncify.py +++ b/test/unit/test_asyncify.py @@ -1,4 +1,5 @@ import os +import subprocess from scripts.test.shared import WASM_OPT, WASM_DIS, WASM_SHELL, NODEJS, run_process from utils import BinaryenTestCase @@ -27,3 +28,23 @@ class AsyncifyTest(BinaryenTestCase): output = run_process(WASM_SHELL + ['a.wast'], capture_output=True).stdout with open(self.input_path('asyncify-pure.txt')) as f: self.assertEqual(f.read(), output) + + def test_asyncify_list_bad(self): + for arg, warning in [ + ('--pass-arg=asyncify-blacklist@nonexistent', 'nonexistent'), + ('--pass-arg=asyncify-whitelist@nonexistent', 'nonexistent'), + ('--pass-arg=asyncify-blacklist@main', None), + ('--pass-arg=asyncify-whitelist@main', None), + ]: + print(arg, warning) + err = run_process(WASM_OPT + [self.input_path('asyncify-pure.wast'), '--asyncify', arg], stdout=subprocess.PIPE, stderr=subprocess.PIPE).stderr.strip() + if warning: + self.assertIn('warning', err) + self.assertIn(warning, err) + else: + self.assertNotIn('warning', err) + + def test_asyncify_blacklist_and_whitelist(self): + proc = run_process(WASM_OPT + [self.input_path('asyncify-pure.wast'), '--asyncify', '--pass-arg=asyncify-whitelist@main', '--pass-arg=asyncify-blacklist@main'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False) + self.assertNotEqual(proc.returncode, 0, 'must error on using both lists at once') + self.assertIn('It makes no sense to use both a blacklist and a whitelist with asyncify', proc.stdout) |