GeezerVison is showing github.com — src: do not enable wasm trap handler if there's not enough vmem · nodejs/node@79262ff
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/cli.md +30-34Lines changed: 30 additions & 34 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -754,40 +754,36 @@ vm.measureMemory(); | 754 | 754 | added: | 755 | 755 | - v22.2.0 | 756 | 756 | - v20.15.0 | 757 | | ---> | 758 | | - | 759 | | -By default, Node.js enables trap-handler-based WebAssembly bound | 760 | | -checks. As a result, V8 does not need to insert inline bound checks | 761 | | -in the code compiled from WebAssembly which may speed up WebAssembly | 762 | | -execution significantly, but this optimization requires allocating | 763 | | -a big virtual memory cage (currently 10GB). If the Node.js process | 764 | | -does not have access to a large enough virtual memory address space | 765 | | -due to system configurations or hardware limitations, users won't | 766 | | -be able to run any WebAssembly that involves allocation in this | 767 | | -virtual memory cage and will see an out-of-memory error. | 768 | | - | 769 | | -```console | 770 | | -$ ulimit -v 5000000 | 771 | | -$ node -p "new WebAssembly.Memory({ initial: 10, maximum: 100 });" | 772 | | -[eval]:1 | 773 | | -new WebAssembly.Memory({ initial: 10, maximum: 100 }); | 774 | | -^ | 775 | | - | 776 | | -RangeError: WebAssembly.Memory(): could not allocate memory | 777 | | - at [eval]:1:1 | 778 | | - at runScriptInThisContext (node:internal/vm:209:10) | 779 | | - at node:internal/process/execution:118:14 | 780 | | - at [eval]-wrapper:6:24 | 781 | | - at runScript (node:internal/process/execution:101:62) | 782 | | - at evalScript (node:internal/process/execution:136:3) | 783 | | - at node:internal/main/eval_string:49:3 | 784 | | - | 785 | | -``` | 786 | | - | 787 | | -`--disable-wasm-trap-handler` disables this optimization so that | 788 | | -users can at least run WebAssembly (with less optimal performance) | 789 | | -when the virtual memory address space available to their Node.js | 790 | | -process is lower than what the V8 WebAssembly memory cage needs. | | 757 | +changes: | | 758 | + - version: | | 759 | + - REPLACEME | | 760 | + pr-url: https://github.com/nodejs/node/pull/62132 | | 761 | + description: Node.js now automatically disables the trap handler when there is not | | 762 | + enough virtual memory available at startup to allocate one cage. | | 763 | +--> | | 764 | + | | 765 | +Node.js enables V8's trap-handler-based WebAssembly bound checks on 64-bit platforms, | | 766 | +which significantly improves WebAssembly performance by eliminating the need for | | 767 | +inline bound checks. This optimization requires allocating a large virtual memory | | 768 | +cage per WebAssembly memory instance (currently typically 8GB for 32-bit WebAssembly memory, | | 769 | +16GB for 64-bit WebAssembly memory) to trap out-of-bound accesses. On most 64-bit | | 770 | +platforms, the virtual memory address space is usually large enough (around 128TB) | | 771 | +to accommodate typical WebAssembly usages, but if the machine has manual limits | | 772 | +on virtual memory (e.g. through `ulimit -v`), WebAssembly memory allocation is | | 773 | +more likely to fail with `WebAssembly.Memory(): could not allocate memory`. | | 774 | + | | 775 | +At startup, Node.js automatically checks whether there is enough virtual memory | | 776 | +available to allocate at least one cage, and if not, the trap-handler optimization | | 777 | +is automatically disabled so that WebAssembly can still run using inline | | 778 | +bound checks (with less optimal performance). But if the application needs to create | | 779 | +many WebAssembly memory instances and the machine still configures a relatively high | | 780 | +limit on virtual memory, allocation of WebAssembly memory instances may still fail | | 781 | +more quickly than expected due to the raised virtual memory usage. | | 782 | + | | 783 | +`--disable-wasm-trap-handler` fully disables this optimization so that WebAssembly memory | | 784 | +instances always use inline bound checks instead of reserving large virtual memory cages. | | 785 | +This allows more instances to be created when the virtual memory address space available | | 786 | +to the Node.js process is limited. | 791 | 787 |
| 792 | 788 | ### `--disallow-code-generation-from-strings` | 793 | 789 |
| |
Collapse file Copy file name to clipboardExpand all lines: src/debug_utils.h +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -44,6 +44,7 @@ void NODE_EXTERN_PRIVATE FWrite(FILE* file, const std::string& str); | 44 | 44 | // from a provider type to a debug category. | 45 | 45 | #define DEBUG_CATEGORY_NAMES(V) \ | 46 | 46 | NODE_ASYNC_PROVIDER_TYPES(V) \ | | 47 | + V(BOOTSTRAP) \ | 47 | 48 | V(CRYPTO) \ | 48 | 49 | V(COMPILE_CACHE) \ | 49 | 50 | V(DIAGNOSTICS) \ | |
Collapse file Copy file name to clipboardExpand all lines: src/node.cc +42-1Lines changed: 42 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -1046,6 +1046,45 @@ static ExitCode InitializeNodeWithArgsInternal( | 1046 | 1046 | return ExitCode::kNoFailure; | 1047 | 1047 | } | 1048 | 1048 |
| | 1049 | +#if NODE_USE_V8_WASM_TRAP_HANDLER | | 1050 | +bool CanEnableWebAssemblyTrapHandler() { | | 1051 | +// On POSIX, the machine may have a limit on the amount of virtual memory | | 1052 | +// available, if it's not enough to allocate at least one cage for WASM, | | 1053 | +// then the trap-handler-based bound checks cannot be used. | | 1054 | +#ifdef __POSIX__ | | 1055 | + struct rlimit lim; | | 1056 | + if (getrlimit(RLIMIT_AS, &lim) != 0 || lim.rlim_cur == RLIM_INFINITY) { | | 1057 | + // Can't get the limit or there's no limit, assume trap handler can be | | 1058 | + // enabled. | | 1059 | + return true; | | 1060 | + } | | 1061 | + uint64_t virtual_memory_available = static_cast<uint64_t>(lim.rlim_cur); | | 1062 | + | | 1063 | + size_t byte_capacity = 64 * 1024; // 64KB, the minimum size of a WASM memory. | | 1064 | + uint64_t cage_size_needed_32 = V8::GetWasmMemoryReservationSizeInBytes( | | 1065 | + V8::WasmMemoryType::kMemory32, byte_capacity); | | 1066 | + uint64_t cage_size_needed_64 = V8::GetWasmMemoryReservationSizeInBytes( | | 1067 | + V8::WasmMemoryType::kMemory64, byte_capacity); | | 1068 | + uint64_t cage_size_needed = | | 1069 | + std::max(cage_size_needed_32, cage_size_needed_64); | | 1070 | + bool can_enable = virtual_memory_available >= cage_size_needed; | | 1071 | + per_process::Debug(DebugCategory::BOOTSTRAP, | | 1072 | + "Virtual memory available: %" PRIu64 " bytes,\n" | | 1073 | + "cage size needed for 32-bit: %" PRIu64 " bytes,\n" | | 1074 | + "cage size needed for 64-bit: %" PRIu64 " bytes,\n" | | 1075 | + "Can%senable WASM trap handler\n", | | 1076 | + virtual_memory_available, | | 1077 | + cage_size_needed_32, | | 1078 | + cage_size_needed_64, | | 1079 | + can_enable ? " " : " not "); | | 1080 | + | | 1081 | + return can_enable; | | 1082 | +#else | | 1083 | + return true; | | 1084 | +#endif // __POSIX__ | | 1085 | +} | | 1086 | +#endif // NODE_USE_V8_WASM_TRAP_HANDLER | | 1087 | + | 1049 | 1088 | static std::shared_ptr<InitializationResultImpl> | 1050 | 1089 | InitializeOncePerProcessInternal(const std::vector<std::string>& args, | 1051 | 1090 | ProcessInitializationFlags::Flags flags = | @@ -1248,7 +1287,9 @@ InitializeOncePerProcessInternal(const std::vector<std::string>& args, | 1248 | 1287 | bool use_wasm_trap_handler = | 1249 | 1288 | !per_process::cli_options->disable_wasm_trap_handler; | 1250 | 1289 | if (!(flags & ProcessInitializationFlags::kNoDefaultSignalHandling) && | 1251 | | - use_wasm_trap_handler) { | | 1290 | + use_wasm_trap_handler && CanEnableWebAssemblyTrapHandler()) { | | 1291 | + per_process::Debug(DebugCategory::BOOTSTRAP, | | 1292 | + "Enabling WebAssembly trap handler for bounds checks\n"); | 1252 | 1293 | #if defined(_WIN32) | 1253 | 1294 | constexpr ULONG first = TRUE; | 1254 | 1295 | per_process::old_vectored_exception_handler = | |
Collapse file Copy file name to clipboardExpand all lines: test/testpy/__init__.py +5-12Lines changed: 5 additions & 12 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 36 | 36 | LS_RE = re.compile(r'^test-.*\.m?js$') | 37 | 37 | ENV_PATTERN = re.compile(r"//\s+Env:(.*)") | 38 | 38 | NODE_TEST_PATTERN = re.compile(r"('|`|\")node:test\1") | | 39 | +RLIMIT_AS_PATTERN = re.compile(r"//\s+RLIMIT_AS:\s*(\d+)") | 39 | 40 |
| 40 | 41 | class SimpleTestCase(test.TestCase): | 41 | 42 |
| @@ -99,6 +100,10 @@ def GetRunConfiguration(self): | 99 | 100 | else: | 100 | 101 | result += flags | 101 | 102 |
| | 103 | + rlimit_as_match = RLIMIT_AS_PATTERN.search(source) | | 104 | + if rlimit_as_match: | | 105 | + self.max_virtual_memory = int(rlimit_as_match.group(1)) | | 106 | + | 102 | 107 | if self.context.use_error_reporter and NODE_TEST_PATTERN.search(source): | 103 | 108 | result += ['--test-reporter=./test/common/test-error-reporter.js', | 104 | 109 | '--test-reporter-destination=stdout'] | @@ -189,15 +194,3 @@ def ListTests(self, current_path, path, arch, mode): | 189 | 194 | for tst in result: | 190 | 195 | tst.disable_core_files = True | 191 | 196 | return result | 192 | | - | 193 | | -class WasmAllocationTestConfiguration(SimpleTestConfiguration): | 194 | | - def __init__(self, context, root, section, additional=None): | 195 | | - super(WasmAllocationTestConfiguration, self).__init__(context, root, section, | 196 | | - additional) | 197 | | - | 198 | | - def ListTests(self, current_path, path, arch, mode): | 199 | | - result = super(WasmAllocationTestConfiguration, self).ListTests( | 200 | | - current_path, path, arch, mode) | 201 | | - for tst in result: | 202 | | - tst.max_virtual_memory = 5 * 1024 * 1024 * 1024 # 5GB | 203 | | - return result |
Collapse file Copy file name to clipboard +12Lines changed: 12 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +// RLIMIT_AS: 3221225472 | | 2 | +// When the virtual memory limit is 3GB, there is not enough virtual memory for | | 3 | +// even one wasm cage. In this case Node.js should automatically adapt and | | 4 | +// skip enabling trap-based bounds checks, so that WASM can at least run with | | 5 | +// inline bound checks. | | 6 | +'use strict'; | | 7 | + | | 8 | +require('../common'); | | 9 | +new WebAssembly.Memory({ initial: 10, maximum: 100 }); | | 10 | + | | 11 | +// Test memory64 works too. | | 12 | +new WebAssembly.Memory({ address: 'i64', initial: 10n, maximum: 100n }); |
Collapse file Copy file name to clipboard +20Lines changed: 20 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +// Flags: --disable-wasm-trap-handler | | 2 | +// RLIMIT_AS: 34359738368 | | 3 | + | | 4 | +// 32GB should be enough for at least 2 cages, but not enough for 30. | | 5 | + | | 6 | +// Test that with limited virtual memory space, --disable-wasm-trap-handler | | 7 | +// fully disables trap-based bounds checks, and thus allows WASM to run with | | 8 | +// inline bound checks. | | 9 | +'use strict'; | | 10 | + | | 11 | +require('../common'); | | 12 | +const instances = []; | | 13 | +for (let i = 0; i < 30; i++) { | | 14 | + instances.push(new WebAssembly.Memory({ initial: 10, maximum: 100 })); | | 15 | +} | | 16 | + | | 17 | +// Test memory64 works too. | | 18 | +for (let i = 0; i < 30; i++) { | | 19 | + instances.push(new WebAssembly.Memory({ initial: 10n, maximum: 100n, address: 'i64' })); | | 20 | +} |
Collapse file Copy file name to clipboard +22Lines changed: 22 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +// RLIMIT_AS: 21474836480 | | 2 | +// With 20GB virtual memory, there's enough space for the first wasm memory64 | | 3 | +// allocation to succeed, but not enough for subsequent ones since each | | 4 | +// wasm memory64 with guard regions reserves 16GB of virtual address space. | | 5 | +'use strict'; | | 6 | + | | 7 | +require('../common'); | | 8 | +const assert = require('assert'); | | 9 | + | | 10 | +// The first allocation should succeed. | | 11 | +const first = new WebAssembly.Memory({ address: 'i64', initial: 10n, maximum: 100n }); | | 12 | +assert(first); | | 13 | + | | 14 | +// Subsequent allocations should eventually fail due to running out of | | 15 | +// virtual address space. memory64 reserves 16GB per allocation (vs 8GB for | | 16 | +// memory32), so the limit is reached even faster. | | 17 | +assert.throws(() => { | | 18 | + const instances = [first]; | | 19 | + for (let i = 1; i < 30; i++) { | | 20 | + instances.push(new WebAssembly.Memory({ address: 'i64', initial: 10n, maximum: 100n })); | | 21 | + } | | 22 | +}, /WebAssembly\.Memory/); |
Collapse file Copy file name to clipboardExpand all lines: test/wasm-allocation/test-wasm-allocation.js +4-3Lines changed: 4 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | | -// Flags: --disable-wasm-trap-handler | 2 | | -// Test that with limited virtual memory space, --disable-wasm-trap-handler | 3 | | -// allows WASM to at least run with inline bound checks. | | 1 | +// RLIMIT_AS: 21474836480 | | 2 | +// With 20GB virtual memory, there's enough space for the first few wasm memory | | 3 | +// allocation to succeed, but not enough for many subsequent ones since each | | 4 | +// wasm memory32 with guard regions reserves 8GB of virtual address space. | 4 | 5 | 'use strict'; | 5 | 6 |
| 6 | 7 | require('../common'); | |
Collapse file Copy file name to clipboardExpand all lines: test/wasm-allocation/testcfg.py +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 3 | 3 | import testpy | 4 | 4 |
| 5 | 5 | def GetConfiguration(context, root): | 6 | | - return testpy.WasmAllocationTestConfiguration(context, root, 'wasm-allocation') | | 6 | + return testpy.SimpleTestConfiguration(context, root, 'wasm-allocation') |
Collapse file Copy file name to clipboardExpand all lines: test/wasm-allocation/wasm-allocation.status +6Lines changed: 6 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -8,3 +8,9 @@ prefix wasm-allocation | 8 | 8 |
| 9 | 9 | [$system!=linux || $asan==on || $pointer_compression==on] | 10 | 10 | test-wasm-allocation: SKIP | | 11 | +test-wasm-allocation-auto-adapt: SKIP | | 12 | +test-wasm-allocation-memory64: SKIP | | 13 | + | | 14 | +[$arch!=x64 && $arch!=arm64] | | 15 | +test-wasm-allocation: SKIP | | 16 | +test-wasm-allocation-memory64: SKIP |
|