Skip to content

Commit de51d96

Browse files
committed
src: do not enable wasm trap handler if there's not enough vmem
1 parent 2be6a64 commit de51d96

File tree

9 files changed

+124
-18
lines changed

9 files changed

+124
-18
lines changed

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(DIAGNOSTICS) \

src/node.cc

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,45 @@ static ExitCode InitializeNodeWithArgsInternal(
10461046
return ExitCode::kNoFailure;
10471047
}
10481048

1049+
#if NODE_USE_V8_WASM_TRAP_HANDLER
1050+
bool CanEnableWebAssemblyTrapHandler() {
1051+
// On POSIX, the machine may have a limit on the amount of virtual memory
1052+
// available, if it's not enough to allocate at least one cage for WASM,
1053+
// then the trap-handler-based bound checks cannot be used.
1054+
#ifdef __POSIX__
1055+
struct rlimit lim;
1056+
if (getrlimit(RLIMIT_AS, &lim) != 0 || lim.rlim_cur == RLIM_INFINITY) {
1057+
// Can't get the limit or there's no limit, assume trap handler can be
1058+
// enabled.
1059+
return true;
1060+
}
1061+
uint64_t virtual_memory_available = static_cast<uint64_t>(lim.rlim_cur);
1062+
1063+
size_t byte_capacity = 64 * 1024; // 64KB, the minimum size of a WASM memory.
1064+
uint64_t cage_size_needed_32 = V8::GetWasmMemoryReservationSizeInBytes(
1065+
V8::WasmMemoryType::kMemory32, byte_capacity);
1066+
uint64_t cage_size_needed_64 = V8::GetWasmMemoryReservationSizeInBytes(
1067+
V8::WasmMemoryType::kMemory64, byte_capacity);
1068+
uint64_t cage_size_needed =
1069+
std::max(cage_size_needed_32, cage_size_needed_64);
1070+
bool can_enable = virtual_memory_available >= cage_size_needed;
1071+
per_process::Debug(DebugCategory::BOOTSTRAP,
1072+
"Virtual memory available: %" PRIu64 " bytes,\n"
1073+
"cage size needed for 32-bit: %" PRIu64 " bytes,\n"
1074+
"cage size needed for 64-bit: %" PRIu64 " bytes,\n"
1075+
"Can%senable WASM trap handler\n",
1076+
virtual_memory_available,
1077+
cage_size_needed_32,
1078+
cage_size_needed_64,
1079+
can_enable ? " " : " not ");
1080+
1081+
return can_enable;
1082+
#else
1083+
return false;
1084+
#endif // __POSIX__
1085+
}
1086+
#endif // NODE_USE_V8_WASM_TRAP_HANDLER
1087+
10491088
static std::shared_ptr<InitializationResultImpl>
10501089
InitializeOncePerProcessInternal(const std::vector<std::string>& args,
10511090
ProcessInitializationFlags::Flags flags =
@@ -1248,7 +1287,9 @@ InitializeOncePerProcessInternal(const std::vector<std::string>& args,
12481287
bool use_wasm_trap_handler =
12491288
!per_process::cli_options->disable_wasm_trap_handler;
12501289
if (!(flags & ProcessInitializationFlags::kNoDefaultSignalHandling) &&
1251-
use_wasm_trap_handler) {
1290+
use_wasm_trap_handler && CanEnableWebAssemblyTrapHandler()) {
1291+
per_process::Debug(DebugCategory::BOOTSTRAP,
1292+
"Enabling WebAssembly trap handler for bounds checks\n");
12521293
#if defined(_WIN32)
12531294
constexpr ULONG first = TRUE;
12541295
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 few wasm memory64
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.
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.ok(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.ok(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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ 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+
test-wasm-allocation-memory64-auto-adapt: SKIP

0 commit comments

Comments
 (0)