Skip to content

Commit b459d10

Browse files
joyeecheungaduh95
authored andcommitted
src: do not enable wasm trap handler if there's not enough vmem
PR-URL: #62132 Backport-PR-URL: #64338 Refs: microsoft/vscode#251777 Refs: https://chromium-review.googlesource.com/c/v8/v8/+/7638233 Signed-off-by: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: Richard Lau <richard.lau@ibm.com>
1 parent 38e03df commit b459d10

10 files changed

Lines changed: 157 additions & 52 deletions

doc/api/cli.md

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -695,40 +695,36 @@ vm.measureMemory();
695695
added:
696696
- v22.2.0
697697
- v20.15.0
698-
-->
699-
700-
By default, Node.js enables trap-handler-based WebAssembly bound
701-
checks. As a result, V8 does not need to insert inline bound checks
702-
in the code compiled from WebAssembly which may speed up WebAssembly
703-
execution significantly, but this optimization requires allocating
704-
a big virtual memory cage (currently 10GB). If the Node.js process
705-
does not have access to a large enough virtual memory address space
706-
due to system configurations or hardware limitations, users won't
707-
be able to run any WebAssembly that involves allocation in this
708-
virtual memory cage and will see an out-of-memory error.
709-
710-
```console
711-
$ ulimit -v 5000000
712-
$ node -p "new WebAssembly.Memory({ initial: 10, maximum: 100 });"
713-
[eval]:1
714-
new WebAssembly.Memory({ initial: 10, maximum: 100 });
715-
^
716-
717-
RangeError: WebAssembly.Memory(): could not allocate memory
718-
at [eval]:1:1
719-
at runScriptInThisContext (node:internal/vm:209:10)
720-
at node:internal/process/execution:118:14
721-
at [eval]-wrapper:6:24
722-
at runScript (node:internal/process/execution:101:62)
723-
at evalScript (node:internal/process/execution:136:3)
724-
at node:internal/main/eval_string:49:3
725-
726-
```
727-
728-
`--disable-wasm-trap-handler` disables this optimization so that
729-
users can at least run WebAssembly (with less optimal performance)
730-
when the virtual memory address space available to their Node.js
731-
process is lower than what the V8 WebAssembly memory cage needs.
698+
changes:
699+
- version:
700+
- REPLACEME
701+
pr-url: https://github.com/nodejs/node/pull/62132
702+
description: Node.js now automatically disables the trap handler when there is not
703+
enough virtual memory available at startup to allocate one cage.
704+
-->
705+
706+
Node.js enables V8's trap-handler-based WebAssembly bound checks on 64-bit platforms,
707+
which significantly improves WebAssembly performance by eliminating the need for
708+
inline bound checks. This optimization requires allocating a large virtual memory
709+
cage per WebAssembly memory instance (currently typically 8GB for 32-bit WebAssembly memory,
710+
16GB for 64-bit WebAssembly memory) to trap out-of-bound accesses. On most 64-bit
711+
platforms, the virtual memory address space is usually large enough (around 128TB)
712+
to accommodate typical WebAssembly usages, but if the machine has manual limits
713+
on virtual memory (e.g. through `ulimit -v`), WebAssembly memory allocation is
714+
more likely to fail with `WebAssembly.Memory(): could not allocate memory`.
715+
716+
At startup, Node.js automatically checks whether there is enough virtual memory
717+
available to allocate at least one cage, and if not, the trap-handler optimization
718+
is automatically disabled so that WebAssembly can still run using inline
719+
bound checks (with less optimal performance). But if the application needs to create
720+
many WebAssembly memory instances and the machine still configures a relatively high
721+
limit on virtual memory, allocation of WebAssembly memory instances may still fail
722+
more quickly than expected due to the raised virtual memory usage.
723+
724+
`--disable-wasm-trap-handler` fully disables this optimization so that WebAssembly memory
725+
instances always use inline bound checks instead of reserving large virtual memory cages.
726+
This allows more instances to be created when the virtual memory address space available
727+
to the Node.js process is limited.
732728

733729
### `--disallow-code-generation-from-strings`
734730

src/debug_utils.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ void NODE_EXTERN_PRIVATE FWrite(FILE* file, const std::string& str);
4444
// from a provider type to a debug category.
4545
#define DEBUG_CATEGORY_NAMES(V) \
4646
NODE_ASYNC_PROVIDER_TYPES(V) \
47+
V(BOOTSTRAP) \
4748
V(CRYPTO) \
4849
V(COMPILE_CACHE) \
4950
V(CONTEXTIFY) \

src/node.cc

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,45 @@ int InitializeNodeWithArgs(std::vector<std::string>* argv,
10661066
InitializeNodeWithArgsInternal(argv, exec_argv, errors, flags));
10671067
}
10681068

