fix(buffer): return fixed-length view from slice on resizable ArrayBuffer (#26822)

## Summary

Follow-up to #26819 ([review
comment](https://github.com/oven-sh/bun/pull/26819#discussion_r2781484939)).
Fixes `Buffer.slice()` / `Buffer.subarray()` on resizable `ArrayBuffer`
/ growable `SharedArrayBuffer` to return a **fixed-length view** instead
of a length-tracking view.

## Problem

The resizable/growable branch was passing `std::nullopt` to
`JSUint8Array::create()`, which creates a length-tracking view. When the
underlying buffer grows, the sliced view's length would incorrectly
expand:

```js
const rab = new ArrayBuffer(10, { maxByteLength: 20 });
const buf = Buffer.from(rab);
const sliced = buf.slice(0, 5);
sliced.length; // 5

rab.resize(20);
sliced.length; // was 10 (wrong), now 5 (correct)
```

Node.js specifies that `Buffer.slice()` always returns a fixed-length
view (verified on Node.js v22).

## Fix

Replace `std::nullopt` with `newLength` in the
`isResizableOrGrowableShared()` branch of
`jsBufferPrototypeFunction_sliceBody`.

## Test

Added a regression test that creates a `Buffer` from a resizable
`ArrayBuffer`, slices it, resizes the buffer, and verifies the slice
length doesn't change.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
SUZUKI Sosuke
2026-02-09 21:48:20 +09:00
committed by GitHub
parent 4494170f74
commit b7475d8768
2 changed files with 20 additions and 1 deletions

View File

@@ -1957,7 +1957,7 @@ static JSC::EncodedJSValue jsBufferPrototypeFunction_sliceBody(JSC::JSGlobalObje
if (castedThis->isResizableOrGrowableShared()) {
auto* subclassStructure = globalObject->JSResizableOrGrowableSharedBufferSubclassStructure();
auto* uint8Array = JSC::JSUint8Array::create(lexicalGlobalObject, subclassStructure, WTF::move(buffer), byteOffset + startOffset, std::nullopt);
auto* uint8Array = JSC::JSUint8Array::create(lexicalGlobalObject, subclassStructure, WTF::move(buffer), byteOffset + startOffset, newLength);
RETURN_IF_EXCEPTION(throwScope, {});
if (!uint8Array) [[unlikely]] {
throwOutOfMemoryError(globalObject, throwScope);

View File

@@ -930,6 +930,25 @@ for (let withOverridenBufferWrite of [false, true]) {
expect(() => buf.subarray(0, 5)).toThrow(TypeError);
});
it("slice() on resizable ArrayBuffer returns fixed-length view", () => {
const rab = new ArrayBuffer(10, { maxByteLength: 20 });
const buf = Buffer.from(rab);
buf[0] = 1;
buf[1] = 2;
buf[2] = 3;
buf[3] = 4;
buf[4] = 5;
const sliced = buf.slice(0, 5);
expect(sliced.length).toBe(5);
expect(sliced[0]).toBe(1);
expect(sliced[4]).toBe(5);
// Growing the buffer should NOT change the slice length
rab.resize(20);
expect(sliced.length).toBe(5);
});
function forEachUnicode(label, test) {
["ucs2", "ucs-2", "utf16le", "utf-16le"].forEach(encoding =>
it(`${label} (${encoding})`, test.bind(null, encoding)),