Skip to content

ai slop - #37944

Closed
Jarred-Sumner wants to merge 2 commits into
mainfrom
claude/offthread-io-wasm-memory-buffers
Closed

ai slop#37944
Jarred-Sumner wants to merge 2 commits into
mainfrom
claude/offthread-io-wasm-memory-buffers

Conversation

@Jarred-Sumner

@Jarred-Sumner Jarred-Sumner commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator

This PR has been marked as AI slop and the description has been updated to avoid confusion or misleading reviewers.

Many AI PRs are fine, but sometimes they submit a PR too early, fail to test if the problem is real, fail to reproduce the problem, or fail to test that the problem is fixed. If you think this PR is not AI slop, please leave a comment.

Off-thread I/O (async node:fs read/write/readv/writev and buffer arguments,
zlib/brotli/zstd async writes, scrypt/pbkdf2, CompressionStream, HTTP parser,
Bun.markdown, shell redirects) borrows caller ArrayBuffers by pinning them.
A pin does not hold the pages of a non-shared WebAssembly.Memory or of a
resizable ArrayBuffer, so those are now reported as not pinnable and every
borrower works on a private copy of the requested window instead, writing
produced bytes back into the object's current storage on completion.
@coderabbitai

coderabbitai Bot commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The PR adds pin-or-copy handling for ArrayBuffer, WebAssembly memory, resizable buffers, asynchronous filesystem operations, compression streams, shell redirections, and related runtime consumers. Tests cover buffer movement, growth, detachment, write-back, and compression results.

ArrayBuffer lifecycle and consumers

