Memory safety / allocation failure handling (Out-of-memory)

MEDIUM
nodejs/node
Commit: 3fed9fbc0b8a
Affected: < 25.9.0
2026-04-05 11:24 UTC

Description

The commit implements memory-allocation failure handling for ArrayBuffer BackingStore usage across several Node.js internals (encoding_binding.cc, node_blob.cc, node_buffer.cc). Previously, allocation failures could result in unchecked pointers or crashes due to relying on an assumption that a backing store (bs/store) would always be non-null (e.g., CHECK(bs);). The patch adds OnFailureMode for backing stores, checks for null returns, and throws a memory-allocation error (ERR_MEMORY_ALLOCATION_FAILED) which is now a RangeError to align with V8's OutOfMemory error type. It also updates ERR_MEMORY_ALLOCATION_FAILED in node_errors.h from a generic Error to a RangeError. This reduces the risk of memory-safety issues (null dereferences or use-after-free) when memory allocation fails during operations like Encoding to UTF-8, Buffer.concat, or ArrayBuffer creation. While not a classic security flaw like XSS/SQLi, it mitigates memory-safety vulnerabilities that could be exploited via resource exhaustion or crashes under memory pressure, which can have security implications in production environments.

Proof of Concept

// Proof-of-concept: attempt to trigger an allocation failure path during common memory-heavy operations // This simulates memory pressure that may cause ArrayBuffer backing store allocation to fail, // resulting in a RangeError: ERR_MEMORY_ALLOCATION_FAILED const huge = 'a'.repeat(2 ** 28); // ~256MB of data in a single string (adjust size for environment) try { // Force UTF-8 encoding path that allocates a backing store (e.g., Buffer.from with a large string) const buf = Buffer.from(huge, 'utf8'); console.log('Allocated buffer length:', buf.length); } catch (e) { console.log('Caught:', e.name, e.message); }

Commit Details

Author: Chengzhong Wu

Date: 2026-01-25 15:18 UTC

Message:

src: throw RangeError on failed ArrayBuffer BackingStore allocation This also updates `ERR_MEMORY_ALLOCATION_FAILED` to be a RangeError, aligning with V8's OutOfMemory error type. PR-URL: https://github.com/nodejs/node/pull/61480 Refs: https://github.com/nodejs/node/blob/c755b0113ce0cb6d83baf2cf070ba381a5673db2/deps/v8/src/builtins/builtins-typed-array.cc#L584 Refs: https://tc39.es/ecma262/#sec-sharedarraybuffer.prototype.grow Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>

Triage Assessment

Vulnerability Type: Memory safety (allocation failure handling)

Confidence: MEDIUM

Reasoning:

The commit changes allocation failure handling for ArrayBuffer BackingStore usage and updates the error type to RangeError. It adds guards on allocation results and returns/throws appropriately, reducing risk of null dereferences or use-after-free when memory allocation fails. While not a classic vulnerability like XSS or SQLi, it mitigates memory-safety related failures that could be exploitable under certain conditions.

Verification Assessment

Vulnerability Type: Memory safety / allocation failure handling (Out-of-memory)

Confidence: MEDIUM

Affected Versions: < 25.9.0

Code Diff

