Skip to content

Buffer.concat(x).slice(...) returns dangling view — data is zeroed/freed when the concat result isn't held in a local #1177

Description

@proggeramlug

Summary

Buffer.concat(chunks).slice(0, N) returns a Buffer whose .length is correct but whose bytes read as undefined — the slice appears to be a view into the temporary Buffer.concat(...) result's memory, and that memory is reclaimed (or otherwise no longer readable) the moment the chained expression's only strong reference moves to the slice. Storing the concat result in a named const first keeps it alive and the slice reads correctly.

Surfaced while verifying #1132 (closed) — the issue-body repro printed RESULT: , , , , , , , (eight empty values) until I rewrote it to store Buffer.concat(chunks) in a temporary first.

Minimal repro — no I/O, fully static input

```ts
const chunks: any[] = [
Buffer.from([0x89, 0x50, 0x4E, 0x47]),
Buffer.from([0x0D, 0x0A, 0x1A, 0x0A]),
];

// A) Bare Buffer.concat without .slice — works
console.log(Array.from(Buffer.concat(chunks)).join(","));
// → "137,80,78,71,13,10,26,10" ✅

// B) Chained concat().slice(), result stored — BROKEN
const b1 = Buffer.concat(chunks).slice(0, 8);
console.log("len=" + b1.length, Array.from(b1).join(","));
// → "len=8 , , , , , , , " ❌ (length right, bytes undefined)

// C) Chained concat().slice() inline — BROKEN (same as B)
console.log(Array.from(Buffer.concat(chunks).slice(0, 8)).join(","));
// → " , , , , , , , " ❌

// D) Store concat first, then slice — WORKS
const concat = Buffer.concat(chunks);
console.log(Array.from(concat.slice(0, 8)).join(","));
// → "137,80,78,71,13,10,26,10" ✅
```

Verified on `perry 0.5.1016` (host `darwin-arm64`, current main `5ee9a043`).

Why this matters

The chained `Buffer.concat(chunks).slice(0, N)` shape is idiomatic Node. Every `Stream` / `http.IncomingMessage` / `https.request` example in the wild reads bodies that way:

```ts
resp.on("end", () => {
const buf = Buffer.concat(chunks);
// …or, more commonly:
const head = Array.from(Buffer.concat(chunks).slice(0, 8));
});
```

Anyone who writes the natural one-liner gets silently wrong bytes — no `undefined` thrown, no length mismatch, just zero/undefined values reaching downstream code (file-type detection, magic-number checks, JSON parses of byte arrays, etc.).

Root-cause hypothesis

`Buffer.prototype.slice` (in Node, deprecated in 16 in favor of `subarray`) returns a view that shares the parent's `ArrayBuffer` rather than copying. Perry's implementation appears to mirror that — the returned Buffer's data pointer aims into the parent's bytes.

Pre-fix that's fine for `const p = Buffer.concat(c); p.slice(0,8)` because `p` is rooted via the JS local.

For `Buffer.concat(chunks).slice(0,8)`, the only strong reference is to the slice, not the parent. The parent Buffer's `BufferHeader` (or its backing arena allocation) isn't pinned by the slice's GC scanner, so the next `gc_check_trigger` reclaims the parent's bytes. The slice's data pointer survives as a length-correct view into freed memory.

Fix shapes that would resolve it:

  1. `Buffer.slice` copies bytes rather than returning a view (matches Node 16+ guidance to use `Buffer.subarray` for views and `Buffer.from(parent.subarray(...))` for copies — Perry could just make `slice` always copy).
  2. GcHeader edge from slice → parent so the slice's scanner marks the parent as reachable.
  3. `Buffer.concat` returns a Buffer whose data lives in a static/long-lived arena, not the nursery, when its only consumers are temporary view ops.

Option 1 is the smallest behavioral change and matches Node's modern recommendation.

Workaround

Always store the `Buffer.concat` result in a `const` before slicing:

```ts
const buf = Buffer.concat(chunks);
const head = Array.from(buf.slice(0, 8)); // works
```

Environment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions