Skip to content

Commit 2007569

Browse files
ChALkeRRafaelGSS
andcommitted
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 <rafael.nunu@hotmail.com> Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com> PR-URL: nodejs-private/node-private#759 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> CVE-ID: CVE-2025-55131
1 parent 2092785 commit 2007569

File tree

4 files changed

+62
-80
lines changed

4 files changed

+62
-80
lines changed

lib/internal/buffer.js

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

3636
const {
@@ -39,13 +39,6 @@ const {
3939
},
4040
} = internalBinding('util');
4141

42-
const {
43-
namespace: {
44-
isBuildingSnapshot,
45-
},
46-
addAfterUserSerializeCallback,
47-
} = require('internal/v8/startup_snapshot');
48-
4942
// Temporary buffers to convert numbers.
5043
const float32Array = new Float32Array(1);
5144
const uInt8Float32Array = new Uint8Array(float32Array.buffer);
@@ -1086,28 +1079,14 @@ function isMarkedAsUntransferable(obj) {
10861079
return obj[untransferable_object_private_symbol] !== undefined;
10871080
}
10881081

1089-
// A toggle used to access the zero fill setting of the array buffer allocator
1090-
// in C++.
1091-
// |zeroFill| can be undefined when running inside an isolate where we
1092-
// do not own the ArrayBuffer allocator. Zero fill is always on in that case.
1093-
let zeroFill;
10941082
function createUnsafeBuffer(size) {
1095-
if (!zeroFill) {
1096-
zeroFill = getZeroFillToggle();
1097-
if (isBuildingSnapshot()) {
1098-
// Reset the toggle so that after serialization, we'll re-create a real
1099-
// toggle connected to the C++ one via getZeroFillToggle().
1100-
addAfterUserSerializeCallback(() => {
1101-
zeroFill = undefined;
1102-
});
1103-
}
1104-
}
1105-
zeroFill[0] = 0;
1106-
try {
1083+
if (size <= 64) {
1084+
// Allocated in heap, doesn't call backing store anyway
1085+
// This is the same that the old impl did implicitly, but explicit now
11071086
return new FastBuffer(size);
1108-
} finally {
1109-
zeroFill[0] = 1;
11101087
}
1088+
1089+
return new FastBuffer(createUnsafeArrayBuffer(size));
11111090
}
11121091

11131092
module.exports = {

src/api/environment.cc

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,8 @@ MaybeLocal<Value> PrepareStackTraceCallback(Local<Context> context,
113113

114114
void* NodeArrayBufferAllocator::Allocate(size_t size) {
115115
void* ret;
116-
if (zero_fill_field_ || per_process::cli_options->zero_fill_all_buffers) {
117-
COUNT_GENERIC_USAGE("NodeArrayBufferAllocator.Allocate.ZeroFilled");
118-
ret = allocator_->Allocate(size);
119-
} else {
120-
COUNT_GENERIC_USAGE("NodeArrayBufferAllocator.Allocate.Uninitialized");
121-
ret = allocator_->AllocateUninitialized(size);
122-
}
116+
COUNT_GENERIC_USAGE("NodeArrayBufferAllocator.Allocate.ZeroFilled");
117+
ret = allocator_->Allocate(size);
123118
if (ret != nullptr) [[likely]] {
124119
total_mem_usage_.fetch_add(size, std::memory_order_relaxed);
125120
}

src/node_buffer.cc

Lines changed: 54 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ using v8::Object;
8080
using v8::SharedArrayBuffer;
8181
using v8::String;
8282
using v8::Uint32;
83-
using v8::Uint32Array;
8483
using v8::Uint8Array;
8584
using v8::Value;
8685

@@ -1244,45 +1243,6 @@ void SetBufferPrototype(const FunctionCallbackInfo<Value>& args) {
12441243
realm->set_buffer_prototype_object(proto);
12451244
}
12461245

1247-
void GetZeroFillToggle(const FunctionCallbackInfo<Value>& args) {
1248-
Environment* env = Environment::GetCurrent(args);
1249-
NodeArrayBufferAllocator* allocator = env->isolate_data()->node_allocator();
1250-
Local<ArrayBuffer> ab;
1251-
// It can be a nullptr when running inside an isolate where we
1252-
// do not own the ArrayBuffer allocator.
1253-
if (allocator == nullptr || env->isolate_data()->is_building_snapshot()) {
1254-
// Create a dummy Uint32Array - the JS land can only toggle the C++ land
1255-
// setting when the allocator uses our toggle. With this the toggle in JS
1256-
// land results in no-ops.
1257-
// When building a snapshot, just use a dummy toggle as well to avoid
1258-
// introducing the dynamic external reference. We'll re-initialize the
1259-
// toggle with a real one connected to the C++ allocator after snapshot
1260-
// deserialization.
1261-
1262-
ab = ArrayBuffer::New(env->isolate(), sizeof(uint32_t));
1263-
} else {
1264-
// TODO(joyeecheung): save ab->GetBackingStore()->Data() in the Node.js
1265-
// array buffer allocator and include it into the C++ toggle while the
1266-
// Environment is still alive.
1267-
uint32_t* zero_fill_field = allocator->zero_fill_field();
1268-
std::unique_ptr<BackingStore> backing =
1269-
ArrayBuffer::NewBackingStore(zero_fill_field,
1270-
sizeof(*zero_fill_field),
1271-
[](void*, size_t, void*) {},
1272-
nullptr);
1273-
ab = ArrayBuffer::New(env->isolate(), std::move(backing));
1274-
}
1275-
1276-
if (ab->SetPrivate(env->context(),
1277-
env->untransferable_object_private_symbol(),
1278-
True(env->isolate()))
1279-
.IsNothing()) {
1280-
return;
1281-
}
1282-
1283-
args.GetReturnValue().Set(Uint32Array::New(ab, 0, 1));
1284-
}
1285-
12861246
static void Btoa(const FunctionCallbackInfo<Value>& args) {
12871247
CHECK_EQ(args.Length(), 1);
12881248
Environment* env = Environment::GetCurrent(args);
@@ -1449,6 +1409,57 @@ void CopyArrayBuffer(const FunctionCallbackInfo<Value>& args) {
14491409
memcpy(dest, src, bytes_to_copy);
14501410
}
14511411

1412+
// Converts a number parameter to size_t suitable for ArrayBuffer sizes
1413+
// Could be larger than uint32_t
1414+
// See v8::internal::TryNumberToSize and v8::internal::NumberToSize
1415+
inline size_t CheckNumberToSize(Local<Value> number) {
1416+
CHECK(number->IsNumber());
1417+
double value = number.As<Number>()->Value();
1418+
// See v8::internal::TryNumberToSize on this (and on < comparison)
1419+
double maxSize = static_cast<double>(std::numeric_limits<size_t>::max());
1420+
CHECK(value >= 0 && value < maxSize);
1421+
size_t size = static_cast<size_t>(value);
1422+
#ifdef V8_ENABLE_SANDBOX
1423+
CHECK_LE(size, kMaxSafeBufferSizeForSandbox);
1424+
#endif
1425+
return size;
1426+
}
1427+
1428+
void CreateUnsafeArrayBuffer(const FunctionCallbackInfo<Value>& args) {
1429+
Environment* env = Environment::GetCurrent(args);
1430+
if (args.Length() != 1) {
1431+
env->ThrowRangeError("Invalid array buffer length");
1432+
return;
1433+
}
1434+
1435+
size_t size = CheckNumberToSize(args[0]);
1436+
1437+
Isolate* isolate = env->isolate();
1438+
1439+
Local<ArrayBuffer> buf;
1440+
1441+
// 0-length, or zero-fill flag is set, or building snapshot
1442+
if (size == 0 || per_process::cli_options->zero_fill_all_buffers ||
1443+
env->isolate_data()->is_building_snapshot()) {
1444+
buf = ArrayBuffer::New(isolate, size);
1445+
} else {
1446+
std::unique_ptr<BackingStore> store = ArrayBuffer::NewBackingStore(
1447+
isolate,
1448+
size,
1449+
BackingStoreInitializationMode::kUninitialized,
1450+
v8::BackingStoreOnFailureMode::kReturnNull);
1451+
1452+
if (!store) {
1453+
env->ThrowRangeError("Array buffer allocation failed");
1454+
return;
1455+
}
1456+
1457+
buf = ArrayBuffer::New(isolate, std::move(store));
1458+
}
1459+
1460+
args.GetReturnValue().Set(buf);
1461+
}
1462+
14521463
template <encoding encoding>
14531464
uint32_t WriteOneByteString(const char* src,
14541465
uint32_t src_len,
@@ -1576,6 +1587,8 @@ void Initialize(Local<Object> target,
15761587
SetMethodNoSideEffect(context, target, "indexOfString", IndexOfString);
15771588

15781589
SetMethod(context, target, "copyArrayBuffer", CopyArrayBuffer);
1590+
SetMethodNoSideEffect(
1591+
context, target, "createUnsafeArrayBuffer", CreateUnsafeArrayBuffer);
15791592

15801593
SetMethod(context, target, "swap16", Swap16);
15811594
SetMethod(context, target, "swap32", Swap32);
@@ -1625,8 +1638,6 @@ void Initialize(Local<Object> target,
16251638
"utf8WriteStatic",
16261639
SlowWriteString<UTF8>,
16271640
&fast_write_string_utf8);
1628-
1629-
SetMethod(context, target, "getZeroFillToggle", GetZeroFillToggle);
16301641
}
16311642

16321643
} // anonymous namespace
@@ -1675,9 +1686,9 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
16751686
registry->Register(StringWrite<HEX>);
16761687
registry->Register(StringWrite<UCS2>);
16771688
registry->Register(StringWrite<UTF8>);
1678-
registry->Register(GetZeroFillToggle);
16791689

16801690
registry->Register(CopyArrayBuffer);
1691+
registry->Register(CreateUnsafeArrayBuffer);
16811692

16821693
registry->Register(Atob);
16831694
registry->Register(Btoa);

src/node_internals.h

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,6 @@ v8::MaybeLocal<v8::Object> InitializePrivateSymbols(
124124

125125
class NodeArrayBufferAllocator : public ArrayBufferAllocator {
126126
public:
127-
inline uint32_t* zero_fill_field() { return &zero_fill_field_; }
128-
129127
void* Allocate(size_t size) override; // Defined in src/node.cc
130128
void* AllocateUninitialized(size_t size) override;
131129
void Free(void* data, size_t size) override;
@@ -142,7 +140,6 @@ class NodeArrayBufferAllocator : public ArrayBufferAllocator {
142140
}
143141

144142
private:
145-
uint32_t zero_fill_field_ = 1; // Boolean but exposed as uint32 to JS land.
146143
std::atomic<size_t> total_mem_usage_ {0};
147144

148145
// Delegate to V8's allocator for compatibility with the V8 memory cage.

0 commit comments

Comments
 (0)