Skip to content

Commit 61b7375

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 ecdd314 commit 61b7375

File tree

8 files changed

+119
-31
lines changed

8 files changed

+119
-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
@@ -2632,6 +2667,7 @@ one is included in the list below.
26322667
* `--diagnostic-dir`
26332668
* `--disable-proto`
26342669
* `--disable-warning`
2670+
* `--disable-wasm-trap-handler`
26352671
* `--dns-result-order`
26362672
* `--enable-fips`
26372673
* `--enable-network-family-autoselection`

src/node.cc

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,7 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
428428
typedef void (*sigaction_cb)(int signo, siginfo_t* info, void* ucontext);
429429
#endif
430430
#if NODE_USE_V8_WASM_TRAP_HANDLER
431+
static std::atomic<bool> is_wasm_trap_handler_configured{false};
431432
#if defined(_WIN32)
432433
static LONG WINAPI TrapWebAssemblyOrContinue(EXCEPTION_POINTERS* exception) {
433434
if (v8::TryHandleWebAssemblyTrapWindows(exception)) {
@@ -473,15 +474,17 @@ void RegisterSignalHandler(int signal,
473474
bool reset_handler) {
474475
CHECK_NOT_NULL(handler);
475476
#if NODE_USE_V8_WASM_TRAP_HANDLER
476-
if (signal == SIGSEGV) {
477+
// Stash the user-registered handlers for TrapWebAssemblyOrContinue
478+
// to call out to when the signal is not coming from a WASM OOM.
479+
if (signal == SIGSEGV && is_wasm_trap_handler_configured.load()) {
477480
CHECK(previous_sigsegv_action.is_lock_free());
478481
CHECK(!reset_handler);
479482
previous_sigsegv_action.store(handler);
480483
return;
481484
}
482-
// TODO(align behavior between macos and other in next major version)
485+
// TODO(align behavior between macos and other in next major version)
483486
#if defined(__APPLE__)
484-
if (signal == SIGBUS) {
487+
if (signal == SIGBUS && is_wasm_trap_handler_configured.load()) {
485488
CHECK(previous_sigbus_action.is_lock_free());
486489
CHECK(!reset_handler);
487490
previous_sigbus_action.store(handler);
@@ -633,25 +636,6 @@ static void PlatformInit(ProcessInitializationFlags::Flags flags) {
633636
if (!(flags & ProcessInitializationFlags::kNoDefaultSignalHandling)) {
634637
RegisterSignalHandler(SIGINT, SignalExit, true);
635638
RegisterSignalHandler(SIGTERM, SignalExit, true);
636-
637-
#if NODE_USE_V8_WASM_TRAP_HANDLER
638-
// Tell V8 to disable emitting WebAssembly
639-
// memory bounds checks. This means that we have
640-
// to catch the SIGSEGV/SIGBUS in TrapWebAssemblyOrContinue
641-
// and pass the signal context to V8.
642-
{
643-
struct sigaction sa;
644-
memset(&sa, 0, sizeof(sa));
645-
sa.sa_sigaction = TrapWebAssemblyOrContinue;
646-
sa.sa_flags = SA_SIGINFO;
647-
CHECK_EQ(sigaction(SIGSEGV, &sa, nullptr), 0);
648-
// TODO(align behavior between macos and other in next major version)
649-
#if defined(__APPLE__)
650-
CHECK_EQ(sigaction(SIGBUS, &sa, nullptr), 0);
651-
#endif
652-
}
653-
V8::EnableWebAssemblyTrapHandler(false);
654-
#endif // NODE_USE_V8_WASM_TRAP_HANDLER
655639
}
656640

657641
if (!(flags & ProcessInitializationFlags::kNoAdjustResourceLimits)) {
@@ -678,14 +662,6 @@ static void PlatformInit(ProcessInitializationFlags::Flags flags) {
678662
}
679663
#endif // __POSIX__
680664
#ifdef _WIN32
681-
#ifdef NODE_USE_V8_WASM_TRAP_HANDLER
682-
{
683-
constexpr ULONG first = TRUE;
684-
per_process::old_vectored_exception_handler =
685-
AddVectoredExceptionHandler(first, TrapWebAssemblyOrContinue);
686-
}
687-
V8::EnableWebAssemblyTrapHandler(false);
688-
#endif // NODE_USE_V8_WASM_TRAP_HANDLER
689665
if (!(flags & ProcessInitializationFlags::kNoStdioInitialization)) {
690666
for (int fd = 0; fd <= 2; ++fd) {
691667
auto handle = reinterpret_cast<HANDLE>(_get_osfhandle(fd));
@@ -1212,6 +1188,37 @@ InitializeOncePerProcessInternal(const std::vector<std::string>& args,
12121188
cppgc::InitializeProcess(allocator);
12131189
}
12141190

1191+
#if NODE_USE_V8_WASM_TRAP_HANDLER
1192+
bool use_wasm_trap_handler =
1193+
!per_process::cli_options->disable_wasm_trap_handler;
1194+
if (!(flags & ProcessInitializationFlags::kNoDefaultSignalHandling) &&
1195+
use_wasm_trap_handler) {
1196+
#if defined(_WIN32)
1197+
constexpr ULONG first = TRUE;
1198+
per_process::old_vectored_exception_handler =
1199+
AddVectoredExceptionHandler(first, TrapWebAssemblyOrContinue);
1200+
#else
1201+
// Tell V8 to disable emitting WebAssembly
1202+
// memory bounds checks. This means that we have
1203+
// to catch the SIGSEGV/SIGBUS in TrapWebAssemblyOrContinue
1204+
// and pass the signal context to V8.
1205+
{
1206+
struct sigaction sa;
1207+
memset(&sa, 0, sizeof(sa));
1208+
sa.sa_sigaction = TrapWebAssemblyOrContinue;
1209+
sa.sa_flags = SA_SIGINFO;
1210+
CHECK_EQ(sigaction(SIGSEGV, &sa, nullptr), 0);
1211+
// TODO(align behavior between macos and other in next major version)
1212+
#if defined(__APPLE__)
1213+
CHECK_EQ(sigaction(SIGBUS, &sa, nullptr), 0);
1214+
#endif
1215+
}
1216+
#endif // defined(_WIN32)
1217+
is_wasm_trap_handler_configured.store(true);
1218+
V8::EnableWebAssemblyTrapHandler(false);
1219+
}
1220+
#endif // NODE_USE_V8_WASM_TRAP_HANDLER
1221+
12151222
performance::performance_v8_start = PERFORMANCE_NOW();
12161223
per_process::v8_initialized = true;
12171224

@@ -1241,7 +1248,7 @@ void TearDownOncePerProcess() {
12411248
}
12421249

12431250
#if NODE_USE_V8_WASM_TRAP_HANDLER && defined(_WIN32)
1244-
if (!(flags & ProcessInitializationFlags::kNoDefaultSignalHandling)) {
1251+
if (is_wasm_trap_handler_configured.load()) {
12451252
RemoveVectoredExceptionHandler(per_process::old_vectored_exception_handler);
12461253
}
12471254
#endif

src/node_options.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,6 +1073,14 @@ PerProcessOptionsParser::PerProcessOptionsParser(
10731073
"Generate a blob that can be embedded into the single executable "
10741074
"application",
10751075
&PerProcessOptions::experimental_sea_config);
1076+
1077+
AddOption(
1078+
"--disable-wasm-trap-handler",
1079+
"Disable trap-handler-based WebAssembly bound checks. V8 will insert "
1080+
"inline bound checks when compiling WebAssembly which may slow down "
1081+
"performance.",
1082+
&PerProcessOptions::disable_wasm_trap_handler,
1083+
kAllowedInEnvvar);
10761084
}
10771085

10781086
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
@@ -308,6 +308,8 @@ class PerProcessOptions : public Options {
308308
bool openssl_shared_config = false;
309309
#endif
310310

311+
bool disable_wasm_trap_handler = false;
312+
311313
// Per-process because reports can be triggered outside a known V8 context.
312314
bool report_on_fatalerror = false;
313315
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)