From af0793883097c7da8eac38ef9c0f82ca470c1446 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Fri, 6 Feb 2026 02:05:35 +0100 Subject: [PATCH 1/2] deps: V8: backport 2733a0848301 Original commit message: [api] Add V8::GetWasmMemoryReservationSizeInBytes() When the system does not have enough virtual memory for the wasm cage, installing the trap handler would cause any code allocating wasm memory to throw. Therefore it's useful for the emebdder to know when the system doesn't have enough virtual address space to allocate any wasm cage and in that case, skip the trap handler installation so that wasm code can at least work (even not at the maximal performance). Node.js previously has a command line option --disable-wasm-trap-handler for this, this new API would allow it to adapt automatically without requiring the use of a command line flag, which is not always under end-user's control (for example, when a VS Code Server is loaded in a remote server for debugging). As a drive by this also makes the trap handlers a bit more robust by making them a no-op of trap handling is not enabled at all. Refs: https://github.com/nodejs/node/pull/52766 Refs: https://github.com/microsoft/vscode/issues/251777 Bug: 40644005 Change-Id: Ie0608970daabe370db4616b875a8f098711c80e2 Refs: https://github.com/v8/v8/commit/2733a0848301c2aa9f8eeb3f99dfd1b9a12db590 Co-authored-by: Joyee Cheung --- common.gypi | 2 +- deps/v8/include/v8-initialization.h | 29 +++++++++++++ deps/v8/src/api/api.cc | 21 ++++++++++ deps/v8/src/objects/backing-store.cc | 42 ++++++++++--------- deps/v8/src/objects/backing-store.h | 5 +++ .../test/unittests/api/api-wasm-unittest.cc | 16 +++++++ 6 files changed, 94 insertions(+), 21 deletions(-) diff --git a/common.gypi b/common.gypi index a4825c5429d761..c58aa7fd89305d 100644 --- a/common.gypi +++ b/common.gypi @@ -38,7 +38,7 @@ # Reset this number to 0 on major V8 upgrades. # Increment by one for each non-official patch applied to deps/v8. - 'v8_embedder_string': '-node.12', + 'v8_embedder_string': '-node.13', ##### V8 defaults for Node.js ##### diff --git a/deps/v8/include/v8-initialization.h b/deps/v8/include/v8-initialization.h index 46a21a02cbcdd6..406f1d0ba54573 100644 --- a/deps/v8/include/v8-initialization.h +++ b/deps/v8/include/v8-initialization.h @@ -253,6 +253,35 @@ class V8_EXPORT V8 { static size_t GetSandboxReservationSizeInBytes(); #endif // V8_ENABLE_SANDBOX + enum class WasmMemoryType { + kMemory32, + kMemory64, + }; + + /** + * Returns the virtual address space reservation size (in bytes) needed + * for one WebAssembly memory instance of the given capacity. + * + * \param type Whether this is a memory32 or memory64 instance. + * \param byte_capacity The maximum size, in bytes, of the WebAssembly + * memory. Values exceeding the engine's maximum allocatable memory + * size for the given type (determined by max_mem32_pages or + * max_mem64_pages) are clamped. + * + * When trap-based bounds checking is enabled by + * EnableWebAssemblyTrapHandler(), the amount of virtual address space + * that V8 needs to reserve for each WebAssembly memory instance can + * be much bigger than the requested size. If the process does + * not have enough virtual memory available, WebAssembly memory allocation + * would fail. During the initialization of V8, embedders can use this method + * to estimate whether the process has enough virtual memory for their + * usage of WebAssembly, and decide whether to enable the trap handler + * via EnableWebAssemblyTrapHandler(), or to skip it and reduce the amount of + * virtual memory required to keep the application running. + */ + static size_t GetWasmMemoryReservationSizeInBytes(WasmMemoryType type, + size_t byte_capacity); + /** * Activate trap-based bounds checking for WebAssembly. * diff --git a/deps/v8/src/api/api.cc b/deps/v8/src/api/api.cc index 5a879e9ff5d9e8..18d762c6443073 100644 --- a/deps/v8/src/api/api.cc +++ b/deps/v8/src/api/api.cc @@ -150,6 +150,7 @@ #include "src/wasm/value-type.h" #include "src/wasm/wasm-engine.h" #include "src/wasm/wasm-js.h" +#include "src/wasm/wasm-limits.h" #include "src/wasm/wasm-objects-inl.h" #include "src/wasm/wasm-result.h" #include "src/wasm/wasm-serialization.h" @@ -6294,6 +6295,26 @@ bool TryHandleWebAssemblyTrapWindows(EXCEPTION_POINTERS* exception) { } #endif +size_t V8::GetWasmMemoryReservationSizeInBytes(WasmMemoryType type, + size_t byte_capacity) { +#if V8_ENABLE_WEBASSEMBLY + bool is_memory64 = type == WasmMemoryType::kMemory64; + uint64_t max_byte_capacity = + is_memory64 ? i::wasm::max_mem64_bytes() : i::wasm::max_mem32_bytes(); + if (byte_capacity > max_byte_capacity) { + byte_capacity = static_cast(max_byte_capacity); + } +#if V8_TRAP_HANDLER_SUPPORTED + if (!is_memory64 || i::v8_flags.wasm_memory64_trap_handling) { + return i::BackingStore::GetWasmReservationSize( + /* has_guard_regions */ true, byte_capacity, + /* is_wasm_memory64 */ is_memory64); + } +#endif // V8_TRAP_HANDLER_SUPPORTED +#endif // V8_ENABLE_WEBASSEMBLY + return byte_capacity; +} + bool V8::EnableWebAssemblyTrapHandler(bool use_v8_signal_handler) { #if V8_ENABLE_WEBASSEMBLY return i::trap_handler::EnableTrapHandler(use_v8_signal_handler); diff --git a/deps/v8/src/objects/backing-store.cc b/deps/v8/src/objects/backing-store.cc index 3292ef26da3469..45fd750264278f 100644 --- a/deps/v8/src/objects/backing-store.cc +++ b/deps/v8/src/objects/backing-store.cc @@ -51,8 +51,25 @@ enum class AllocationStatus { kOtherFailure // Failed for an unknown reason }; -size_t GetReservationSize(bool has_guard_regions, size_t byte_capacity, - bool is_wasm_memory64) { +base::AddressRegion GetReservedRegion(bool has_guard_regions, + bool is_wasm_memory64, void* buffer_start, + size_t byte_capacity) { + return base::AddressRegion( + reinterpret_cast
(buffer_start), + BackingStore::GetWasmReservationSize(has_guard_regions, byte_capacity, + is_wasm_memory64)); +} + +void RecordStatus(Isolate* isolate, AllocationStatus status) { + isolate->counters()->wasm_memory_allocation_result()->AddSample( + static_cast(status)); +} + +} // namespace + +size_t BackingStore::GetWasmReservationSize(bool has_guard_regions, + size_t byte_capacity, + bool is_wasm_memory64) { #if V8_TARGET_ARCH_64_BIT && V8_ENABLE_WEBASSEMBLY DCHECK_IMPLIES(is_wasm_memory64 && has_guard_regions, v8_flags.wasm_memory64_trap_handling); @@ -73,21 +90,6 @@ size_t GetReservationSize(bool has_guard_regions, size_t byte_capacity, return byte_capacity; } -base::AddressRegion GetReservedRegion(bool has_guard_regions, - bool is_wasm_memory64, void* buffer_start, - size_t byte_capacity) { - return base::AddressRegion( - reinterpret_cast
(buffer_start), - GetReservationSize(has_guard_regions, byte_capacity, is_wasm_memory64)); -} - -void RecordStatus(Isolate* isolate, AllocationStatus status) { - isolate->counters()->wasm_memory_allocation_result()->AddSample( - static_cast(status)); -} - -} // namespace - // The backing store for a Wasm shared memory remembers all the isolates // with which it has been shared. struct SharedWasmMemoryData { @@ -168,7 +170,7 @@ BackingStore::~BackingStore() { #if V8_ENABLE_WEBASSEMBLY if (is_wasm_memory()) { - size_t reservation_size = GetReservationSize( + size_t reservation_size = GetWasmReservationSize( has_guard_regions(), byte_capacity_, is_wasm_memory64()); TRACE_BS( "BSw:free bs=%p mem=%p (length=%zu, capacity=%zu, reservation=%zu)\n", @@ -324,8 +326,8 @@ std::unique_ptr BackingStore::TryAllocateAndPartiallyCommitMemory( }; size_t byte_capacity = maximum_pages * page_size; - size_t reservation_size = - GetReservationSize(has_guard_regions, byte_capacity, is_wasm_memory64); + size_t reservation_size = GetWasmReservationSize( + has_guard_regions, byte_capacity, is_wasm_memory64); //-------------------------------------------------------------------------- // Allocate pages (inaccessible by default). diff --git a/deps/v8/src/objects/backing-store.h b/deps/v8/src/objects/backing-store.h index 70882e9bdeafce..609307b0197052 100644 --- a/deps/v8/src/objects/backing-store.h +++ b/deps/v8/src/objects/backing-store.h @@ -183,6 +183,11 @@ class V8_EXPORT_PRIVATE BackingStore : public BackingStoreBase { uint32_t id() const { return id_; } + // Return the size of the reservation needed for a wasm backing store. + static size_t GetWasmReservationSize(bool has_guard_regions, + size_t byte_capacity, + bool is_wasm_memory64); + private: friend class GlobalBackingStoreRegistry; diff --git a/deps/v8/test/unittests/api/api-wasm-unittest.cc b/deps/v8/test/unittests/api/api-wasm-unittest.cc index 7cac935e6f689f..a8b04bac28508b 100644 --- a/deps/v8/test/unittests/api/api-wasm-unittest.cc +++ b/deps/v8/test/unittests/api/api-wasm-unittest.cc @@ -263,4 +263,20 @@ TEST_F(ApiWasmTest, WasmEnableDisableCustomDescriptors) { } } +TEST_F(ApiWasmTest, GetWasmMemoryReservationSizeInBytes) { + constexpr size_t kCapacity = 64 * 1024; // 64 KiB + size_t reservation = V8::GetWasmMemoryReservationSizeInBytes( + V8::WasmMemoryType::kMemory32, kCapacity); + size_t reservation64 = V8::GetWasmMemoryReservationSizeInBytes( + V8::WasmMemoryType::kMemory64, kCapacity); + +#if V8_TRAP_HANDLER_SUPPORTED + EXPECT_GE(reservation, kCapacity); + EXPECT_GE(reservation64, kCapacity); +#else + EXPECT_EQ(reservation, kCapacity); + EXPECT_EQ(reservation64, kCapacity); +#endif // V8_TRAP_HANDLER_SUPPORTED +} + } // namespace v8 From 0c431d971b5807f6035908771ae3bf386614f155 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Sat, 28 Feb 2026 02:30:02 +0100 Subject: [PATCH 2/2] src: do not enable wasm trap handler if there's not enough vmem --- src/debug_utils.h | 1 + src/node.cc | 42 ++++++++++++++++++- test/testpy/__init__.py | 17 +++----- .../test-wasm-allocation-auto-adapt.js | 12 ++++++ ...st-wasm-allocation-disable-trap-handler.js | 20 +++++++++ .../test-wasm-allocation-memory64.js | 22 ++++++++++ test/wasm-allocation/test-wasm-allocation.js | 22 ++++++++-- test/wasm-allocation/testcfg.py | 2 +- test/wasm-allocation/wasm-allocation.status | 3 ++ 9 files changed, 123 insertions(+), 18 deletions(-) create mode 100644 test/wasm-allocation/test-wasm-allocation-auto-adapt.js create mode 100644 test/wasm-allocation/test-wasm-allocation-disable-trap-handler.js create mode 100644 test/wasm-allocation/test-wasm-allocation-memory64.js diff --git a/src/debug_utils.h b/src/debug_utils.h index 8f6165e1b5faf4..587836d73f9ee7 100644 --- a/src/debug_utils.h +++ b/src/debug_utils.h @@ -44,6 +44,7 @@ void NODE_EXTERN_PRIVATE FWrite(FILE* file, const std::string& str); // from a provider type to a debug category. #define DEBUG_CATEGORY_NAMES(V) \ NODE_ASYNC_PROVIDER_TYPES(V) \ + V(BOOTSTRAP) \ V(CRYPTO) \ V(COMPILE_CACHE) \ V(DIAGNOSTICS) \ diff --git a/src/node.cc b/src/node.cc index 8b80de8d9e46f1..ca82c4db288e3e 100644 --- a/src/node.cc +++ b/src/node.cc @@ -1055,6 +1055,44 @@ static ExitCode InitializeNodeWithArgsInternal( return ExitCode::kNoFailure; } +#if NODE_USE_V8_WASM_TRAP_HANDLER +bool CanEnableWebAssemblyTrapHandler() { +// On POSIX, the machine may have a limit on the amount of virtual memory +// available, if it's not enough to allocate at least one cage for WASM, +// then the trap-handler-based bound checks cannot be used. +#ifdef __POSIX__ + struct rlimit lim; + if (getrlimit(RLIMIT_AS, &lim) != 0 || lim.rlim_cur == RLIM_INFINITY) { + // Can't get the limit or there's no limit, assume trap handler can be + // enabled. + return true; + } + uint64_t virtual_memory_available = static_cast(lim.rlim_cur); + + size_t byte_capacity = 64 * 1024; // 64KB, the minimum size of a WASM memory. + uint64_t cage_size_needed_32 = + V8::GetWasmMemoryReservationSizeInBytes(V8::WasmMemoryType::kMemory32, byte_capacity); + uint64_t cage_size_needed_64 = + V8::GetWasmMemoryReservationSizeInBytes(V8::WasmMemoryType::kMemory64, byte_capacity); + uint64_t cage_size_needed = std::max(cage_size_needed_32, cage_size_needed_64); + bool can_enable = virtual_memory_available >= cage_size_needed; + per_process::Debug(DebugCategory::BOOTSTRAP, + "Virtual memory available: %" PRIu64 " bytes,\n" + "cage size needed for 32-bit: %" PRIu64 " bytes,\n" + "cage size needed for 64-bit: %" PRIu64 " bytes,\n" + "Can%senable WASM trap handler\n", + virtual_memory_available, + cage_size_needed_32, + cage_size_needed_64, + can_enable ? " " : " not "); + + return can_enable; +#else + return false; +#endif // __POSIX__ +} +#endif // NODE_USE_V8_WASM_TRAP_HANDLER + static std::shared_ptr InitializeOncePerProcessInternal(const std::vector& args, ProcessInitializationFlags::Flags flags = @@ -1257,7 +1295,9 @@ InitializeOncePerProcessInternal(const std::vector& args, bool use_wasm_trap_handler = !per_process::cli_options->disable_wasm_trap_handler; if (!(flags & ProcessInitializationFlags::kNoDefaultSignalHandling) && - use_wasm_trap_handler) { + use_wasm_trap_handler && CanEnableWebAssemblyTrapHandler()) { + per_process::Debug(DebugCategory::BOOTSTRAP, + "Enabling WebAssembly trap handler for bounds checks\n"); #if defined(_WIN32) constexpr ULONG first = TRUE; per_process::old_vectored_exception_handler = diff --git a/test/testpy/__init__.py b/test/testpy/__init__.py index 9e7686c450b74b..3eb1d2fc2ea525 100644 --- a/test/testpy/__init__.py +++ b/test/testpy/__init__.py @@ -36,6 +36,7 @@ LS_RE = re.compile(r'^test-.*\.m?js$') ENV_PATTERN = re.compile(r"//\s+Env:(.*)") NODE_TEST_PATTERN = re.compile(r"('|`|\")node:test\1") +RLIMIT_AS_PATTERN = re.compile(r"//\s+RLIMIT_AS:\s*(\d+)") class SimpleTestCase(test.TestCase): @@ -99,6 +100,10 @@ def GetRunConfiguration(self): else: result += flags + rlimit_as_match = RLIMIT_AS_PATTERN.search(source) + if rlimit_as_match: + self.max_virtual_memory = int(rlimit_as_match.group(1)) + if self.context.use_error_reporter and NODE_TEST_PATTERN.search(source): result += ['--test-reporter=./test/common/test-error-reporter.js', '--test-reporter-destination=stdout'] @@ -189,15 +194,3 @@ def ListTests(self, current_path, path, arch, mode): for tst in result: tst.disable_core_files = True return result - -class WasmAllocationTestConfiguration(SimpleTestConfiguration): - def __init__(self, context, root, section, additional=None): - super(WasmAllocationTestConfiguration, self).__init__(context, root, section, - additional) - - def ListTests(self, current_path, path, arch, mode): - result = super(WasmAllocationTestConfiguration, self).ListTests( - current_path, path, arch, mode) - for tst in result: - tst.max_virtual_memory = 5 * 1024 * 1024 * 1024 # 5GB - return result diff --git a/test/wasm-allocation/test-wasm-allocation-auto-adapt.js b/test/wasm-allocation/test-wasm-allocation-auto-adapt.js new file mode 100644 index 00000000000000..10ddd97186a4ec --- /dev/null +++ b/test/wasm-allocation/test-wasm-allocation-auto-adapt.js @@ -0,0 +1,12 @@ +// RLIMIT_AS: 3221225472 +// When the virtual memory limit is 3GB, there is not enough virtual memory for +// even one wasm cage. In this case Node.js should automatically adapt and +// skip enabling trap-based bounds checks, so that WASM can at least run with +// inline bound checks. +'use strict'; + +require('../common'); +new WebAssembly.Memory({ initial: 10, maximum: 100 }); + +// Test memory64 works too. +new WebAssembly.Memory({ address: 'i64', initial: 10n, maximum: 100n }); diff --git a/test/wasm-allocation/test-wasm-allocation-disable-trap-handler.js b/test/wasm-allocation/test-wasm-allocation-disable-trap-handler.js new file mode 100644 index 00000000000000..b5aedeffb505ae --- /dev/null +++ b/test/wasm-allocation/test-wasm-allocation-disable-trap-handler.js @@ -0,0 +1,20 @@ +// Flags: --disable-wasm-trap-handler +// RLIMIT_AS: 34359738368 + +// 32GB should be enough for at least 2 cages, but not enough for 30. + +// Test that with limited virtual memory space, --disable-wasm-trap-handler +// fully disables trap-based bounds checks, and thus allows WASM to run with +// inline bound checks. +'use strict'; + +require('../common'); +const instances = []; +for (let i = 0; i < 30; i++) { + instances.push(new WebAssembly.Memory({ initial: 10, maximum: 100 })); +} + +// Test memory64 works too. +for (let i = 0; i < 30; i++) { + instances.push(new WebAssembly.Memory({ initial: 10n, maximum: 100n, address: 'i64' })); +} diff --git a/test/wasm-allocation/test-wasm-allocation-memory64.js b/test/wasm-allocation/test-wasm-allocation-memory64.js new file mode 100644 index 00000000000000..9bbadb8919c7be --- /dev/null +++ b/test/wasm-allocation/test-wasm-allocation-memory64.js @@ -0,0 +1,22 @@ +// RLIMIT_AS: 21474836480 +// With 20GB virtual memory, there's enough space for the first few wasm memory64 +// allocation to succeed, but not enough for many subsequent ones since each +// wasm memory32 with guard regions reserves 8GB of virtual address space. +'use strict'; + +require('../common'); +const assert = require('assert'); + +// The first allocation should succeed. +const first = new WebAssembly.Memory({ address: 'i64', initial: 10n, maximum: 100n }); +assert.ok(first); + +// Subsequent allocations should eventually fail due to running out of +// virtual address space. memory64 reserves 16GB per allocation (vs 8GB for +// memory32), so the limit is reached even faster. +assert.throws(() => { + const instances = [first]; + for (let i = 1; i < 30; i++) { + instances.push(new WebAssembly.Memory({ address: 'i64', initial: 10n, maximum: 100n })); + } +}, /WebAssembly\.Memory/); diff --git a/test/wasm-allocation/test-wasm-allocation.js b/test/wasm-allocation/test-wasm-allocation.js index 8ef8df70d3074c..0eec45607e32bd 100644 --- a/test/wasm-allocation/test-wasm-allocation.js +++ b/test/wasm-allocation/test-wasm-allocation.js @@ -1,7 +1,21 @@ -// Flags: --disable-wasm-trap-handler -// Test that with limited virtual memory space, --disable-wasm-trap-handler -// allows WASM to at least run with inline bound checks. +// RLIMIT_AS: 21474836480 +// With 20GB virtual memory, there's enough space for the first few wasm memory +// allocation to succeed, but not enough for many subsequent ones since each +// wasm memory32 with guard regions reserves 8GB of virtual address space. 'use strict'; require('../common'); -new WebAssembly.Memory({ initial: 10, maximum: 100 }); +const assert = require('assert'); + +// The first allocation should succeed. +const first = new WebAssembly.Memory({ initial: 10, maximum: 100 }); +assert.ok(first); + +// Subsequent allocations should eventually fail due to running out of +// virtual address space. +assert.throws(() => { + const instances = [first]; + for (let i = 1; i < 30; i++) { + instances.push(new WebAssembly.Memory({ initial: 10, maximum: 100 })); + } +}, /WebAssembly\.Memory/); diff --git a/test/wasm-allocation/testcfg.py b/test/wasm-allocation/testcfg.py index fbc899f3ea0d51..4962550b4b6993 100644 --- a/test/wasm-allocation/testcfg.py +++ b/test/wasm-allocation/testcfg.py @@ -3,4 +3,4 @@ import testpy def GetConfiguration(context, root): - return testpy.WasmAllocationTestConfiguration(context, root, 'wasm-allocation') + return testpy.SimpleTestConfiguration(context, root, 'wasm-allocation') diff --git a/test/wasm-allocation/wasm-allocation.status b/test/wasm-allocation/wasm-allocation.status index 4663809cbd327a..4d5cf26109fe13 100644 --- a/test/wasm-allocation/wasm-allocation.status +++ b/test/wasm-allocation/wasm-allocation.status @@ -8,3 +8,6 @@ prefix wasm-allocation [$system!=linux || $asan==on || $pointer_compression==on] test-wasm-allocation: SKIP +test-wasm-allocation-auto-adapt: SKIP +test-wasm-allocation-memory64: SKIP +test-wasm-allocation-memory64-auto-adapt: SKIP