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 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