Skip to content

Commit fb85d38

Browse files
joyeecheungtargos
authored andcommitted
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 <rafael.nunu@hotmail.com>
1 parent c6043fe commit fb85d38

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
@@ -2634,6 +2673,7 @@ one is included in the list below.
26342673
* `--diagnostic-dir`
26352674
* `--disable-proto`
26362675
* `--disable-warning`
2676+
* `--disable-wasm-trap-handler`
26372677
* `--dns-result-order`
26382678
* `--enable-fips`
26392679
* `--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
@@ -425,6 +425,7 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
425425
typedef void (*sigaction_cb)(int signo, siginfo_t* info, void* ucontext);
426426
#endif
427427
#if NODE_USE_V8_WASM_TRAP_HANDLER
428+
static std::atomic<bool> is_wasm_trap_handler_configured{false};
428429
#if defined(_WIN32)
429430
static LONG WINAPI TrapWebAssemblyOrContinue(EXCEPTION_POINTERS* exception) {
430431
if (v8::TryHandleWebAssemblyTrapWindows(exception)) {
@@ -470,15 +471,17 @@ void RegisterSignalHandler(int signal,
470471
bool reset_handler) {
471472
CHECK_NOT_NULL(handler);
472473
#if NODE_USE_V8_WASM_TRAP_HANDLER
473-
if (signal == SIGSEGV) {
474+
// Stash the user-registered handlers for TrapWebAssemblyOrContinue
475+
// to call out to when the signal is not coming from a WASM OOM.
476+
if (signal == SIGSEGV && is_wasm_trap_handler_configured.load()) {
474477
CHECK(previous_sigsegv_action.is_lock_free());
475478
CHECK(!reset_handler);
476479
previous_sigsegv_action.store(handler);
477480
return;
478481
}
479-
// TODO(align behavior between macos and other in next major version)
482+
// TODO(align behavior between macos and other in next major version)
480483
#if defined(__APPLE__)
481-
if (signal == SIGBUS) {
484+
if (signal == SIGBUS && is_wasm_trap_handler_configured.load()) {
482485
CHECK(previous_sigbus_action.is_lock_free());
483486
CHECK(!reset_handler);
484487
previous_sigbus_action.store(handler);
@@ -630,25 +633,6 @@ static void PlatformInit(ProcessInitializationFlags::Flags flags) {
630633
if (!(flags & ProcessInitializationFlags::kNoDefaultSignalHandling)) {
631634
RegisterSignalHandler(SIGINT, SignalExit, true);
632635
RegisterSignalHandler(SIGTERM, SignalExit, true);
633-
634-
#if NODE_USE_V8_WASM_TRAP_HANDLER
635-
// Tell V8 to disable emitting WebAssembly
636-
// memory bounds checks. This means that we have
637-
// to catch the SIGSEGV/SIGBUS in TrapWebAssemblyOrContinue
638-
// and pass the signal context to V8.
639-
{
640-
struct sigaction sa;
641-
memset(&sa, 0, sizeof(sa));
642-
sa.sa_sigaction = TrapWebAssemblyOrContinue;
643-
sa.sa_flags = SA_SIGINFO;
644-
CHECK_EQ(sigaction(SIGSEGV, &sa, nullptr), 0);
645-
// TODO(align behavior between macos and other in next major version)
646-
#if defined(__APPLE__)
647-
CHECK_EQ(sigaction(SIGBUS, &sa, nullptr), 0);
648-
#endif
649-
}
650-
V8::EnableWebAssemblyTrapHandler(false);
651-
#endif // NODE_USE_V8_WASM_TRAP_HANDLER
652636
}
653637

654638
if (!(flags & ProcessInitializationFlags::kNoAdjustResourceLimits)) {
@@ -675,14 +659,6 @@ static void PlatformInit(ProcessInitializationFlags::Flags flags) {
675659
}
676660
#endif // __POSIX__
677661
#ifdef _WIN32
678-
#ifdef NODE_USE_V8_WASM_TRAP_HANDLER
679-
{
680-
constexpr ULONG first = TRUE;
681-
per_process::old_vectored_exception_handler =
682-
AddVectoredExceptionHandler(first, TrapWebAssemblyOrContinue);
683-
}
684-
V8::EnableWebAssemblyTrapHandler(false);
685-
#endif // NODE_USE_V8_WASM_TRAP_HANDLER
686662
if (!(flags & ProcessInitializationFlags::kNoStdioInitialization)) {
687663
for (int fd = 0; fd <= 2; ++fd) {
688664
auto handle = reinterpret_cast<HANDLE>(_get_osfhandle(fd));
@@ -1225,6 +1201,37 @@ InitializeOncePerProcessInternal(const std::vector<std::string>& args,
12251201
cppgc::InitializeProcess(allocator);
12261202
}
12271203

1204+
#if NODE_USE_V8_WASM_TRAP_HANDLER
1205+
bool use_wasm_trap_handler =
1206+
!per_process::cli_options->disable_wasm_trap_handler;
1207+
if (!(flags & ProcessInitializationFlags::kNoDefaultSignalHandling) &&
1208+
use_wasm_trap_handler) {
1209+
#if defined(_WIN32)
1210+
constexpr ULONG first = TRUE;
1211+
per_process::old_vectored_exception_handler =
1212+
AddVectoredExceptionHandler(first, TrapWebAssemblyOrContinue);
1213+
#else
1214+
// Tell V8 to disable emitting WebAssembly
1215+
// memory bounds checks. This means that we have
1216+
// to catch the SIGSEGV/SIGBUS in TrapWebAssemblyOrContinue
1217+
// and pass the signal context to V8.
1218+
{
1219+
struct sigaction sa;
1220+
memset(&sa, 0, sizeof(sa));
1221+
sa.sa_sigaction = TrapWebAssemblyOrContinue;
1222+
sa.sa_flags = SA_SIGINFO;
1223+
CHECK_EQ(sigaction(SIGSEGV, &sa, nullptr), 0);
1224+
// TODO(align behavior between macos and other in next major version)
1225+
#if defined(__APPLE__)
1226+
CHECK_EQ(sigaction(SIGBUS, &sa, nullptr), 0);
1227+
#endif
1228+
}
1229+
#endif // defined(_WIN32)
1230+
is_wasm_trap_handler_configured.store(true);
1231+
V8::EnableWebAssemblyTrapHandler(false);
1232+
}
1233+
#endif // NODE_USE_V8_WASM_TRAP_HANDLER
1234+
12281235
performance::performance_v8_start = PERFORMANCE_NOW();
12291236
per_process::v8_initialized = true;
12301237

@@ -1254,7 +1261,7 @@ void TearDownOncePerProcess() {
12541261
}
12551262

12561263
#if NODE_USE_V8_WASM_TRAP_HANDLER && defined(_WIN32)
1257-
if (!(flags & ProcessInitializationFlags::kNoDefaultSignalHandling)) {
1264+
if (is_wasm_trap_handler_configured.load()) {
12581265
RemoveVectoredExceptionHandler(per_process::old_vectored_exception_handler);
12591266
}
12601267
#endif

Diff for: src/node_options.cc

+7
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,13 @@ PerProcessOptionsParser::PerProcessOptionsParser(
10661066
AddOption("--run",
10671067
"Run a script specified in package.json",
10681068
&PerProcessOptions::run);
1069+
AddOption(
1070+
"--disable-wasm-trap-handler",
1071+
"Disable trap-handler-based WebAssembly bound checks. V8 will insert "
1072+
"inline bound checks when compiling WebAssembly which may slow down "
1073+
"performance.",
1074+
&PerProcessOptions::disable_wasm_trap_handler,
1075+
kAllowedInEnvvar);
10691076
}
10701077

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

Diff for: src/node_options.h

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

309+
bool disable_wasm_trap_handler = false;
310+
309311
// Per-process because reports can be triggered outside a known V8 context.
310312
bool report_on_fatalerror = false;
311313
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)