Layer / File(s) Summary
Pinning contract and buffer lifecycle
src/jsc/..., src/runtime/api/MarkdownObject.rs, src/runtime/image/Image.rs, src/sql_jsc/mysql/MySQLValue.rs
ArrayBuffer access now reports NotPinned, Pinned, or MustCopy. Copied storage supports ranged access, output write-back, refresh, and cleanup.
Asynchronous filesystem and span buffers
src/runtime/node/node_fs.rs, src/runtime/node/types.rs, test/harness.ts, test/js/node/crypto/scrypt.test.ts, test/js/node/fs/fs.test.ts
Async filesystem operations pin requested ranges, use stand-in storage when needed, and write completed bytes back to JavaScript views.
Asynchronous compression buffers
src/js/node/zlib.ts, src/runtime/node/node_zlib_binding.rs, src/runtime/node/zlib/*, src/runtime/webcore/CompressionStreamCoder.rs, test/js/node/zlib/*
Compression writes retain pinned or substitute buffers, process copied inputs, write outputs back, and release state during completion or abandonment.
Shell and runtime consumers
src/runtime/shell/*, src/runtime/server/NodeHTTPResponse.rs, src/jsc/bindings/node/http/JSHTTPParserPrototype.cpp, test/js/bun/shell/bunshell.test.ts
Shell, subprocess, HTTP, and parser paths use direct buffers when pinned and copied views otherwise. Cleanup and command failure handling were updated.

Possibly related PRs

  • oven-sh/bun#37669: Both changes modify zlib native state handling in src/runtime/node/zlib/NativeZlib.rs.

Suggested reviewers: robobun, dylan-conway

Mergeability Score: 🟡 Moderate · up to 822a4

The PR changes async buffer handling to copy storage that can move, but shell redirection can still silently drop subprocess output when a resizable buffer changes during execution. Because this can cause concrete data loss for affected commands, merge should wait for that path to be fixed or explicitly accepted; the remaining issues are bounded follow-ups.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: copying ArrayBuffer storage when pinning cannot keep it in place.
Description check ✅ Passed The description fully addresses the requested change and verification sections with detailed behavior, edge cases, and test coverage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/runtime/shell/subproc.rs (1)

1600-1610: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Refreshed-to-empty storage now silently discards output bytes.

buf.refresh(...) sets array_buffer to the default empty descriptor when the held value has been detached. slice_mut() is then empty, idx >= array_buf_slice.len() is true, and append returns without writing and without advancing i. The subprocess output is dropped with no error.

Before this change the descriptor was a pin, so it could not become empty mid-run. With resizable buffers and WebAssembly memory the detached case is now reachable, which makes the existing TODO on Line 1603 a live data-loss path rather than a theoretical one.

Record the failure so the shell can report it, for example by setting an error on the PipeReader when refresh yields an empty buffer while bytes remain. Do you want me to open an issue to track this?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/shell/subproc.rs` around lines 1600 - 1610, Update the append
logic around buf.refresh, slice_mut, and the idx bounds check to record an error
on the relevant PipeReader when refresh leaves the buffer empty while bytes
still need to be written, instead of silently returning. Preserve normal copying
and index advancement for valid storage, and use the existing PipeReader
error-reporting mechanism.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runtime/image/Image.rs`:
- Around line 742-743: Update the method documentation near the storage pinning
logic to replace the outdated “We deliberately DON'T copy” statement with the
actual policy: use zero-copy for pinned storage, while duplicating storage that
cannot remain pinned, including FastTypedArray and WebAssembly.Memory. Keep the
comment focused on this durable, non-obvious behavior.

In `@src/runtime/node/node_fs.rs`:
- Around line 3850-3868: Update NodeFS::read so zero-length reads return before
evaluating args.buffer.slice() or otherwise accessing the backing store.
Preserve the existing behavior for nonzero reads, and ensure Read::from_js does
not leave a zero-length buffer vulnerable to background access.

In `@src/runtime/node/types.rs`:
- Around line 1379-1404: Update stand_in_for_spans to distinguish the Windows
total-size limit from genuine allocation failures, using a small error type or
equivalent variant that preserves both cases. Propagate that distinction to its
caller so the u32::MAX overflow maps to a range/size error, while reservation
and allocation failures continue mapping to status 2 out-of-memory.
- Around line 1493-1500: Replace the local magic status value 2 in the status
normalization block with a clearly named constant representing the pin-copy
failure condition, declared near the related status handling. Update the
subsequent match arms to use the same named constant while preserving the
existing FFI status values and behavior.
- Around line 1364-1375: Add a debug_assert_eq! in Node::release immediately
before the views/pins drain loop to verify both vectors have equal lengths,
preserving the existing unpin and unprotect behavior.

In `@src/runtime/shell/subproc.rs`:
- Around line 1576-1592: Refactor ArrayBufferStrong to expose a shared accessor
for current bytes that handles both pinned and unpinned states, then make both
slice and refresh use it instead of duplicating held/as_array_buffer lookup
logic. Because slice obtains the global internally, document on
BufferedOutput::slice that it must run on the JS thread, or pass the caller’s
JSGlobalObject through as append does.

In `@test/js/node/crypto/scrypt.test.ts`:
- Around line 94-118: Restrict the test around the fixture’s fs.read blockers to
non-Windows platforms, since Windows uses a separate libuv pool and may not
enforce the intended ordering. Add the platform guard using the test suite’s
existing platform-detection convention, while preserving the current WorkPool
synchronization and assertions on supported platforms.

In `@test/js/node/fs/fs.test.ts`:
- Around line 5100-5132: Extract the duplicated parked-stdin subprocess protocol
from the run helper in test/js/node/fs/fs.test.ts (lines 5100-5132) into a
shared test utility accepting fixture source, argv, and stdin byte counts for
the park and ready sentinels, returning { report, exitCode }. Replace the inline
driver in test/js/node/crypto/scrypt.test.ts (lines 119-145) with this utility
call; update both sites as described.
- Around line 5134-5187: Expand the test matrix around the run helper to cover
the missing resizable-buffer scenarios: add shrinking cases for write and readv,
plus stable move:false cases for write and writev. Assert each result, detached
state, and written or memory contents consistently with the existing
read/readv/write/writev tests.

In `@test/js/node/zlib/zlib-reset-race.test.ts`:
- Around line 166-190: Extend the resizableWriteFixture in
test/js/node/zlib/zlib-reset-race.test.ts:166-190 with a variant that resizes
resizable.buffer after h.write and before await done, then verify all
DeflateRaw, Brotli, and Zstd engines recover the original 64-byte input. Add the
matching resize-during-write case in
test/js/node/zlib/zlib-handle-bounds-check.test.ts:460-486, resizing between
h.write and the h.cb callback and asserting the recovered input remains
unchanged.

---

Outside diff comments:
In `@src/runtime/shell/subproc.rs`:
- Around line 1600-1610: Update the append logic around buf.refresh, slice_mut,
and the idx bounds check to record an error on the relevant PipeReader when
refresh leaves the buffer empty while bytes still need to be written, instead of
silently returning. Preserve normal copying and index advancement for valid
storage, and use the existing PipeReader error-reporting mechanism.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 00a79915-2ab7-49f2-8dfb-8d08238dbd90

📥 Commits

Reviewing files that changed from the base of the PR and between 165dc9f and 0ac7542.

📒 Files selected for processing (29)
  • src/js/node/zlib.ts
  • src/jsc/JSValue.rs
  • src/jsc/array_buffer.rs
  • src/jsc/bindings/bindings.cpp
  • src/jsc/bindings/headers-handwritten.h
  • src/jsc/bindings/node/http/JSHTTPParserPrototype.cpp
  • src/jsc/lib.rs
  • src/jsc/node_path.rs
  • src/runtime/api/MarkdownObject.rs
  • src/runtime/api/bun/spawn/stdio.rs
  • src/runtime/image/Image.rs
  • src/runtime/node/node_fs.rs
  • src/runtime/node/node_zlib_binding.rs
  • src/runtime/node/types.rs
  • src/runtime/node/zlib/NativeBrotli.rs
  • src/runtime/node/zlib/NativeZlib.rs
  • src/runtime/node/zlib/NativeZstd.rs
  • src/runtime/server/NodeHTTPResponse.rs
  • src/runtime/shell/Builtin.rs
  • src/runtime/shell/states/Cmd.rs
  • src/runtime/shell/subproc.rs
  • src/runtime/webcore/CompressionStreamCoder.rs
  • src/sql_jsc/mysql/MySQLValue.rs
  • test/js/bun/shell/bunshell.test.ts
  • test/js/node/crypto/scrypt.test.ts
  • test/js/node/fs/fs.test.ts
  • test/js/node/zlib/zlib-handle-bounds-check.test.ts
  • test/js/node/zlib/zlib-reset-race.test.ts
  • test/js/node/zlib/zlib.test.js

Comment thread src/runtime/image/Image.rs
Comment thread src/runtime/node/node_fs.rs
Comment thread src/runtime/node/types.rs
Comment thread src/runtime/node/types.rs Outdated
Comment thread src/runtime/node/types.rs Outdated
Comment thread src/runtime/shell/subproc.rs
Comment thread test/js/node/crypto/scrypt.test.ts Outdated
Comment thread test/js/node/fs/fs.test.ts
Comment thread test/js/node/fs/fs.test.ts
Comment thread test/js/node/zlib/zlib-reset-race.test.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and the bug-hunting pass found no issues. Given the breadth — an FFI ABI change to the pin primitive, new copy/write-back lifetimes threaded through fs/zlib/crypto/shell/HTTP off-thread paths, a new Drop on ArrayBufferStrong, and user-visible behavior changes (zlib handle.write() now returns a substitute buffer; shell blob-redirect failure now fails the command instead of throwing) — a human pass on the design and the memory-ownership edges is still worthwhile.

What was reviewed:

  • Bun::tryPin / ArrayBufferPin ABI (repr(u8) ↔ enum class : uint8_t) and every Rust/C++ caller updated in lockstep, including collectBufferSpans and borrowBytesForOffThread.
  • Pin/unpin balance: new ArrayBufferStrong::Drop vs. the removed manual unpins in subproc.rs/Builtin.rs; defuse_array_buffer_unpins now clears pinned so the finalizer-time drop is a no-op.
  • from_js_pinned_range window clamping and the write-back path (write_back / write_back_into) — bounds are clamped against the object's current storage, detached targets no-op.
  • zlib WriteBuffers lifecycle: buffers held until next write/close so the stream context's dangling pointers stay valid; release_write_buffers runs on every completion arm including this_value-null and VM-teardown.
Extended reasoning...

Overview

This PR reworks how async native operations borrow caller-supplied ArrayBuffer storage when a pin cannot actually hold that storage in place (non-shared WebAssembly.Memory buffers and resizable ArrayBuffers). It introduces a tri-state ArrayBufferPin returned across the C++/Rust FFI boundary, adds MarkedArrayBuffer::from_js_pinned{,_range} / private_copy / private_zeroed / write_back helpers, and threads the new must-copy path through ~15 call sites: async node:fs read/readv/write/writev and path buffers, the zlib/brotli/zstd native handle, scrypt/pbkdf2 (via StringOrBuffer), CompressionStream, NodeHTTPResponse, Bun.markdown, HTTPParser.execute, shell stdin/stdout redirects, and VectorArrayBuffer (readv/writev). It also adds Drop to ArrayBufferStrong, a new WriteBuffers field on the compression-stream mixin, and changes shell blob-redirect refusal from a thrown JS exception into a per-command stderr + exit-1.

Security risks

The change is squarely in the memory-safety category the repo's review guide flags as most-blocked: raw pointers into JS-owned storage held across thread boundaries, pin/unpin balance, and Drop ordering during VM finalization. The direction of the change is defensive (copy instead of holding a pointer that can be unmapped), but the surface area for getting an unpin/free wrong is large. No auth/crypto correctness or injection surface is touched; scrypt/pbkdf2 only change how the input bytes are borrowed.

Level of scrutiny

High. This is not mechanical: it changes an FFI signature, adds a Drop impl to a widely-held type, introduces new ownership states (owns_buffer + buffer.value set) on MarkedArrayBuffer, and reorders flush_write_result before release_write_buffers in the zlib completion path. It also makes user-visible behavior changes that are effectively API decisions — zlib handle.write() now returns undefined-or-a-Uint8Array and the JS wrapper consumes it, and $…` > ${blob}`` no longer rejects the whole shell promise. Those are reasonable choices but a maintainer should sign off on them.

Other factors

Test coverage is thorough (per-subsystem fixtures that force the worker to run after the storage moves via a parked-threadpool trick, plus no-move variants asserting the bytes land). The bug-hunting pass examined pin/unpin pairing on every early-return, the VectorArrayBuffer stand-in path (pins on already-pinned elements are still released via release()), the private_owned len == 0 dangling-ptr case (owns_buffer=false so destroy() skips it), and the ArrayBufferStrong finalizer defuse — none surfaced a defect. Deferring to a human because the scope and the number of interacting lifetimes exceed what an automated approval should clear on its own.

…in zlib tests, named stand-in errors, shared ArrayBufferStrong::current

- test/harness.ts: runParkedFixture drives the park/ready stdin protocol
  (stdout and stderr drained concurrently); fs, zlib and scrypt tests use it
- fs tests: complete the resizable/stay matrix
- zlib tests: resize the resizable buffer while the write is in flight
- scrypt/zlib parked tests skip Windows, where fs.read runs on libuv's pool
- zero-length async fs.read no longer carries a borrowed descriptor
- VectorArrayBuffer: assert views/pins pairing; a stand-in too large for one
  Windows I/O vector is a range error, not out-of-memory
- ArrayBufferStrong::current shared by refresh() and BufferedOutput::slice
- Image.rs: bring the copy-policy doc in line

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/runtime/node/types.rs (1)

1142-1147: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Reject bare ArrayBuffer path arguments.

Buffer::from_typed_array unwraps value.as_array_buffer(ctx), so a populated, non-NUL ArrayBuffer passes both validators and becomes a path. Node rejects bare ArrayBuffer with ERR_INVALID_ARG_TYPE. Remove JSType::ArrayBuffer from this arm and add a regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/node/types.rs` around lines 1142 - 1147, Update the path argument
match arm in the relevant validation function to exclude JSType::ArrayBuffer, so
bare ArrayBuffer values no longer reach Buffer::from_typed_array or become valid
paths. Preserve the Uint8Array and DataView handling, and add a regression test
asserting that a bare ArrayBuffer is rejected with Node’s ERR_INVALID_ARG_TYPE
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runtime/image/Image.rs`:
- Around line 709-711: Update the documentation near source_from_js to remove
the claim that resizable buffers are duplicated, since source_from_js rejects
resizable and shared ArrayBuffers before storage; document only the supported
non-shared buffer behavior.

In `@test/harness.ts`:
- Around line 628-648: Wrap the proc.stdout drain loop in the helper containing
stderr, stdout, parked, and released so any rejection from stdin.flush() or
stdin.end() cannot bypass cleanup. Ensure the stderr promise is awaited on both
successful and error paths, preserving the existing return values while
propagating the original stdin error after stderr has been drained.

In `@test/js/node/zlib/zlib.test.js`:
- Line 803: Update the child-process command around the `cmd` array to pass an
absolute path for `fixture.mjs`, resolving it from the existing `dir` variable
before spawning the child. Preserve the current executable and arguments while
ensuring the fixture path is suitable for file operations regardless of the
working directory.

---

Outside diff comments:
In `@src/runtime/node/types.rs`:
- Around line 1142-1147: Update the path argument match arm in the relevant
validation function to exclude JSType::ArrayBuffer, so bare ArrayBuffer values
no longer reach Buffer::from_typed_array or become valid paths. Preserve the
Uint8Array and DataView handling, and add a regression test asserting that a
bare ArrayBuffer is rejected with Node’s ERR_INVALID_ARG_TYPE behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6a2c88b3-820f-4115-aca3-e9035b1331a2

📥 Commits

Reviewing files that changed from the base of the PR and between 0ac7542 and 822a435.

📒 Files selected for processing (11)
  • src/jsc/array_buffer.rs
  • src/runtime/image/Image.rs
  • src/runtime/node/node_fs.rs
  • src/runtime/node/types.rs
  • src/runtime/shell/subproc.rs
  • test/harness.ts
  • test/js/node/crypto/scrypt.test.ts
  • test/js/node/fs/fs.test.ts
  • test/js/node/zlib/zlib-handle-bounds-check.test.ts
  • test/js/node/zlib/zlib-reset-race.test.ts
  • test/js/node/zlib/zlib.test.js

Comment on lines +709 to +711
/// We DON'T copy storage a pin can hold in place (small `FastTypedArray`s
/// and non-shared `WebAssembly.Memory`/resizable buffers are duplicated
/// instead): the encoded input can be tens of MB and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find where Image construction inspects `shared`/`resizable` on the source ArrayBuffer.
set -euo pipefail

rg -nP -C 12 '\bresizable\b|\bshared\b' src/runtime/image/Image.rs

# Locate the constructor / source-assignment path for classification context.
ast-grep outline src/runtime/image/Image.rs --items all

Repository: oven-sh/bun

Length of output: 11570


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '331,438p' src/runtime/image/Image.rs
sed -n '680,818p' src/runtime/image/Image.rs

Repository: oven-sh/bun

Length of output: 11665


Document one resizable-buffer policy. source_from_js rejects ab.resizable and ab.shared before storage. Remove the claim that resizable buffers are duplicated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/image/Image.rs` around lines 709 - 711, Update the documentation
near source_from_js to remove the claim that resizable buffers are duplicated,
since source_from_js rejects resizable and shared ArrayBuffers before storage;
document only the supported non-shared buffer behavior.

Source: Coding guidelines

Comment thread test/harness.ts
Comment on lines +628 to +648
const stderr = proc.stderr.text();
let stdout = "";
let parked = false;
let released = false;
const decoder = new TextDecoder();
for await (const chunk of proc.stdout) {
stdout += decoder.decode(chunk, { stream: true });
if (!parked && stdout.includes("park\n")) {
parked = true;
proc.stdin.write(Buffer.alloc(options.parkBytes ?? 16, "c"));
await proc.stdin.flush();
}
if (!released && stdout.includes("ready\n")) {
released = true;
proc.stdin.write(Buffer.alloc(options.readyBytes, "c"));
await proc.stdin.end();
}
}
const exitCode = await proc.exited;
const tail = stdout.slice(stdout.indexOf("ready\n") + "ready\n".length).trim();
return { report: tail ? JSON.parse(tail) : undefined, stderr: await stderr, exitCode };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Await stderr on every path so child diagnostics survive a stdin failure.

Line 628 starts draining stderr without awaiting it. The only await on that promise is in the return statement at line 648. If the child exits between the park\n sentinel and the stdin write, proc.stdin.flush() or proc.stdin.end() rejects with EPIPE, and that rejection propagates out of the for await loop. The helper then never awaits the stderr promise, so the caller sees the pipe error instead of the child's actual failure output, and the abandoned promise can surface as an unrelated unhandled rejection.

Wrap the drain loop so the helper always awaits stderr.

This helper now backs test/js/node/fs/fs.test.ts, test/js/node/crypto/scrypt.test.ts, and test/js/node/zlib/zlib.test.js, so the diagnostics matter for three suites.

♻️ Proposed change to always await the stderr drain
   const stderr = proc.stderr.text();
   let stdout = "";
   let parked = false;
   let released = false;
   const decoder = new TextDecoder();
-  for await (const chunk of proc.stdout) {
-    stdout += decoder.decode(chunk, { stream: true });
-    if (!parked && stdout.includes("park\n")) {
-      parked = true;
-      proc.stdin.write(Buffer.alloc(options.parkBytes ?? 16, "c"));
-      await proc.stdin.flush();
-    }
-    if (!released && stdout.includes("ready\n")) {
-      released = true;
-      proc.stdin.write(Buffer.alloc(options.readyBytes, "c"));
-      await proc.stdin.end();
-    }
-  }
-  const exitCode = await proc.exited;
+  let driveError: unknown;
+  try {
+    for await (const chunk of proc.stdout) {
+      stdout += decoder.decode(chunk, { stream: true });
+      if (!parked && stdout.includes("park\n")) {
+        parked = true;
+        proc.stdin.write(Buffer.alloc(options.parkBytes ?? 16, "c"));
+        await proc.stdin.flush();
+      }
+      if (!released && stdout.includes("ready\n")) {
+        released = true;
+        proc.stdin.write(Buffer.alloc(options.readyBytes, "c"));
+        await proc.stdin.end();
+      }
+    }
+  } catch (e) {
+    driveError = e;
+  }
+  const [text, exitCode] = await Promise.all([stderr, proc.exited]);
+  if (driveError !== undefined && exitCode === 0) throw driveError;
   const tail = stdout.slice(stdout.indexOf("ready\n") + "ready\n".length).trim();
-  return { report: tail ? JSON.parse(tail) : undefined, stderr: await stderr, exitCode };
+  return { report: tail ? JSON.parse(tail) : undefined, stderr: text, exitCode };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const stderr = proc.stderr.text();
let stdout = "";
let parked = false;
let released = false;
const decoder = new TextDecoder();
for await (const chunk of proc.stdout) {
stdout += decoder.decode(chunk, { stream: true });
if (!parked && stdout.includes("park\n")) {
parked = true;
proc.stdin.write(Buffer.alloc(options.parkBytes ?? 16, "c"));
await proc.stdin.flush();
}
if (!released && stdout.includes("ready\n")) {
released = true;
proc.stdin.write(Buffer.alloc(options.readyBytes, "c"));
await proc.stdin.end();
}
}
const exitCode = await proc.exited;
const tail = stdout.slice(stdout.indexOf("ready\n") + "ready\n".length).trim();
return { report: tail ? JSON.parse(tail) : undefined, stderr: await stderr, exitCode };
const stderr = proc.stderr.text();
let stdout = "";
let parked = false;
let released = false;
const decoder = new TextDecoder();
let driveError: unknown;
try {
for await (const chunk of proc.stdout) {
stdout += decoder.decode(chunk, { stream: true });
if (!parked && stdout.includes("park\n")) {
parked = true;
proc.stdin.write(Buffer.alloc(options.parkBytes ?? 16, "c"));
await proc.stdin.flush();
}
if (!released && stdout.includes("ready\n")) {
released = true;
proc.stdin.write(Buffer.alloc(options.readyBytes, "c"));
await proc.stdin.end();
}
}
} catch (e) {
driveError = e;
}
const [text, exitCode] = await Promise.all([stderr, proc.exited]);
if (driveError !== undefined && exitCode === 0) throw driveError;
const tail = stdout.slice(stdout.indexOf("ready\n") + "ready\n".length).trim();
return { report: tail ? JSON.parse(tail) : undefined, stderr: text, exitCode };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/harness.ts` around lines 628 - 648, Wrap the proc.stdout drain loop in
the helper containing stderr, stdout, parked, and released so any rejection from
stdin.flush() or stdin.end() cannot bypass cleanup. Ensure the stderr promise is
awaited on both successful and error paths, preserving the existing return
values while propagating the original stdin error after stderr has been drained.

async function run(body) {
using dir = tempDir("zlib-wasm-memory-view", { "fixture.mjs": prologue + body });
const { report, stderr, exitCode } = await runParkedFixture({
cmd: [bunExe(), "fixture.mjs"],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use an absolute fixture path.

Line 803 passes fixture.mjs as a relative path. Build the fixture path from dir before spawning the child.

Proposed fix
+import { join } from "node:path";
 import { bunEnv, bunExe, isWindows, runParkedFixture, tempDir, tmpdirSync } from "harness";
 
-      cmd: [bunExe(), "fixture.mjs"],
+      cmd: [bunExe(), join(String(dir), "fixture.mjs")],

As per coding guidelines: “Use absolute paths in file operations.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cmd: [bunExe(), "fixture.mjs"],
cmd: [bunExe(), join(String(dir), "fixture.mjs")],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/js/node/zlib/zlib.test.js` at line 803, Update the child-process command
around the `cmd` array to pass an absolute path for `fixture.mjs`, resolving it
from the existing `dir` variable before spawning the child. Preserve the current
executable and arguments while ensuring the fixture path is suitable for file
operations regardless of the working directory.

Source: Coding guidelines

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and the bug hunting system found no issues. Given the scope — a new pin-or-copy contract across the C++/Rust FFI boundary, buffer lifetime changes in async fs/zlib/shell/HTTP paths, and new/removed Drop impls on ArrayBufferStrong/BufferedOutput — a human look at the memory-ownership and write-back semantics is still worthwhile.

What was reviewed:

  • Bun::tryPin / ArrayBufferPin FFI contract and every Rust call site's handling of MustCopy.
  • MarkedArrayBuffer::from_js_pinned_range window clamping, write_back bounds against detached/shrunk targets, and private_owned's owns_buffer = len != 0 edge.
  • Zlib WriteBuffers lifecycle: flush_write_result now runs before release_write_buffers, and stand-ins persist until the next write/close.
  • ArrayBufferStrong::Drop vs the shell's defuse_array_buffer_unpins finalizer path; VectorArrayBuffer release/unpin pairing.
Extended reasoning...

Overview

This PR reworks how Bun borrows caller-supplied ArrayBuffer bytes for operations that outlive the calling JS turn. It introduces a three-state ArrayBufferPin (NotPinned/Pinned/MustCopy) returned by Bun::tryPin in C++ and mirrored in Rust, replacing the previous boolean pin. Storage a pin cannot hold in place (non-shared WebAssembly.Memory, resizable ArrayBuffer) is now copied into a private stand-in; for output buffers, results are written back into the JS object's current storage on completion. The change fans out across ~30 files: bindings.cpp, array_buffer.rs, async fs (node_fs.rs, types.rs), the zlib/brotli/zstd native handle, CompressionStream, the HTTP parser, NodeHTTPResponse, shell redirects (Builtin.rs, Cmd.rs, subproc.rs), Bun.markdown, PathLike, and VectorArrayBuffer. It also adds a Drop impl to ArrayBufferStrong, removes one from BufferedOutput, and changes shell error handling for immutable-blob redirects from a thrown exception to a per-command failure.

Security risks

No new attack surface is introduced; this is a memory-safety hardening. The risks are all in the direction the PR is trying to fix: use-after-free/write-to-freed-pages if a pin state is mishandled, double-unpin if a Drop and an explicit unpin() both fire, or leaked pins/roots on an error path. The review checked that MustCopy never reaches unpinArrayBuffer, that write_back_into clamps to the destination's live length, that VectorArrayBuffer::release only unpins Pinned entries, and that the shell finalizer's pinned = false defuse still short-circuits the new ArrayBufferStrong::Drop.

Level of scrutiny

High. Per REVIEW.md, native memory safety is the most-blocked category, and this PR touches exactly the patterns it calls out: pointers held across JS re-entry, cross-thread buffer ownership, refcount/pin balancing on every terminal path, and GC-visible Drop ordering. The FFI signature change (pinArrayBuffer bool → enum, collectBufferSpans callback gained a parameter) means every caller had to be updated correctly. The zlib change reorders flush_write_result relative to buffer release and introduces a substitute-input Uint8Array handed back to JS. The shell change replaces a bespoke PinnedArrayBuf wrapper with ArrayBufferStrong and its new Drop. These are all correct-looking but non-mechanical.

Other factors

The PR ships extensive new tests (fs/zlib/scrypt/shell) that exercise the mid-operation move for both wasm-memory and resizable-buffer storage, including move: false control cases, and the author addressed the substantive CodeRabbit feedback in commit 822a435 (harness helper extraction, debug_assert_eq! on views/pins, resize-mid-write zlib coverage, StandInError enum, ArrayBufferStrong::current shared accessor, zero-length read fix). The remaining unresolved CodeRabbit comments are trivial nits (magic status constant, variant-matrix completeness). No bugs were found by the multi-agent hunt. Still, a change of this breadth in pin/unpin lifecycle across a dozen subsystems warrants a maintainer's eyes before merge.

@robobun

robobun commented Aug 13, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 7:50 PM PT - Aug 12th, 2026

❌ @Jarred-Sumner, your commit 822a435 has some failures in Build #93701 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 37944

That installs a local version of the PR into your bun-37944 executable, so you can run:

bun-37944 --bun

@github-actions

Copy link
Copy Markdown
Contributor

This PR has been closed because it was flagged as AI slop.

Many AI-generated PRs are fine, but this one was identified as having one or more of the following issues:

  • Fails to verify the problem actually exists
  • Fails to test that the fix works
  • Makes incorrect assumptions about the codebase
  • Submits changes that are incomplete or misleading

If you believe this was done in error, please leave a comment explaining why.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants