Skip to content

Commit 77fabfb

Browse files
committed
cli: allow running wasm in limited vmem with --disable-wasm-trap-handler
By default, Node.js enables trap-handler-based WebAssembly bound checks. As a result, V8 does not need to insert inline bound checks int the code compiled from WebAssembly which may speedup WebAssembly execution significantly, but this optimization requires allocating a big virtual memory cage (currently 10GB). If the Node.js process does not have access to a large enough virtual memory address space due to system configurations or hardware limitations, users won't be able to run any WebAssembly that involves allocation in this virtual memory cage and will see an out-of-memory error. ```console $ ulimit -v 5000000 $ node -p "new WebAssembly.Memory({ initial: 10, maximum: 100 });" [eval]:1 new WebAssembly.Memory({ initial: 10, maximum: 100 }); ^ RangeError: WebAssembly.Memory(): could not allocate memory at [eval]:1:1 at runScriptInThisContext (node:internal/vm:209:10) at node:internal/process/execution:118:14 at [eval]-wrapper:6:24 at runScript (node:internal/process/execution:101:62) at evalScript (node:internal/process/execution:136:3) at node:internal/main/eval_string:49:3 ``` `--disable-wasm-trap-handler` disables this optimization so that users can at least run WebAssembly (with a less optimial performance) when the virtual memory address space available to their Node.js process is lower than what the V8 WebAssembly memory cage needs. PR-URL: #52766 Reviewed-By: Rafael Gonzaga <[email protected]>
1 parent a1770d4 commit 77fabfb

File tree

9 files changed

+127
-31
lines changed

9 files changed

+127
-31
lines changed

Diff for: doc/api/cli.md

