You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Buffer.concat and Buffer.copy silently produce invalid results when the operation involves indices equal or greater than 2^32 #55422
changed the title [-]`Buffer.concat` silently produces invalid output when its output size is greater than 4GB[/-][+]`Buffer.concat` silently produces invalid output when its output size is greater than 4GiB[/+]on Oct 17, 2024
Anyone care to open a PR? I think this could be a simple case of just switching to .set(srcBuffer) (instead of using native methods) in the case where the total length exceeds e.g. 2 GB.
I'm not sure what exactly the binding refers to, but I found a candidate method in the C++ code (at node/src/node_buffer.cc) that treats all arguments as Uint32:
Measured on linux-x64 release binaries. big = Buffer.alloc(2**32 + 16, 111) with marker bytes 1..16 written past the 4 GiB boundary, then partial copy() calls that cannot take the TypedArrayPrototypeSet whole-buffer fast path in _copyActual:
D big.copy(dst, 0, 2**32 + 1, 2**32 + 11): v22/v24 fill dst with 6f6f6f… (source offset wrapped to 1); v26.4+ give 02030405060708090a0b.
E big.copy(dst, 0, 1, 2**32 + 16): v22/v24 return 4294967311 but only 15 bytes are written (dst.indexOf(0) === 15).
F2 small.copy(dst, 2**32 + 2, 1): v22/v24 write at offset 2 instead of 2**32 + 2.
G (the concat case above) passes everywhere now; SlowCopy on v22.x/v24.x still reads the offsets as Uint32.
So this is fixed on 26.x since v26.4.0 via #63828, but that PR is labeled dont-land-on-v22.x / dont-land-on-v24.x (needs the new V8 CopyArrayBufferBytes API), so both LTS lines still silently corrupt partial copies past 4 GiB.
Is a v22.x/v24.x-only change to SlowCopy/FastCopy (read offsets as doubles into size_t, no V8 change) acceptable, or should this be closed as fixed in 26.4.0? I can open that PR if wanted. Only checked linux-x64.
Picking this up — proposed LTS-only fix path, asking for a sign-off before coding
Hi — reading @aron-intframe's 2026-09-16 note and the five earlier attempts (#55492, #61914, #62157, #62500, #62942) that all expired to stale-bot without a technical rejection. Rather than open PR #6 cold, I'd rather confirm the direction so this one makes it through LTS review+CI.
Scope. Main is fine since v26.4.0 (via #63828, @ronag). This proposal only targets v22.x and v24.x — the two LTS lines where silent data corruption past 4 GiB still ships. #63828 was marked dont-land-on-v22.x / dont-land-on-v24.x because it uses the new V8 CopyArrayBufferBytes API, so the LTS lines need an independent, V8-ABI-neutral patch.
Current state on both LTS branches (verified against src/node_buffer.cc HEAD of v22.x and v24.x just now):
(On v22.x the shape is identical — args[2..4]->Uint32Value(env->context()).To(&target_start) at lines 574–576.)
Proposed LTS-only fix (no V8 ABI change, no new API required):
Widen CopyImpl params from uint32_t to size_t.
In SlowCopy, read the three offsets as IntegerValue(ctx) (which gives int64_t on every V8 version) and bound-check < 0 + fit-in-size_t before assigning to size_t. Uint32::Value() is replaced by this safer reader.
Keep FastCopy's V8 signature untouched (uint32_t params) — the eager fallback lives in _copyActual (lib/internal/buffer.js): before calling the _copy binding, if to_copy > 0xFFFFFFFF or either offset is > 0xFFFFFFFF, route to a new slow-path-only binding (e.g. _copyLarge) that is not registered via SetFastMethod. V8 fast API keeps its invariants, the sub-4-GiB hot path is untouched.
SlowCopy's return value: args.GetReturnValue().Set(uint32_t) still works for the common case; for to_copy > UINT32_MAX, use Set(static_cast<double>(to_copy)) since partial-copy semantics cap by surviving buffer length and we stay well under Number.MAX_SAFE_INTEGER (2^53).
Test matrix (ported directly from @aron-intframe's 2026-09-16 scenarios so the test fails on current v22.x/v24.x HEAD and passes on the fix branch):
Each lives behind common.enoughTestMem so CI without 4 GiB headroom skips cleanly, matching the pattern already in test/parallel/test-buffer-concat-4gb.js.
Asking for direction before I open the PR:
Is a v22.x/v24.x-only fix welcome, or does the LTS release-captain want this closed as "fixed on 26.4.0 — don't backport"? If (b), I'll close out and the issue can retire.
FastCopy strategy: eager fallback-to-SlowCopy on to_copy > UINT32_MAX (my preference, zero V8-signature change), vs. a FastCopy with size_t params (requires one of V8's int64 fast-API variants if available on v22.x's V8 pin).
Which branches first: v24.x then cherry to v22.x, or parallel PRs. I can carry CI on both.
Work already in flight we should not duplicate.
If any of (1a/b, 2, 3) is pre-decided, I'll open the PR against that direction with the test matrix + benchmark number showing no regression on the fast path for sub-4-GiB copies. Zero V8 version bump requested.
Activity
[-]`Buffer.concat` silently produces invalid output when its output size is greater than 4GB[/-][+]`Buffer.concat` silently produces invalid output when its output size is greater than 4GiB[/+]rotemdan commentedon Oct 17, 2024
My current workaround (tested to produce correct results with sizes greater than 4 GiB):
avivkeller commentedon Oct 17, 2024
The issue started in v22.7.0. I'll start bisecting. Maybe #54087?
avivkeller commentedon Oct 17, 2024
I've finished bisecting. This was indeed caused by #54087 cc @ronag.
ronag commentedon Oct 21, 2024
Anyone care to open a PR? I think this could be a simple case of just switching to
.set(srcBuffer)(instead of using native methods) in the case where the total length exceeds e.g. 2 GB.duncpro commentedon Oct 21, 2024
I reproduced this on macOS.
@ronag I'd like to try and tackle this one.
MrJithil commentedon Oct 21, 2024
good luck.
rotemdan commentedon Oct 21, 2024
This call to
_copyis possibly the reason:_copyis imported from some sort of internal binding.A thorough solution is to ensure this method correctly handles large array sizes, or fails.
Just working around it by falling back to
TypedArray.set, would leave the possibility of a future issue if some other code calls_copy.duncpro commentedon Oct 21, 2024
So the root cause of this problem is 32-bit integer overflow in
SlowCopyinnode_buffer.cchere.Apparently
Uint32Valueperforms a wrapping conversion. So that's why in the example below the target buffer only gets filled with 5 bytes.Simply replacing
Uint32ValuewithIntegerValuewill fix this barring edge cases I've yet to fully consider.rotemdan commentedon Oct 21, 2024
I'm not sure what exactly the binding refers to, but I found a candidate method in the C++ code (at
node/src/node_buffer.cc) that treats all arguments as Uint32:Regardless on whether it's the method used in the binding, using
Uint32Valueto extract the arguments doesn't seem right.This method follows, also taking in
uint32_ts:duncpro commentedon Oct 21, 2024
@rotemdan this is correct
11 remaining items
buffer: fix Buffer.copy for large offsets >= 2^32
buffer: optimize Buffer.prototype.copy
buffer: optimize Buffer.prototype.copy
buffer: speed up Buffer.prototype.copy via view-level copy
buffer: optimize Buffer.prototype.copy
LeonxLJX commentedon Sep 1, 2026
aron-intframe commentedon Sep 16, 2026
Measured on linux-x64 release binaries.
big = Buffer.alloc(2**32 + 16, 111)with marker bytes 1..16 written past the 4 GiB boundary, then partialcopy()calls that cannot take theTypedArrayPrototypeSetwhole-buffer fast path in_copyActual:big.copy(dst, 0, 2**32 + 1, 2**32 + 11): v22/v24 filldstwith6f6f6f…(source offset wrapped to 1); v26.4+ give02030405060708090a0b.big.copy(dst, 0, 1, 2**32 + 16): v22/v24 return4294967311but only 15 bytes are written (dst.indexOf(0) === 15).small.copy(dst, 2**32 + 2, 1): v22/v24 write at offset 2 instead of 2**32 + 2.concatcase above) passes everywhere now;SlowCopyon v22.x/v24.x still reads the offsets asUint32.So this is fixed on 26.x since v26.4.0 via #63828, but that PR is labeled
dont-land-on-v22.x/dont-land-on-v24.x(needs the new V8CopyArrayBufferBytesAPI), so both LTS lines still silently corrupt partial copies past 4 GiB.Is a v22.x/v24.x-only change to
SlowCopy/FastCopy(read offsets as doubles intosize_t, no V8 change) acceptable, or should this be closed as fixed in 26.4.0? I can open that PR if wanted. Only checked linux-x64.buffer: optimize Buffer.prototype.copy
buffer: optimize Buffer.prototype.copy
61465 commentedon Oct 14, 2026
Picking this up — proposed LTS-only fix path, asking for a sign-off before coding
Hi — reading @aron-intframe's 2026-09-16 note and the five earlier attempts (#55492, #61914, #62157, #62500, #62942) that all expired to stale-bot without a technical rejection. Rather than open PR #6 cold, I'd rather confirm the direction so this one makes it through LTS review+CI.
Scope. Main is fine since v26.4.0 (via #63828, @ronag). This proposal only targets
v22.xandv24.x— the two LTS lines where silent data corruption past 4 GiB still ships. #63828 was markeddont-land-on-v22.x/dont-land-on-v24.xbecause it uses the new V8CopyArrayBufferBytesAPI, so the LTS lines need an independent, V8-ABI-neutral patch.Current state on both LTS branches (verified against
src/node_buffer.ccHEAD ofv22.xandv24.xjust now):(On
v22.xthe shape is identical —args[2..4]->Uint32Value(env->context()).To(&target_start)at lines 574–576.)Proposed LTS-only fix (no V8 ABI change, no new API required):
CopyImplparams fromuint32_ttosize_t.SlowCopy, read the three offsets asIntegerValue(ctx)(which givesint64_ton every V8 version) and bound-check< 0+ fit-in-size_tbefore assigning tosize_t.Uint32::Value()is replaced by this safer reader.FastCopy's V8 signature untouched (uint32_tparams) — the eager fallback lives in_copyActual(lib/internal/buffer.js): before calling the_copybinding, ifto_copy > 0xFFFFFFFFor either offset is> 0xFFFFFFFF, route to a new slow-path-only binding (e.g._copyLarge) that is not registered viaSetFastMethod. V8 fast API keeps its invariants, the sub-4-GiB hot path is untouched.SlowCopy's return value:args.GetReturnValue().Set(uint32_t)still works for the common case; forto_copy > UINT32_MAX, useSet(static_cast<double>(to_copy))since partial-copy semantics cap by surviving buffer length and we stay well underNumber.MAX_SAFE_INTEGER(2^53).Test matrix (ported directly from @aron-intframe's 2026-09-16 scenarios so the test fails on current
v22.x/v24.xHEAD and passes on the fix branch):big = Buffer.alloc(2**32 + 16, 111)+ marker bytes 1..16;big.copy(dst, 0, 2**32 + 1, 2**32 + 11)dststarts02 03 04 05 06 07 08 09 0a 0bbig.copy(dst, 0, 1, 2**32 + 16)4294967311)small.copy(dst, 2**32 + 2, 1)2**32 + 2, not2Buffer.concat([Buffer.alloc(2, 112), Buffer.alloc(2**32 + 5, 111), Buffer.alloc(2, 115)]).slice(-12)[0x6f × 10, 0x73, 0x73]Each lives behind
common.enoughTestMemso CI without 4 GiB headroom skips cleanly, matching the pattern already intest/parallel/test-buffer-concat-4gb.js.Asking for direction before I open the PR:
v22.x/v24.x-only fix welcome, or does the LTS release-captain want this closed as "fixed on 26.4.0 — don't backport"? If (b), I'll close out and the issue can retire.to_copy > UINT32_MAX(my preference, zero V8-signature change), vs. a FastCopy withsize_tparams (requires one of V8's int64 fast-API variants if available onv22.x's V8 pin).v24.xthen cherry tov22.x, or parallel PRs. I can carry CI on both.If any of (1a/b, 2, 3) is pre-decided, I'll open the PR against that direction with the test matrix + benchmark number showing no regression on the fast path for sub-4-GiB copies. Zero V8 version bump requested.
cc @ChALkeR + @ronag — thanks for carrying this on 26.x and keeping the thread honest.
cc @aron-intframe — this is your writeup; please pull me in if you'd rather drive the PR yourself. Credit goes to you + @duncpro, @danielalanbates, @Felipeness, @crawfordxx, @maruthang for the earlier attempts that mapped out the shape.
🤖 Used Claude Code for the code-trace and comment drafting; all content signed off by me.