Skip to content

Commit 51f4de4

Browse files
ChALkeRRafaelGSSjoyeecheung
committed
src,lib: refactor unsafe buffer creation to remove zero-fill toggle
This removes the zero-fill toggle mechanism that allowed JavaScript to control ArrayBuffer initialization via shared memory. Instead, unsafe buffer creation now uses a dedicated C++ API. Refs: https://hackerone.com/reports/3405778 Co-Authored-By: Rafael Gonzaga <[email protected]> Co-Authored-By: Joyee Cheung <[email protected]> Signed-off-by: RafaelGSS <[email protected]> PR-URL: nodejs-private/node-private#759 Backport-PR-URL: nodejs-private/node-private#799 CVE-ID: CVE-2025-55131
1 parent d7a5c58 commit 51f4de4

File tree

6 files changed

+82
-54
lines changed

6 files changed

+82
-54
lines changed

deps/v8/include/v8-array-buffer.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,13 @@ class V8_EXPORT ArrayBuffer : public Object {
244244
*/
245245
static std::unique_ptr<BackingStore> NewBackingStore(Isolate* isolate,
246246
size_t byte_length);
247+
/**
248+
* Returns a new standalone BackingStore with uninitialized memory and
249+
* return nullptr on failure.
250+
* This variant is for not breaking ABI on Node.js LTS. DO NOT USE.
251+
*/
252+
static std::unique_ptr<BackingStore> NewBackingStoreForNodeLTS(
253+
Isolate* isolate, size_t byte_length);
247254
/**
248255
* Returns a new standalone BackingStore that takes over the ownership of
249256
* the given buffer. The destructor of the BackingStore invokes the given

deps/v8/src/api/api.cc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8743,6 +8743,23 @@ std::unique_ptr<v8::BackingStore> v8::ArrayBuffer::NewBackingStore(
87438743
static_cast<v8::BackingStore*>(backing_store.release()));
87448744
}
87458745

8746+
std::unique_ptr<v8::BackingStore> v8::ArrayBuffer::NewBackingStoreForNodeLTS(
8747+
Isolate* v8_isolate, size_t byte_length) {
8748+
i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(v8_isolate);
8749+
API_RCS_SCOPE(i_isolate, ArrayBuffer, NewBackingStore);
8750+
CHECK_LE(byte_length, i::JSArrayBuffer::kMaxByteLength);
8751+
ENTER_V8_NO_SCRIPT_NO_EXCEPTION(i_isolate);
8752+
std::unique_ptr<i::BackingStoreBase> backing_store =
8753+
i::BackingStore::Allocate(i_isolate, byte_length,
8754+
i::SharedFlag::kNotShared,
8755+
i::InitializedFlag::kUninitialized);
8756+
if (!backing_store) {
8757+
return nullptr;
8758+
}
8759+
return std::unique_ptr<v8::BackingStore>(
8760+
static_cast<v8::BackingStore*>(backing_store.release()));
8761+
}
8762+
87468763
std::unique_ptr<v8::BackingStore> v8::ArrayBuffer::NewBackingStore(
87478764
void* data, size_t byte_length, v8::BackingStore::DeleterCallback deleter,
87488765
void* deleter_data) {

lib/internal/buffer.js

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const {
3030
hexWrite,
3131
ucs2Write,
3232
utf8Write,
33-
getZeroFillToggle,
33+
createUnsafeArrayBuffer,
3434
} = internalBinding('buffer');
3535

3636
const {
@@ -1053,26 +1053,14 @@ function markAsUntransferable(obj) {
10531053
obj[untransferable_object_private_symbol] = true;
10541054
}
10551055

1056-
// A toggle used to access the zero fill setting of the array buffer allocator
1057-
// in C++.
1058-
// |zeroFill| can be undefined when running inside an isolate where we
1059-
// do not own the ArrayBuffer allocator. Zero fill is always on in that case.
1060-
let zeroFill = getZeroFillToggle();
10611056
function createUnsafeBuffer(size) {
1062-
zeroFill[0] = 0;
1063-
try {
1057+
if (size <= 64) {
1058+
// Allocated in heap, doesn't call backing store anyway
1059+
// This is the same that the old impl did implicitly, but explicit now
10641060
return new FastBuffer(size);
1065-
} finally {
1066-
zeroFill[0] = 1;
10671061
}
1068-
}
10691062

1070-
// The connection between the JS land zero fill toggle and the
1071-
// C++ one in the NodeArrayBufferAllocator gets lost if the toggle
1072-
// is deserialized from the snapshot, because V8 owns the underlying
1073-
// memory of this toggle. This resets the connection.
1074-
function reconnectZeroFillToggle() {
1075-
zeroFill = getZeroFillToggle();
1063+
return new FastBuffer(createUnsafeArrayBuffer(size));
10761064
}
10771065

10781066
module.exports = {
@@ -1082,5 +1070,4 @@ module.exports = {
10821070
createUnsafeBuffer,
10831071
readUInt16BE,
10841072
readUInt32BE,
1085-
reconnectZeroFillToggle,
10861073
};

lib/internal/process/pre_execution.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ const {
2424
refreshOptions,
2525
getEmbedderOptions,
2626
} = require('internal/options');
27-
const { reconnectZeroFillToggle } = require('internal/buffer');
2827
const {
2928
exposeInterface,
3029
exposeLazyInterfaces,
@@ -98,7 +97,6 @@ function prepareExecution(options) {
9897
const { expandArgv1, initializeModules, isMainThread } = options;
9998

10099
refreshRuntimeOptions();
101-
reconnectZeroFillToggle();
102100

103101
// Patch the process object and get the resolved main entry point.
104102
const mainEntry = patchProcessObject(expandArgv1);

src/api/environment.cc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,9 @@ void* NodeArrayBufferAllocator::Allocate(size_t size) {
107107
ret = allocator_->Allocate(size);
108108
else
109109
ret = allocator_->AllocateUninitialized(size);
110-
if (LIKELY(ret != nullptr))
110+
if (ret != nullptr) [[likely]] {
111111
total_mem_usage_.fetch_add(size, std::memory_order_relaxed);
112+
}
112113
return ret;
113114
}
114115

src/node_buffer.cc

Lines changed: 51 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ using v8::Object;
7575
using v8::SharedArrayBuffer;
7676
using v8::String;
7777
using v8::Uint32;
78-
using v8::Uint32Array;
7978
using v8::Uint8Array;
8079
using v8::Value;
8180

@@ -1177,35 +1176,6 @@ void SetBufferPrototype(const FunctionCallbackInfo<Value>& args) {
11771176
realm->set_buffer_prototype_object(proto);
11781177
}
11791178

1180-
void GetZeroFillToggle(const FunctionCallbackInfo<Value>& args) {
1181-
Environment* env = Environment::GetCurrent(args);
1182-
NodeArrayBufferAllocator* allocator = env->isolate_data()->node_allocator();
1183-
Local<ArrayBuffer> ab;
1184-
// It can be a nullptr when running inside an isolate where we
1185-
// do not own the ArrayBuffer allocator.
1186-
if (allocator == nullptr) {
1187-
// Create a dummy Uint32Array - the JS land can only toggle the C++ land
1188-
// setting when the allocator uses our toggle. With this the toggle in JS
1189-
// land results in no-ops.
1190-
ab = ArrayBuffer::New(env->isolate(), sizeof(uint32_t));
1191-
} else {
1192-
uint32_t* zero_fill_field = allocator->zero_fill_field();
1193-
std::unique_ptr<BackingStore> backing =
1194-
ArrayBuffer::NewBackingStore(zero_fill_field,
1195-
sizeof(*zero_fill_field),
1196-
[](void*, size_t, void*) {},
1197-
nullptr);
1198-
ab = ArrayBuffer::New(env->isolate(), std::move(backing));
1199-
}
1200-
1201-
ab->SetPrivate(
1202-
env->context(),
1203-
env->untransferable_object_private_symbol(),
1204-
True(env->isolate())).Check();
1205-
1206-
args.GetReturnValue().Set(Uint32Array::New(ab, 0, 1));
1207-
}
1208-
12091179
void DetachArrayBuffer(const FunctionCallbackInfo<Value>& args) {
12101180
Environment* env = Environment::GetCurrent(args);
12111181
if (args[0]->IsArrayBuffer()) {
@@ -1397,6 +1367,54 @@ void CopyArrayBuffer(const FunctionCallbackInfo<Value>& args) {
13971367
memcpy(dest, src, bytes_to_copy);
13981368
}
13991369

1370+
// Converts a number parameter to size_t suitable for ArrayBuffer sizes
1371+
// Could be larger than uint32_t
1372+
// See v8::internal::TryNumberToSize and v8::internal::NumberToSize
1373+
inline size_t CheckNumberToSize(Local<Value> number) {
1374+
CHECK(number->IsNumber());
1375+
double value = number.As<Number>()->Value();
1376+
// See v8::internal::TryNumberToSize on this (and on < comparison)
1377+
double maxSize = static_cast<double>(std::numeric_limits<size_t>::max());
1378+
CHECK(value >= 0 && value < maxSize);
1379+
size_t size = static_cast<size_t>(value);
1380+
#ifdef V8_ENABLE_SANDBOX
1381+
CHECK_LE(size, kMaxSafeBufferSizeForSandbox);
1382+
#endif
1383+
return size;
1384+
}
1385+
1386+
void CreateUnsafeArrayBuffer(const FunctionCallbackInfo<Value>& args) {
1387+
Environment* env = Environment::GetCurrent(args);
1388+
if (args.Length() != 1) {
1389+
env->ThrowRangeError("Invalid array buffer length");
1390+
return;
1391+
}
1392+
1393+
size_t size = CheckNumberToSize(args[0]);
1394+
1395+
Isolate* isolate = env->isolate();
1396+
1397+
Local<ArrayBuffer> buf;
1398+
1399+
NodeArrayBufferAllocator* allocator = env->isolate_data()->node_allocator();
1400+
// 0-length, or zero-fill flag is set, or building snapshot
1401+
if (size == 0 || per_process::cli_options->zero_fill_all_buffers ||
1402+
allocator == nullptr) {
1403+
buf = ArrayBuffer::New(isolate, size);
1404+
} else {
1405+
std::unique_ptr<BackingStore> store =
1406+
ArrayBuffer::NewBackingStoreForNodeLTS(isolate, size);
1407+
if (!store) {
1408+
// This slightly differs from the old behavior,
1409+
// as in v8 that's a RangeError, and this is an Error with code
1410+
return env->ThrowRangeError("Array buffer allocation failed");
1411+
}
1412+
buf = ArrayBuffer::New(isolate, std::move(store));
1413+
}
1414+
1415+
args.GetReturnValue().Set(buf);
1416+
}
1417+
14001418
void Initialize(Local<Object> target,
14011419
Local<Value> unused,
14021420
Local<Context> context,
@@ -1428,6 +1446,8 @@ void Initialize(Local<Object> target,
14281446

14291447
SetMethod(context, target, "detachArrayBuffer", DetachArrayBuffer);
14301448
SetMethod(context, target, "copyArrayBuffer", CopyArrayBuffer);
1449+
SetMethodNoSideEffect(
1450+
context, target, "createUnsafeArrayBuffer", CreateUnsafeArrayBuffer);
14311451

14321452
SetMethod(context, target, "swap16", Swap16);
14331453
SetMethod(context, target, "swap32", Swap32);
@@ -1464,8 +1484,6 @@ void Initialize(Local<Object> target,
14641484
SetMethod(context, target, "hexWrite", StringWrite<HEX>);
14651485
SetMethod(context, target, "ucs2Write", StringWrite<UCS2>);
14661486
SetMethod(context, target, "utf8Write", StringWrite<UTF8>);
1467-
1468-
SetMethod(context, target, "getZeroFillToggle", GetZeroFillToggle);
14691487
}
14701488

14711489
} // anonymous namespace
@@ -1508,10 +1526,10 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
15081526
registry->Register(StringWrite<HEX>);
15091527
registry->Register(StringWrite<UCS2>);
15101528
registry->Register(StringWrite<UTF8>);
1511-
registry->Register(GetZeroFillToggle);
15121529

15131530
registry->Register(DetachArrayBuffer);
15141531
registry->Register(CopyArrayBuffer);
1532+
registry->Register(CreateUnsafeArrayBuffer);
15151533

15161534
registry->Register(Atob);
15171535
registry->Register(Btoa);

0 commit comments

Comments
 (0)