diff --git a/src/encoding_binding.cc b/src/encoding_binding.cc index 7683d205aa6a3e..aa901350180b3c 100644 --- a/src/encoding_binding.cc +++ b/src/encoding_binding.cc @@ -17,6 +17,7 @@ namespace encoding_binding { using v8::ArrayBuffer; using v8::BackingStore; using v8::BackingStoreInitializationMode; +using v8::BackingStoreOnFailureMode; using v8::Context; using v8::FunctionCallbackInfo; using v8::HandleScope; @@ -317,9 +318,15 @@ void BindingData::EncodeUtf8String(const FunctionCallbackInfo<Value>& args) { Local<ArrayBuffer> ab; { std::unique_ptr<BackingStore> bs = ArrayBuffer::NewBackingStore( - isolate, length, BackingStoreInitializationMode::kUninitialized); + isolate, + length, + BackingStoreInitializationMode::kUninitialized, + BackingStoreOnFailureMode::kReturnNull); - CHECK(bs); + if (!bs) [[unlikely]] { + THROW_ERR_MEMORY_ALLOCATION_FAILED(isolate); + return; + } // We are certain that `data` is sufficiently large str->WriteUtf8V2(isolate, diff --git a/src/node_blob.cc b/src/node_blob.cc index ab862bf93a411e..417cd8cbd307b9 100644 --- a/src/node_blob.cc +++ b/src/node_blob.cc @@ -22,6 +22,7 @@ using v8::ArrayBuffer; using v8::ArrayBufferView; using v8::BackingStore; using v8::BackingStoreInitializationMode; +using v8::BackingStoreOnFailureMode; using v8::Context; using v8::Function; using v8::FunctionCallbackInfo; @@ -83,7 +84,14 @@ void Concat(const FunctionCallbackInfo<Value>& args) { } std::shared_ptr<BackingStore> store = ArrayBuffer::NewBackingStore( - isolate, total, BackingStoreInitializationMode::kUninitialized); + isolate, + total, + BackingStoreInitializationMode::kUninitialized, + BackingStoreOnFailureMode::kReturnNull); + if (!store) [[unlikely]] { + THROW_ERR_MEMORY_ALLOCATION_FAILED(isolate); + return; + } uint8_t* ptr = static_cast<uint8_t*>(store->Data()); for (size_t n = 0; n < views.size(); n++) { uint8_t* from = diff --git a/src/node_buffer.cc b/src/node_buffer.cc index ddee7b7e40c3ee..e1bee00825d140 100644 --- a/src/node_buffer.cc +++ b/src/node_buffer.cc @@ -59,6 +59,7 @@ using v8::ArrayBuffer; using v8::ArrayBufferView; using v8::BackingStore; using v8::BackingStoreInitializationMode; +using v8::BackingStoreOnFailureMode; using v8::CFunction; using v8::Context; using v8::EscapableHandleScope; @@ -304,17 +305,20 @@ MaybeLocal<Object> New(Isolate* isolate, EscapableHandleScope scope(isolate); size_t length; - if (!StringBytes::Size(isolate, string, enc).To(&length)) - return Local<Object>(); + if (!StringBytes::Size(isolate, string, enc).To(&length)) return {}; size_t actual = 0; std::unique_ptr<BackingStore> store; if (length > 0) { - store = ArrayBuffer::NewBackingStore(isolate, length); + store = ArrayBuffer::NewBackingStore( + isolate, + length, + BackingStoreInitializationMode::kZeroInitialized, + BackingStoreOnFailureMode::kReturnNull); if (!store) [[unlikely]] { THROW_ERR_MEMORY_ALLOCATION_FAILED(isolate); - return Local<Object>(); + return {}; } actual = StringBytes::Write( @@ -329,7 +333,14 @@ MaybeLocal<Object> New(Isolate* isolate, if (actual < length) { std::unique_ptr<BackingStore> old_store = std::move(store); store = ArrayBuffer::NewBackingStore( - isolate, actual, BackingStoreInitializationMode::kUninitialized); + isolate, + actual, + BackingStoreInitializationMode::kUninitialized, + BackingStoreOnFailureMode::kReturnNull); + if (!store) [[unlikely]] { + THROW_ERR_MEMORY_ALLOCATION_FAILED(isolate); + return {}; + } memcpy(store->Data(), old_store->Data(), actual); } Local<ArrayBuffer> buf = ArrayBuffer::New(isolate, std::move(store)); @@ -372,7 +383,14 @@ MaybeLocal<Object> New(Environment* env, size_t length) { Local<ArrayBuffer> ab; { std::unique_ptr<BackingStore> bs = ArrayBuffer::NewBackingStore( - isolate, length, BackingStoreInitializationMode::kUninitialized); + isolate, + length, + BackingStoreInitializationMode::kUninitialized, + BackingStoreOnFailureMode::kReturnNull); + if (!bs) [[unlikely]] { + THROW_ERR_MEMORY_ALLOCATION_FAILED(isolate); + return {}; + } CHECK(bs); @@ -412,9 +430,14 @@ MaybeLocal<Object> Copy(Environment* env, const char* data, size_t length) { } std::unique_ptr<BackingStore> bs = ArrayBuffer::NewBackingStore( - isolate, length, BackingStoreInitializationMode::kUninitialized); - - CHECK(bs); + isolate, + length, + BackingStoreInitializationMode::kUninitialized, + BackingStoreOnFailureMode::kReturnNull); + if (!bs) [[unlikely]] { + THROW_ERR_MEMORY_ALLOCATION_FAILED(isolate); + return {}; + } if (length > 0) memcpy(bs->Data(), data, length); @@ -1449,8 +1472,8 @@ void CreateUnsafeArrayBuffer(const FunctionCallbackInfo<Value>& args) { BackingStoreInitializationMode::kUninitialized, v8::BackingStoreOnFailureMode::kReturnNull); - if (!store) { - env->ThrowRangeError("Array buffer allocation failed"); + if (!store) [[unlikely]] { + THROW_ERR_MEMORY_ALLOCATION_FAILED(env); return; } diff --git a/src/node_errors.h b/src/node_errors.h index 191374210aae2b..406ad251bcf4bb 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -106,7 +106,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details); V(ERR_INVALID_URL_PATTERN, TypeError) \ V(ERR_INVALID_URL_SCHEME, TypeError) \ V(ERR_LOAD_SQLITE_EXTENSION, Error) \ - V(ERR_MEMORY_ALLOCATION_FAILED, Error) \ + V(ERR_MEMORY_ALLOCATION_FAILED, RangeError) \ V(ERR_MESSAGE_TARGET_CONTEXT_UNAVAILABLE, Error) \ V(ERR_MISSING_ARGS, TypeError) \ V(ERR_MISSING_PASSPHRASE, TypeError) \
← Back to Alerts View on GitHub →