Skip to content

Commit a087263

Browse files
committed
cli: allow running wasm in limited vmemory 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.
1 parent 3d8aa53 commit a087263

File tree

8 files changed

+118
-31
lines changed

8 files changed

+118
-31
lines changed

doc/api/cli.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,41 @@ const vm = require('node:vm');
565565
vm.measureMemory();
566566
```
567567

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

570605
<!-- YAML
@@ -2599,6 +2634,7 @@ one is included in the list below.
25992634
* `--diagnostic-dir`
26002635
* `--disable-proto`
26012636
* `--disable-warning`
2637+
* `--disable-wasm-trap-handler`
26022638
* `--dns-result-order`
26032639
* `--enable-fips`
26042640
* `--enable-network-family-autoselection`

src/node.cc

Lines changed: 38 additions & 31 deletions
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

src/node_options.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,6 +1070,13 @@ PerProcessOptionsParser::PerProcessOptionsParser(
10701070
AddOption("--run",
10711071
"Run a script specified in package.json",
10721072
&PerProcessOptions::run);
1073+
AddOption(
1074+
"--disable-wasm-trap-handler",
1075+
"Disable trap-handler-based WebAssembly bound checks. V8 will insert "
1076+
"inline bound checks when compiling WebAssembly which may slow down "
1077+
"performance.",
1078+
&PerProcessOptions::disable_wasm_trap_handler,
1079+
kAllowedInEnvvar);
10731080
}
10741081

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

src/node_options.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,8 @@ class PerProcessOptions : public Options {
307307
bool openssl_shared_config = false;
308308
#endif
309309

310+
bool disable_wasm_trap_handler = false;
311+
310312
// Per-process because reports can be triggered outside a known V8 context.
311313
bool report_on_fatalerror = false;
312314
bool report_compact = false;

test/testpy/__init__.py

Lines changed: 12 additions & 0 deletions
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
Lines changed: 7 additions & 0 deletions
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 });

test/wasm-allocation/testcfg.py

Lines changed: 6 additions & 0 deletions
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')
Lines changed: 10 additions & 0 deletions
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)