1069+
#if NODE_USE_V8_WASM_TRAP_HANDLER
1070+
bool CanEnableWebAssemblyTrapHandler() {
1071+
// On POSIX, the machine may have a limit on the amount of virtual memory
1072+
// available, if it's not enough to allocate at least one cage for WASM,
1073+
// then the trap-handler-based bound checks cannot be used.
1074+
#ifdef __POSIX__
1075+
struct rlimit lim;
1076+
if (getrlimit(RLIMIT_AS, &lim) != 0 || lim.rlim_cur == RLIM_INFINITY) {
1077+
// Can't get the limit or there's no limit, assume trap handler can be
1078+
// enabled.
1079+
return true;
1080+
}
1081+
uint64_t virtual_memory_available = static_cast<uint64_t>(lim.rlim_cur);
1082+
1083+
size_t byte_capacity = 64 * 1024; // 64KB, the minimum size of a WASM memory.
1084+
uint64_t cage_size_needed_32 = V8::GetWasmMemoryReservationSizeInBytes(
1085+
V8::WasmMemoryType::kMemory32, byte_capacity);
1086+
uint64_t cage_size_needed_64 = V8::GetWasmMemoryReservationSizeInBytes(
1087+
V8::WasmMemoryType::kMemory64, byte_capacity);
1088+
uint64_t cage_size_needed =
1089+
std::max(cage_size_needed_32, cage_size_needed_64);
1090+
bool can_enable = virtual_memory_available >= cage_size_needed;
1091+
per_process::Debug(DebugCategory::BOOTSTRAP,
1092+
"Virtual memory available: %" PRIu64 " bytes,\n"
1093+
"cage size needed for 32-bit: %" PRIu64 " bytes,\n"
1094+
"cage size needed for 64-bit: %" PRIu64 " bytes,\n"
1095+
"Can%senable WASM trap handler\n",
1096+
virtual_memory_available,
1097+
cage_size_needed_32,
1098+
cage_size_needed_64,
1099+
can_enable ? " " : " not ");
1100+
1101+
return can_enable;
1102+
#else
1103+
return true;
1104+
#endif // __POSIX__
1105+
}
1106+
#endif // NODE_USE_V8_WASM_TRAP_HANDLER
1107+
10691108
static std::shared_ptr<InitializationResultImpl>
10701109
InitializeOncePerProcessInternal(const std::vector<std::string>& args,
10711110
ProcessInitializationFlags::Flags flags =
@@ -1268,7 +1307,9 @@ InitializeOncePerProcessInternal(const std::vector<std::string>& args,
12681307
bool use_wasm_trap_handler =
12691308
!per_process::cli_options->disable_wasm_trap_handler;
12701309
if (!(flags & ProcessInitializationFlags::kNoDefaultSignalHandling) &&
1271-
use_wasm_trap_handler) {
1310+
use_wasm_trap_handler && CanEnableWebAssemblyTrapHandler()) {
1311+
per_process::Debug(DebugCategory::BOOTSTRAP,
1312+
"Enabling WebAssembly trap handler for bounds checks\n");
12721313
#if defined(_WIN32)
12731314
constexpr ULONG first = TRUE;
12741315
per_process::old_vectored_exception_handler =

test/testpy/__init__.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
LS_RE = re.compile(r'^test-.*\.m?js$')
3737
ENV_PATTERN = re.compile(r"//\s+Env:(.*)")
3838
NODE_TEST_PATTERN = re.compile(r"('|`|\")node:test\1")
39+
RLIMIT_AS_PATTERN = re.compile(r"//\s+RLIMIT_AS:\s*(\d+)")
3940

4041
class SimpleTestCase(test.TestCase):
4142

@@ -99,6 +100,10 @@ def GetRunConfiguration(self):
99100
else:
100101
result += flags
101102

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+
102107
if self.context.use_error_reporter and NODE_TEST_PATTERN.search(source):
103108
result += ['--test-reporter=./test/common/test-error-reporter.js',
104109
'--test-reporter-destination=stdout']
@@ -189,15 +194,3 @@ def ListTests(self, current_path, path, arch, mode):
189194
for tst in result:
190195
tst.disable_core_files = True
191196
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
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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 });
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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/);
Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
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.
45
'use strict';
56

67
require('../common');
7-
new WebAssembly.Memory({ initial: 10, maximum: 100 });
8+
const assert = require('assert');
9+
10+
// The first allocation should succeed.
11+
const first = new WebAssembly.Memory({ initial: 10, maximum: 100 });
12+
assert(first);
13+
14+
// Subsequent allocations should eventually fail due to running out of
15+
// virtual address space.
16+
assert.throws(() => {
17+
const instances = [first];
18+
for (let i = 1; i < 30; i++) {
19+
instances.push(new WebAssembly.Memory({ initial: 10, maximum: 100 }));
20+
}
21+
}, /WebAssembly\.Memory/);

test/wasm-allocation/testcfg.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
import testpy
44

55
def GetConfiguration(context, root):
6-
return testpy.WasmAllocationTestConfiguration(context, root, 'wasm-allocation')
6+
return testpy.SimpleTestConfiguration(context, root, 'wasm-allocation')

test/wasm-allocation/wasm-allocation.status

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,9 @@ prefix wasm-allocation
88

99
[$system!=linux || $asan==on || $pointer_compression==on]
1010
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

0 commit comments

Comments
 (0)