+40
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,45 @@ const vm = require('node:vm');
565565
vm.measureMemory();
566566
```
567567

568+
### `--disable-wasm-trap-handler`
569+
570+
<!-- YAML
571+
added: REPLACEME
572+
-->
573+
574+
By default, Node.js enables trap-handler-based WebAssembly bound
575+
checks. As a result, V8 does not need to insert inline bound checks
576+
int the code compiled from WebAssembly which may speedup WebAssembly
577+
execution significantly, but this optimization requires allocating
578+
a big virtual memory cage (currently 10GB). If the Node.js process
579+
does not have access to a large enough virtual memory address space
580+
due to system configurations or hardware limitations, users won't
581+
be able to run any WebAssembly that involves allocation in this
582+
virtual memory cage and will see an out-of-memory error.
583+
584+
```console
585+
$ ulimit -v 5000000
586+
$ node -p "new WebAssembly.Memory({ initial: 10, maximum: 100 });"
587+
[eval]:1
588+
new WebAssembly.Memory({ initial: 10, maximum: 100 });
589+
^
590+
591+
RangeError: WebAssembly.Memory(): could not allocate memory
592+
at [eval]:1:1
593+
at runScriptInThisContext (node:internal/vm:209:10)
594+
at node:internal/process/execution:118:14
595+
at [eval]-wrapper:6:24
596+
at runScript (node:internal/process/execution:101:62)
597+
at evalScript (node:internal/process/execution:136:3)
598+
at node:internal/main/eval_string:49:3
599+
600+
```
601+
602+
`--disable-wasm-trap-handler` disables this optimization so that
603+
users can at least run WebAssembly (with less optimal performance)
604+
when the virtual memory address space available to their Node.js
605+
process is lower than what the V8 WebAssembly memory cage needs.
606+
568607
### `--disable-proto=mode`
569608

570609
<!-- YAML
@@ -2587,6 +2626,7 @@ one is included in the list below.
25872626
* `--diagnostic-dir`
25882627
* `--disable-proto`
25892628
* `--disable-warning`
2629+
* `--disable-wasm-trap-handler`
25902630
* `--dns-result-order`
25912631
* `--enable-fips`
25922632
* `--enable-network-family-autoselection`

Diff for: doc/node.1

+5
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ is `delete`, the property will be removed entirely. If
142142
is `throw`, accesses to the property will throw an exception with the code
143143
`ERR_PROTO_ACCESS`.
144144
.
145+
.It Fl -disable-wasm-trap-handler Ns = Ns Ar mode
146+
Disable trap-handler-based WebAssembly bound checks and fall back to
147+
inline bound checks so that WebAssembly can be run with limited virtual
148+
memory.
149+
.
145150
.It Fl -disallow-code-generation-from-strings
146151
Make built-in language features like `eval` and `new Function` that generate
147152
code from strings throw an exception instead. This does not affect the Node.js

Diff for: src/node.cc

+38-31
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
376376
typedef void (*sigaction_cb)(int signo, siginfo_t* info, void* ucontext);
377377
#endif
378378
#if NODE_USE_V8_WASM_TRAP_HANDLER
379+
static std::atomic<bool> is_wasm_trap_handler_configured{false};
379380
#if defined(_WIN32)
380381
static LONG WINAPI TrapWebAssemblyOrContinue(EXCEPTION_POINTERS* exception) {
381382
if (v8::TryHandleWebAssemblyTrapWindows(exception)) {
@@ -421,15 +422,17 @@ void RegisterSignalHandler(int signal,
421422
bool reset_handler) {
422423
CHECK_NOT_NULL(handler);
423424
#if NODE_USE_V8_WASM_TRAP_HANDLER
424-
if (signal == SIGSEGV) {
425+
// Stash the user-registered handlers for TrapWebAssemblyOrContinue
426+
// to call out to when the signal is not coming from a WASM OOM.
427+
if (signal == SIGSEGV && is_wasm_trap_handler_configured.load()) {
425428
CHECK(previous_sigsegv_action.is_lock_free());
426429
CHECK(!reset_handler);
427430
previous_sigsegv_action.store(handler);
428431
return;
429432
}
430-
// TODO(align behavior between macos and other in next major version)
433+
// TODO(align behavior between macos and other in next major version)
431434
#if defined(__APPLE__)
432-
if (signal == SIGBUS) {
435+
if (signal == SIGBUS && is_wasm_trap_handler_configured.load()) {
433436
CHECK(previous_sigbus_action.is_lock_free());
434437
CHECK(!reset_handler);
435438
previous_sigbus_action.store(handler);
@@ -581,25 +584,6 @@ static void PlatformInit(ProcessInitializationFlags::Flags flags) {
581584
if (!(flags & ProcessInitializationFlags::kNoDefaultSignalHandling)) {
582585
RegisterSignalHandler(SIGINT, SignalExit, true);
583586
RegisterSignalHandler(SIGTERM, SignalExit, true);
584-
585-
#if NODE_USE_V8_WASM_TRAP_HANDLER
586-
// Tell V8 to disable emitting WebAssembly
587-
// memory bounds checks. This means that we have
588-
// to catch the SIGSEGV/SIGBUS in TrapWebAssemblyOrContinue
589-
// and pass the signal context to V8.
590-
{
591-
struct sigaction sa;
592-
memset(&sa, 0, sizeof(sa));
593-
sa.sa_sigaction = TrapWebAssemblyOrContinue;
594-
sa.sa_flags = SA_SIGINFO;
595-
CHECK_EQ(sigaction(SIGSEGV, &sa, nullptr), 0);
596-
// TODO(align behavior between macos and other in next major version)
597-
#if defined(__APPLE__)
598-
CHECK_EQ(sigaction(SIGBUS, &sa, nullptr), 0);
599-
#endif
600-
}
601-
V8::EnableWebAssemblyTrapHandler(false);
602-
#endif // NODE_USE_V8_WASM_TRAP_HANDLER
603587
}
604588

605589
if (!(flags & ProcessInitializationFlags::kNoAdjustResourceLimits)) {
@@ -626,14 +610,6 @@ static void PlatformInit(ProcessInitializationFlags::Flags flags) {
626610
}
627611
#endif // __POSIX__
628612
#ifdef _WIN32
629-
#ifdef NODE_USE_V8_WASM_TRAP_HANDLER
630-
{
631-
constexpr ULONG first = TRUE;
632-
per_process::old_vectored_exception_handler =
633-
AddVectoredExceptionHandler(first, TrapWebAssemblyOrContinue);
634-
}
635-
V8::EnableWebAssemblyTrapHandler(false);
636-
#endif // NODE_USE_V8_WASM_TRAP_HANDLER
637613
if (!(flags & ProcessInitializationFlags::kNoStdioInitialization)) {
638614
for (int fd = 0; fd <= 2; ++fd) {
639615
auto handle = reinterpret_cast<HANDLE>(_get_osfhandle(fd));
@@ -1176,6 +1152,37 @@ InitializeOncePerProcessInternal(const std::vector<std::string>& args,
11761152
cppgc::InitializeProcess(allocator);
11771153
}
11781154

1155+
#if NODE_USE_V8_WASM_TRAP_HANDLER
1156+
bool use_wasm_trap_handler =
1157+
!per_process::cli_options->disable_wasm_trap_handler;
1158+
if (!(flags & ProcessInitializationFlags::kNoDefaultSignalHandling) &&
1159+
use_wasm_trap_handler) {
1160+
#if defined(_WIN32)
1161+
constexpr ULONG first = TRUE;
1162+
per_process::old_vectored_exception_handler =
1163+
AddVectoredExceptionHandler(first, TrapWebAssemblyOrContinue);
1164+
#else
1165+
// Tell V8 to disable emitting WebAssembly
1166+
// memory bounds checks. This means that we have
1167+
// to catch the SIGSEGV/SIGBUS in TrapWebAssemblyOrContinue
1168+
// and pass the signal context to V8.
1169+
{
1170+
struct sigaction sa;
1171+
memset(&sa, 0, sizeof(sa));
1172+
sa.sa_sigaction = TrapWebAssemblyOrContinue;
1173+
sa.sa_flags = SA_SIGINFO;
1174+
CHECK_EQ(sigaction(SIGSEGV, &sa, nullptr), 0);
1175+
// TODO(align behavior between macos and other in next major version)
1176+
#if defined(__APPLE__)
1177+
CHECK_EQ(sigaction(SIGBUS, &sa, nullptr), 0);
1178+
#endif
1179+
}
1180+
#endif // defined(_WIN32)
1181+
is_wasm_trap_handler_configured.store(true);
1182+
V8::EnableWebAssemblyTrapHandler(false);
1183+
}
1184+
#endif // NODE_USE_V8_WASM_TRAP_HANDLER
1185+
11791186
performance::performance_v8_start = PERFORMANCE_NOW();
11801187
per_process::v8_initialized = true;
11811188

@@ -1205,7 +1212,7 @@ void TearDownOncePerProcess() {
12051212
}
12061213

12071214
#if NODE_USE_V8_WASM_TRAP_HANDLER && defined(_WIN32)
1208-
if (!(flags & ProcessInitializationFlags::kNoDefaultSignalHandling)) {
1215+
if (is_wasm_trap_handler_configured.load()) {
12091216
RemoveVectoredExceptionHandler(per_process::old_vectored_exception_handler);
12101217
}
12111218
#endif

Diff for: src/node_options.cc

+7
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,13 @@ PerProcessOptionsParser::PerProcessOptionsParser(
10501050
AddOption("--run",
10511051
"Run a script specified in package.json",
10521052
&PerProcessOptions::run);
1053+
AddOption(
1054+
"--disable-wasm-trap-handler",
1055+
"Disable trap-handler-based WebAssembly bound checks. V8 will insert "
1056+
"inline bound checks when compiling WebAssembly which may slow down "
1057+
"performance.",
1058+
&PerProcessOptions::disable_wasm_trap_handler,
1059+
kAllowedInEnvvar);
10531060
}
10541061

10551062
inline std::string RemoveBrackets(const std::string& host) {

Diff for: src/node_options.h

+2
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,8 @@ class PerProcessOptions : public Options {
304304
bool openssl_shared_config = false;
305305
#endif
306306

307+
bool disable_wasm_trap_handler = false;
308+
307309
// Per-process because reports can be triggered outside a known V8 context.
308310
bool report_on_fatalerror = false;
309311
bool report_compact = false;

Diff for: test/testpy/__init__.py

+12
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,15 @@ def ListTests(self, current_path, path, arch, mode):
167167
for tst in result:
168168
tst.disable_core_files = True
169169
return result
170+
171+
class WasmAllocationTestConfiguration(SimpleTestConfiguration):
172+
def __init__(self, context, root, section, additional=None):
173+
super(WasmAllocationTestConfiguration, self).__init__(context, root, section,
174+
additional)
175+
176+
def ListTests(self, current_path, path, arch, mode):
177+
result = super(WasmAllocationTestConfiguration, self).ListTests(
178+
current_path, path, arch, mode)
179+
for tst in result:
180+
tst.max_virtual_memory = 5 * 1024 * 1024 * 1024 # 5GB
181+
return result

Diff for: test/wasm-allocation/test-wasm-allocation.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
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.
4+
'use strict';
5+
6+
require('../common');
7+
new WebAssembly.Memory({ initial: 10, maximum: 100 });

Diff for: test/wasm-allocation/testcfg.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import sys, os
2+
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
3+
import testpy
4+
5+
def GetConfiguration(context, root):
6+
return testpy.WasmAllocationTestConfiguration(context, root, 'wasm-allocation')

Diff for: test/wasm-allocation/wasm-allocation.status

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
prefix wasm-allocation
2+
3+
# To mark a test as flaky, list the test name in the appropriate section
4+
# below, without ".js", followed by ": PASS,FLAKY". Example:
5+
# sample-test : PASS,FLAKY
6+
7+
[true] # This section applies to all platforms
8+
9+
[$system!=linux || $asan==on]
10+
test-wasm-allocation: SKIP

0 commit comments

Comments
 (0)