Skip to content

Commit 88d37df

Browse files
[High] Patch nodejs18 for CVE-2025-55131 (#15566)
Co-authored-by: jslobodzian <joslobo@microsoft.com>
1 parent 4185146 commit 88d37df

2 files changed

Lines changed: 311 additions & 1 deletion

File tree

SPECS/nodejs/CVE-2025-55131.patch

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
From 51f4de4b4a52b5b0eb2c63ecbb4126577e05f636 Mon Sep 17 00:00:00 2001
2+
From: =?UTF-8?q?=D0=A1=D0=BA=D0=BE=D0=B2=D0=BE=D1=80=D0=BE=D0=B4=D0=B0=20?=
3+
=?UTF-8?q?=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0=20=D0=90=D0=BD=D0=B4=D1=80?=
4+
=?UTF-8?q?=D0=B5=D0=B5=D0=B2=D0=B8=D1=87?= <chalkerx@gmail.com>
5+
Date: Fri, 7 Nov 2025 11:50:57 -0300
6+
Subject: [PATCH] src,lib: refactor unsafe buffer creation to remove zero-fill
7+
toggle
8+
9+
This removes the zero-fill toggle mechanism that allowed JavaScript
10+
to control ArrayBuffer initialization via shared memory. Instead,
11+
unsafe buffer creation now uses a dedicated C++ API.
12+
13+
Refs: https://hackerone.com/reports/3405778
14+
Co-Authored-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
15+
Co-Authored-By: Joyee Cheung <joyeec9h3@gmail.com>
16+
Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com>
17+
PR-URL: https://github.com/nodejs-private/node-private/pull/759
18+
Backport-PR-URL: https://github.com/nodejs-private/node-private/pull/799
19+
CVE-ID: CVE-2025-55131
20+
21+
Upstream Patch Reference: https://github.com/nodejs/node/commit/51f4de4b4a.patch
22+
---
23+
deps/v8/include/v8-array-buffer.h | 7 +++
24+
deps/v8/src/api/api.cc | 17 ++++++
25+
lib/internal/buffer.js | 23 ++-----
26+
lib/internal/process/pre_execution.js | 2 -
27+
src/api/environment.cc | 3 +-
28+
src/node_buffer.cc | 86 ++++++++++++++++-----------
29+
6 files changed, 83 insertions(+), 55 deletions(-)
30+
31+
diff --git a/deps/v8/include/v8-array-buffer.h b/deps/v8/include/v8-array-buffer.h
32+
index cc5d2d43..bf1df3e7 100644
33+
--- a/deps/v8/include/v8-array-buffer.h
34+
+++ b/deps/v8/include/v8-array-buffer.h
35+
@@ -223,6 +223,13 @@ class V8_EXPORT ArrayBuffer : public Object {
36+
*/
37+
static std::unique_ptr<BackingStore> NewBackingStore(Isolate* isolate,
38+
size_t byte_length);
39+
+ /**
40+
+ * Returns a new standalone BackingStore with uninitialized memory and
41+
+ * return nullptr on failure.
42+
+ * This variant is for not breaking ABI on Node.js LTS. DO NOT USE.
43+
+ */
44+
+ static std::unique_ptr<BackingStore> NewBackingStoreForNodeLTS(
45+
+ Isolate* isolate, size_t byte_length);
46+
/**
47+
* Returns a new standalone BackingStore that takes over the ownership of
48+
* the given buffer. The destructor of the BackingStore invokes the given
49+
diff --git a/deps/v8/src/api/api.cc b/deps/v8/src/api/api.cc
50+
index 3b1a8168..e542eb5a 100644
51+
--- a/deps/v8/src/api/api.cc
52+
+++ b/deps/v8/src/api/api.cc
53+
@@ -8213,6 +8213,23 @@ std::unique_ptr<v8::BackingStore> v8::SharedArrayBuffer::NewBackingStore(
54+
static_cast<v8::BackingStore*>(backing_store.release()));
55+
}
56+
57+
+std::unique_ptr<v8::BackingStore> v8::ArrayBuffer::NewBackingStoreForNodeLTS(
58+
+ Isolate* v8_isolate, size_t byte_length) {
59+
+ i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(v8_isolate);
60+
+ API_RCS_SCOPE(i_isolate, ArrayBuffer, NewBackingStore);
61+
+ CHECK_LE(byte_length, i::JSArrayBuffer::kMaxByteLength);
62+
+ ENTER_V8_NO_SCRIPT_NO_EXCEPTION(i_isolate);
63+
+ std::unique_ptr<i::BackingStoreBase> backing_store =
64+
+ i::BackingStore::Allocate(i_isolate, byte_length,
65+
+ i::SharedFlag::kNotShared,
66+
+ i::InitializedFlag::kUninitialized);
67+
+ if (!backing_store) {
68+
+ return nullptr;
69+
+ }
70+
+ return std::unique_ptr<v8::BackingStore>(
71+
+ static_cast<v8::BackingStore*>(backing_store.release()));
72+
+}
73+
+
74+
std::unique_ptr<v8::BackingStore> v8::SharedArrayBuffer::NewBackingStore(
75+
void* data, size_t byte_length, v8::BackingStore::DeleterCallback deleter,
76+
void* deleter_data) {
77+
diff --git a/lib/internal/buffer.js b/lib/internal/buffer.js
78+
index fbe9de24..23df382f 100644
79+
--- a/lib/internal/buffer.js
80+
+++ b/lib/internal/buffer.js
81+
@@ -30,7 +30,7 @@ const {
82+
hexWrite,
83+
ucs2Write,
84+
utf8Write,
85+
- getZeroFillToggle,
86+
+ createUnsafeArrayBuffer,
87+
} = internalBinding('buffer');
88+
89+
const {
90+
@@ -1053,26 +1053,14 @@ function markAsUntransferable(obj) {
91+
obj[untransferable_object_private_symbol] = true;
92+
}
93+
94+
-// A toggle used to access the zero fill setting of the array buffer allocator
95+
-// in C++.
96+
-// |zeroFill| can be undefined when running inside an isolate where we
97+
-// do not own the ArrayBuffer allocator. Zero fill is always on in that case.
98+
-let zeroFill = getZeroFillToggle();
99+
function createUnsafeBuffer(size) {
100+
- zeroFill[0] = 0;
101+
- try {
102+
+ if (size <= 64) {
103+
+ // Allocated in heap, doesn't call backing store anyway
104+
+ // This is the same that the old impl did implicitly, but explicit now
105+
return new FastBuffer(size);
106+
- } finally {
107+
- zeroFill[0] = 1;
108+
}
109+
-}
110+
111+
-// The connection between the JS land zero fill toggle and the
112+
-// C++ one in the NodeArrayBufferAllocator gets lost if the toggle
113+
-// is deserialized from the snapshot, because V8 owns the underlying
114+
-// memory of this toggle. This resets the connection.
115+
-function reconnectZeroFillToggle() {
116+
- zeroFill = getZeroFillToggle();
117+
+ return new FastBuffer(createUnsafeArrayBuffer(size));
118+
}
119+
120+
module.exports = {
121+
@@ -1082,5 +1070,4 @@ module.exports = {
122+
createUnsafeBuffer,
123+
readUInt16BE,
124+
readUInt32BE,
125+
- reconnectZeroFillToggle,
126+
};
127+
diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js
128+
index 4795be82..f95ab3de 100644
129+
--- a/lib/internal/process/pre_execution.js
130+
+++ b/lib/internal/process/pre_execution.js
131+
@@ -16,7 +16,6 @@ const {
132+
getOptionValue,
133+
refreshOptions,
134+
} = require('internal/options');
135+
-const { reconnectZeroFillToggle } = require('internal/buffer');
136+
const {
137+
defineOperation,
138+
exposeInterface,
139+
@@ -56,7 +55,6 @@ function prepareExecution(options) {
140+
const { expandArgv1, initializeModules, isMainThread } = options;
141+
142+
refreshRuntimeOptions();
143+
- reconnectZeroFillToggle();
144+
145+
// Patch the process object and get the resolved main entry point.
146+
const mainEntry = patchProcessObject(expandArgv1);
147+
diff --git a/src/api/environment.cc b/src/api/environment.cc
148+
index de58a26f..cbe77199 100644
149+
--- a/src/api/environment.cc
150+
+++ b/src/api/environment.cc
151+
@@ -104,8 +104,9 @@ void* NodeArrayBufferAllocator::Allocate(size_t size) {
152+
ret = allocator_->Allocate(size);
153+
else
154+
ret = allocator_->AllocateUninitialized(size);
155+
- if (LIKELY(ret != nullptr))
156+
+ if (ret != nullptr) [[likely]] {
157+
total_mem_usage_.fetch_add(size, std::memory_order_relaxed);
158+
+ }
159+
return ret;
160+
}
161+
162+
diff --git a/src/node_buffer.cc b/src/node_buffer.cc
163+
index 4bc7336e..cf284fd6 100644
164+
--- a/src/node_buffer.cc
165+
+++ b/src/node_buffer.cc
166+
@@ -72,7 +72,6 @@ using v8::Object;
167+
using v8::SharedArrayBuffer;
168+
using v8::String;
169+
using v8::Uint32;
170+
-using v8::Uint32Array;
171+
using v8::Uint8Array;
172+
using v8::Value;
173+
174+
@@ -1207,7 +1206,7 @@ static void EncodeInto(const FunctionCallbackInfo<Value>& args) {
175+
size_t dest_length = dest->ByteLength();
176+
177+
// results = [ read, written ]
178+
- Local<Uint32Array> result_arr = args[2].As<Uint32Array>();
179+
+ Local<v8::Uint32Array> result_arr = args[2].As<v8::Uint32Array>();
180+
uint32_t* results = reinterpret_cast<uint32_t*>(
181+
static_cast<char*>(result_arr->Buffer()->Data()) +
182+
result_arr->ByteOffset());
183+
@@ -1261,35 +1260,6 @@ void SetBufferPrototype(const FunctionCallbackInfo<Value>& args) {
184+
env->set_buffer_prototype_object(proto);
185+
}
186+
187+
-void GetZeroFillToggle(const FunctionCallbackInfo<Value>& args) {
188+
- Environment* env = Environment::GetCurrent(args);
189+
- NodeArrayBufferAllocator* allocator = env->isolate_data()->node_allocator();
190+
- Local<ArrayBuffer> ab;
191+
- // It can be a nullptr when running inside an isolate where we
192+
- // do not own the ArrayBuffer allocator.
193+
- if (allocator == nullptr) {
194+
- // Create a dummy Uint32Array - the JS land can only toggle the C++ land
195+
- // setting when the allocator uses our toggle. With this the toggle in JS
196+
- // land results in no-ops.
197+
- ab = ArrayBuffer::New(env->isolate(), sizeof(uint32_t));
198+
- } else {
199+
- uint32_t* zero_fill_field = allocator->zero_fill_field();
200+
- std::unique_ptr<BackingStore> backing =
201+
- ArrayBuffer::NewBackingStore(zero_fill_field,
202+
- sizeof(*zero_fill_field),
203+
- [](void*, size_t, void*) {},
204+
- nullptr);
205+
- ab = ArrayBuffer::New(env->isolate(), std::move(backing));
206+
- }
207+
-
208+
- ab->SetPrivate(
209+
- env->context(),
210+
- env->untransferable_object_private_symbol(),
211+
- True(env->isolate())).Check();
212+
-
213+
- args.GetReturnValue().Set(Uint32Array::New(ab, 0, 1));
214+
-}
215+
-
216+
void DetachArrayBuffer(const FunctionCallbackInfo<Value>& args) {
217+
Environment* env = Environment::GetCurrent(args);
218+
if (args[0]->IsArrayBuffer()) {
219+
@@ -1357,6 +1327,54 @@ void CopyArrayBuffer(const FunctionCallbackInfo<Value>& args) {
220+
memcpy(dest, src, bytes_to_copy);
221+
}
222+
223+
+// Converts a number parameter to size_t suitable for ArrayBuffer sizes
224+
+// Could be larger than uint32_t
225+
+// See v8::internal::TryNumberToSize and v8::internal::NumberToSize
226+
+inline size_t CheckNumberToSize(Local<Value> number) {
227+
+ CHECK(number->IsNumber());
228+
+ double value = number.As<Number>()->Value();
229+
+ // See v8::internal::TryNumberToSize on this (and on < comparison)
230+
+ double maxSize = static_cast<double>(std::numeric_limits<size_t>::max());
231+
+ CHECK(value >= 0 && value < maxSize);
232+
+ size_t size = static_cast<size_t>(value);
233+
+#ifdef V8_ENABLE_SANDBOX
234+
+ CHECK_LE(size, kMaxSafeBufferSizeForSandbox);
235+
+#endif
236+
+ return size;
237+
+}
238+
+
239+
+void CreateUnsafeArrayBuffer(const FunctionCallbackInfo<Value>& args) {
240+
+ Environment* env = Environment::GetCurrent(args);
241+
+ if (args.Length() != 1) {
242+
+ env->ThrowRangeError("Invalid array buffer length");
243+
+ return;
244+
+ }
245+
+
246+
+ size_t size = CheckNumberToSize(args[0]);
247+
+
248+
+ Isolate* isolate = env->isolate();
249+
+
250+
+ Local<ArrayBuffer> buf;
251+
+
252+
+ NodeArrayBufferAllocator* allocator = env->isolate_data()->node_allocator();
253+
+ // 0-length, or zero-fill flag is set, or building snapshot
254+
+ if (size == 0 || per_process::cli_options->zero_fill_all_buffers ||
255+
+ allocator == nullptr) {
256+
+ buf = ArrayBuffer::New(isolate, size);
257+
+ } else {
258+
+ std::unique_ptr<BackingStore> store =
259+
+ ArrayBuffer::NewBackingStoreForNodeLTS(isolate, size);
260+
+ if (!store) {
261+
+ // This slightly differs from the old behavior,
262+
+ // as in v8 that's a RangeError, and this is an Error with code
263+
+ return env->ThrowRangeError("Array buffer allocation failed");
264+
+ }
265+
+ buf = ArrayBuffer::New(isolate, std::move(store));
266+
+ }
267+
+
268+
+ args.GetReturnValue().Set(buf);
269+
+}
270+
+
271+
void Initialize(Local<Object> target,
272+
Local<Value> unused,
273+
Local<Context> context,
274+
@@ -1379,6 +1397,8 @@ void Initialize(Local<Object> target,
275+
276+
SetMethod(context, target, "detachArrayBuffer", DetachArrayBuffer);
277+
SetMethod(context, target, "copyArrayBuffer", CopyArrayBuffer);
278+
+ SetMethodNoSideEffect(
279+
+ context, target, "createUnsafeArrayBuffer", CreateUnsafeArrayBuffer);
280+
281+
SetMethod(context, target, "swap16", Swap16);
282+
SetMethod(context, target, "swap32", Swap32);
283+
@@ -1418,8 +1438,6 @@ void Initialize(Local<Object> target,
284+
SetMethod(context, target, "hexWrite", StringWrite<HEX>);
285+
SetMethod(context, target, "ucs2Write", StringWrite<UCS2>);
286+
SetMethod(context, target, "utf8Write", StringWrite<UTF8>);
287+
-
288+
- SetMethod(context, target, "getZeroFillToggle", GetZeroFillToggle);
289+
}
290+
291+
} // anonymous namespace
292+
@@ -1463,10 +1481,10 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
293+
registry->Register(StringWrite<HEX>);
294+
registry->Register(StringWrite<UCS2>);
295+
registry->Register(StringWrite<UTF8>);
296+
- registry->Register(GetZeroFillToggle);
297+
298+
registry->Register(DetachArrayBuffer);
299+
registry->Register(CopyArrayBuffer);
300+
+ registry->Register(CreateUnsafeArrayBuffer);
301+
}
302+
303+
} // namespace Buffer
304+
--
305+
2.45.4
306+

SPECS/nodejs/nodejs18.spec

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Name: nodejs18
66
# WARNINGS: MUST check and update the 'npm_version' macro for every version update of this package.
77
# The version of NPM can be found inside the sources under 'deps/npm/package.json'.
88
Version: 18.20.3
9-
Release: 10%{?dist}
9+
Release: 11%{?dist}
1010
License: BSD and MIT and Public Domain and NAIST-2003 and Artistic-2.0
1111
Group: Applications/System
1212
Vendor: Microsoft Corporation
@@ -29,6 +29,7 @@ Patch9: CVE-2025-23166.patch
2929
Patch10: CVE-2025-7656.patch
3030
Patch11: CVE-2025-5889.patch
3131
Patch12: CVE-2025-5222.patch
32+
Patch13: CVE-2025-55131.patch
3233
BuildRequires: brotli-devel
3334
BuildRequires: coreutils >= 8.22
3435
BuildRequires: gcc
@@ -129,6 +130,9 @@ make cctest
129130
%{_datadir}/systemtap/tapset/node.stp
130131

131132
%changelog
133+
* Fri Jan 23 2026 Aditya Singh <v-aditysing@microsoft.com> - 18.20.3-11
134+
- Patch for CVE-2025-55131
135+
132136
* Fri Nov 07 2025 Azure Linux Security Servicing Account <azurelinux-security@microsoft.com> - 18.20.3-10
133137
- Patch for CVE-2025-5222
134138

0 commit comments

Comments
 (0)