Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 30 additions & 13 deletions src/node_buffer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -587,9 +587,9 @@ void StringSlice(const FunctionCallbackInfo<Value>& args) {

void CopyImpl(Local<Value> source_obj,
Local<Value> target_obj,
const uint32_t target_start,
const uint32_t source_start,
const uint32_t to_copy) {
const size_t target_start,
const size_t source_start,
const size_t to_copy) {
ArrayBufferViewContents<char> source(source_obj);
SPREAD_BUFFER_ARG(target_obj, target);

Expand All @@ -598,29 +598,46 @@ void CopyImpl(Local<Value> source_obj,

// Assume caller has properly validated args.
void SlowCopy(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Local<Value> source_obj = args[0];
Local<Value> target_obj = args[1];
const uint32_t target_start = args[2].As<Uint32>()->Value();
const uint32_t source_start = args[3].As<Uint32>()->Value();
const uint32_t to_copy = args[4].As<Uint32>()->Value();
int64_t target_start, source_start, to_copy;
if (!args[2]->IntegerValue(env->context()).To(&target_start) ||
!args[3]->IntegerValue(env->context()).To(&source_start) ||
!args[4]->IntegerValue(env->context()).To(&to_copy)) {
return;
}

// Guard against negative values that would wrap to huge size_t.
if (target_start < 0 || source_start < 0 || to_copy < 0) {
return;
}

CopyImpl(source_obj, target_obj, target_start, source_start, to_copy);
CopyImpl(source_obj,
target_obj,
static_cast<size_t>(target_start),
static_cast<size_t>(source_start),
static_cast<size_t>(to_copy));

args.GetReturnValue().Set(to_copy);
args.GetReturnValue().Set(static_cast<double>(to_copy));
}

// Assume caller has properly validated args.
uint32_t FastCopy(Local<Value> receiver,
uint64_t FastCopy(Local<Value> receiver,
Local<Value> source_obj,
Local<Value> target_obj,
uint32_t target_start,
uint32_t source_start,
uint32_t to_copy,
uint64_t target_start,
uint64_t source_start,
uint64_t to_copy,
// NOLINTNEXTLINE(runtime/references)
FastApiCallbackOptions& options) {
HandleScope scope(options.isolate);

CopyImpl(source_obj, target_obj, target_start, source_start, to_copy);
CopyImpl(source_obj,
target_obj,
static_cast<size_t>(target_start),
static_cast<size_t>(source_start),
static_cast<size_t>(to_copy));

return to_copy;
}
Expand Down
54 changes: 54 additions & 0 deletions test/pummel/test-buffer-copy-large.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use strict';

// Regression test for https://github.com/nodejs/node/issues/55422
// Buffer.copy and Buffer.concat silently produced incorrect results when
// indices >= 2^32 due to uint32_t overflow in the native SlowCopy path.

const common = require('../common');
const assert = require('assert');

// Cannot test on 32-bit platforms since buffers that large cannot exist.
common.skipIf32Bits();

const THRESHOLD = 2 ** 32; // 4 GiB

// Allocate a large target buffer (just over 4 GiB). Skip if there is not
// enough memory available in the current environment (e.g. CI).
let target;
try {
target = Buffer.alloc(THRESHOLD + 10, 0);
} catch (e) {
if (e.code === 'ERR_MEMORY_ALLOCATION_FAILED' ||
/Array buffer allocation failed/.test(e.message)) {
common.skip('insufficient memory for large buffer allocation');
}
throw e;
}

const source = Buffer.alloc(10, 111);

// Test 1: Buffer.copy with targetStart >= 2^32
// Copy only the first 5 bytes so _copyActual falls through to the native
// _copy (SlowCopy) instead of using TypedArrayPrototypeSet.
source.copy(target, THRESHOLD, 0, 5);

assert.strictEqual(target[0], 0, 'position 0 must not have been overwritten');
assert.strictEqual(target[THRESHOLD], 111, 'byte at THRESHOLD must be 111');
assert.strictEqual(target[THRESHOLD + 4], 111, 'byte at THRESHOLD+4 must be 111');
assert.strictEqual(target[THRESHOLD + 5], 0, 'byte at THRESHOLD+5 must be 0');

// Test 2: Buffer.copy at the 2^32 - 1 boundary (crossing the 32-bit edge)
target.fill(0);
source.copy(target, THRESHOLD - 2, 0, 5);
assert.strictEqual(target[THRESHOLD - 2], 111, 'byte at boundary start');
assert.strictEqual(target[THRESHOLD + 2], 111, 'byte crossing boundary');
assert.strictEqual(target[THRESHOLD + 3], 0, 'byte after copied range');

// Test 3: Buffer.concat producing a result with total length > 2^32.
// Note: concat uses TypedArrayPrototypeSet (V8), not the native _copy path
// fixed above. This test verifies the V8 path also handles large offsets.
const small = Buffer.alloc(2, 115);
const result = Buffer.concat([target, small]);
assert.strictEqual(result.length, THRESHOLD + 12);
assert.strictEqual(result[THRESHOLD + 10], 115, 'concat: byte from second buffer');
assert.strictEqual(result[THRESHOLD + 11], 115, 'concat: second byte from second buffer');