Skip to content

blob: snapshot shared buffers before materializing bytes - #31648

Open
EffortlessSteven wants to merge 1 commit into
oven-sh:mainfrom
EffortlessSteven:claude/s3-write-stable-bytes
Open

blob: snapshot shared buffers before materializing bytes#31648
EffortlessSteven wants to merge 1 commit into
oven-sh:mainfrom
EffortlessSteven:claude/s3-write-stable-bytes

Conversation

@EffortlessSteven

@EffortlessSteven EffortlessSteven commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

What this does

new Blob([view]) (and S3 uploads through it) accept a SharedArrayBuffer- or resizable-ArrayBuffer-backed part from safe JS. Materializing the Blob reads it as a Rust &[u8] while another agent can mutate or resize that backing. That is undefined behavior under Rust's aliasing rules.

Fix: shared and resizable parts are copied in C++ (ArrayBuffer::copy_to_unshared) to a fresh non-shared buffer and read as owned bytes, so Rust never slices shared memory. Fixed unshared parts keep the zero-copy borrowed path.

Verification

Behavior-preserving: tests assert public behavior and pass on old code by design; the change is the removed &[u8] over shared backing. fmt clean.

Review map

  • array_buffer.rs: copy_to_unshared, reusing the existing FFI
  • Blob.rs: snapshot shared/resizable parts (single + multi-part); fast path for fixed unshared
  • blob.test.ts / s3.test.ts: SAB, resizable, DataView, multi-part, S3 input

@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.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

BlobExt::from_js_without_defer_gc now detects shared or resizable ArrayBuffer backing stores and snapshots them into fresh non-shared copies before extracting bytes, using a new FFI-backed snapshot_shared_array_buffer helper. This ensures Blob construction reads stable bytes from both single-buffer and multi-part joiner paths. Blob and S3 tests validate the snapshot behavior.

Changes

SharedArrayBuffer snapshotting in Blob

Layer / File(s) Summary
Snapshot helper and Blob path integration
src/runtime/webcore/Blob.rs
Adds internal snapshot_shared_array_buffer FFI helper using Bun__createArrayBufferForCopy to copy shared/resizable ArrayBuffer data, then integrates it into the single-ArrayBuffer fast path (3416–3426) and typed-array joiner loop (3619–3625); helper declared at 7446–7473.
Blob snapshot unit tests
test/js/web/fetch/blob.test.ts
Validates offset Uint8Array view snapshotting (excluding guard bytes), mixed-part concatenation with strings, multiple SAB view concatenation, DataView range snapshotting, and immutability of Blob bytes after construction via blob.arrayBuffer() comparisons (560–619).
S3 upload integration tests
test/js/bun/s3/s3.test.ts
Adds "SharedArrayBuffer upload (stable bytes)" suite (1804–1840) with fake S3 server and five test cases verifying S3 paths transmit exact bytes of offset views, raw SharedArrayBuffer, and zero-length uploads (1842–1897).

Possibly related PRs

  • oven-sh/bun#31493: Both PRs modify src/runtime/webcore/Blob.rs's typed-array byte-assembly path around StringJoiner usage—this PR adds snapshotting of shared/resizable ArrayBuffer bytes, while the retrieved PR adjusts lifetime-detached segment appending.
  • oven-sh/bun#31358: Both PRs modify from_js_without_defer_gc logic in src/runtime/webcore/Blob.rs; changes are related to Blob construction paths.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: snapshotting shared buffers before materializing Blob bytes to prevent undefined behavior.
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.
Description check ✅ Passed The PR description provides clear context on the issue (undefined behavior from concurrent mutation of SharedArrayBuffer-backed Blob parts), the fix (snapshotting shared/resizable parts), and verification approach (behavior-preserving tests, fmt clean).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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: 1

🤖 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 `@test/js/bun/s3/s3.test.ts`:
- Around line 1805-1828: Change the test suite declaration for
"SharedArrayBuffer upload (stable bytes)" to run concurrently by using the
concurrent variant of describe (e.g., replace describe(...) with
describe.concurrent(...)); update the block that defines fakeS3 (and any tests
inside that describe) to ensure they still create isolated servers (using port:
0) and rely on the per-test returned server/options/received so tests remain
independent when run in parallel.
🪄 Autofix (Beta)

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: 0cfa05dc-8eac-4d78-992d-37ef17331035

📥 Commits

Reviewing files that changed from the base of the PR and between 5ac120c and 50f6d5d.

📒 Files selected for processing (3)
  • src/runtime/webcore/Blob.rs
  • test/js/bun/s3/s3.test.ts
  • test/js/web/fetch/blob.test.ts

Comment thread test/js/bun/s3/s3.test.ts Outdated
@EffortlessSteven
EffortlessSteven force-pushed the claude/s3-write-stable-bytes branch from 50f6d5d to 5d5bf19 Compare June 1, 2026 05:30

@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: 8

🤖 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 @.factory/review-prompts/README.md:
- Around line 22-24: The README statement claiming security review "lives here
only" is inaccurate because bun-ai-review-gh-hosted.yml already defines a
security lane; update the text in .factory/review-prompts/README.md to remove or
rephrase that claim and accurately reflect that security reviews run from both
.github/workflows/droid-tag.yml (on-demand `@droid` review/security/fill) and
bun-ai-review-gh-hosted.yml (the automated security lane), mentioning the actual
workflow names (`.github/workflows/droid-tag.yml` and
`bun-ai-review-gh-hosted.yml`) and the `security` lane so readers aren’t misled.

In @.factory/skills/review-guidelines/SKILL.md:
- Around line 106-118: The fenced example block shown in the SKILL.md diff is
unlabeled and triggers MD040; update that triple-backtick fence to include the
language label "text" (i.e., change ``` to ```text) so the block is explicitly
marked as illustrative output and stops triggering the MD040 lint rule; locate
the example block in SKILL.md (the one containing the "No blocking finding..."
and "Best failed objection" sections) and add the label accordingly.

In @.github/workflows/bun-ai-review-gh-hosted.yml:
- Around line 246-255: The workflow currently hard-fails when FIREPASS is
missing by exporting FIREPASS in env and running the check `test -n
"${FIREPASS:-}" || { echo "::error::Missing FIREPASS secret"; exit 1; }`; remove
the FIREPASS requirement by either deleting the FIREPASS env line and its `test
-n "${FIREPASS:-}"` check, or change that check to a non-fatal warning (e.g.,
echo a warning without `exit 1`) so missing FIREPASS no longer fails the job;
update the run block where `test -n "${FIREPASS:-}"` appears to implement the
chosen approach.
- Around line 236-242: Replace the tag-based checkout action by pinning
actions/checkout to a specific commit SHA (replace uses: actions/checkout@v6
with uses: actions/checkout@<commit-sha>) so the workflow uses an immutable
release; also address the FIREPASS handling by either making the secret optional
where used (remove or change the unconditional test -n "${FIREPASS:-}" and
conditionalize injection of "apiKey": "${FIREPASS}") or require and document the
secret for jobs with pull-requests: write permissions to avoid injecting an
empty/dormant secret into the generated config.

In @.github/workflows/droid-tag.yml:
- Around line 43-52: The workflow currently declares env secrets GLM, FIREPASS,
and MINIMAX and then requires all three with test -n checks, but FIREPASS and
MINIMAX are unused and block runs; remove the unused secret declarations and
their guards: delete the FIREPASS and MINIMAX entries under the env: block and
remove the corresponding test -n "${FIREPASS:-}" and test -n "${MINIMAX:-}"
checks so only GLM remains enforced; keep the GLM env and its test -n check
intact to preserve the required credential validation.
- Around line 35-39: Update the workflow to pin actions/checkout to a specific
commit SHA instead of the mutable tag `actions/checkout@v6` (replace the
reference in the Checkout step with the chosen commit SHA), and make the
BYOK/FIREPASS/MINIMAX secret usage conditional: change the BYOK config step (the
logic that writes configs for FirePass/MiniMax) so it only requires or writes
FIREPASS/MINIMAX when those models are selected, or mark those secrets optional,
and ensure `review_model` and `security_model` (currently set to
`custom:GLM-5.1-ZAI-Coding-0`) only trigger generation of their respective
configs; in short, pin the checkout action to a commit SHA and guard the BYOK
secret/config generation behind the selected model checks to avoid failing runs
when unused.

In @.github/workflows/ub-review-packet.yml:
- Around line 31-88: Replace the mutable action refs in the UB review packet
workflow with pinned commit SHAs: change actions/checkout@v5,
dtolnay/rust-toolchain@master, actions/cache@v4,
EffortlessMetrics/ub-review@main, and actions/upload-artifact@v7 to the exact
commit SHAs (or tag+sha) used in other workflows (e.g., how
.github/workflows/vscode-release.yml pins actions) so the workflow always uses
immutable commits; update the five `uses:` entries accordingly and verify the
pinned SHAs resolve and pass CI.
- Around line 27-29: The workflow currently uses mutable action refs (e.g.,
actions/checkout@v5, dtolnay/rust-toolchain@master, actions/cache@v4,
EffortlessMetrics/ub-review@main, actions/upload-artifact@v7) which should be
pinned to immutable commit SHAs for supply-chain safety; update each uses:
reference in the workflow to the corresponding full commit SHA (replace the
tag/branch after the @ with the exact commit hash), leaving job-level env keys
like CARGO_TARGET_DIR and CARGO_INSTALL_JOBS unchanged, and ensure all updated
uses refer to the intended pinned commits.
🪄 Autofix (Beta)

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: 7d0fb8f4-189e-4361-a3c6-bf581d66079e

📥 Commits

Reviewing files that changed from the base of the PR and between 5d5bf19 and bc79264.

📒 Files selected for processing (5)
  • .factory/review-prompts/README.md
  • .factory/skills/review-guidelines/SKILL.md
  • .github/workflows/bun-ai-review-gh-hosted.yml
  • .github/workflows/droid-tag.yml
  • .github/workflows/ub-review-packet.yml

Comment thread .factory/review-prompts/README.md Outdated
Comment thread .factory/skills/review-guidelines/SKILL.md Outdated
Comment thread .github/workflows/bun-ai-review-gh-hosted.yml Outdated
Comment thread .github/workflows/bun-ai-review-gh-hosted.yml Outdated
Comment thread .github/workflows/droid-tag.yml Outdated
Comment thread .github/workflows/droid-tag.yml Outdated
Comment thread .github/workflows/ub-review-packet.yml Outdated
Comment thread .github/workflows/ub-review-packet.yml Outdated
@EffortlessSteven
EffortlessSteven force-pushed the claude/s3-write-stable-bytes branch 2 times, most recently from 5d5bf19 to 00e594b Compare June 2, 2026 17:42

@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: 2

🤖 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/webcore/Blob.rs`:
- Around line 3416-3426: The fast-paths that take a raw &[u8] from JS backing
memory must snapshot shared/resizable ArrayBuffers before creating Rust borrows;
update the small-buffer file-write fast path inside write_file_internal so that
instead of passing buffer_view.byte_slice() directly into
write_bytes_to_file_fast you call snapshot_shared_array_buffer(global,
&array_buffer) when array_buffer.shared || array_buffer.resizable and then use
the snapshot's byte_slice(); apply the same pattern used around Blob::try_create
(top_value.as_array_buffer / snapshot_shared_array_buffer) to the
write_bytes_to_file_fast call and the other similar call sites (the ones
referenced around write_file_internal and the other locations) so no
shared/resizable backing ever produces a direct Rust &[u8].

In `@test/js/web/fetch/blob.test.ts`:
- Around line 560-602: Add a new test that constructs a Blob from a
SharedArrayBuffer view (e.g., using new Uint8Array(sab, offset, length) or new
DataView(...)), then mutates the underlying SharedArrayBuffer after Blob
construction, and finally asserts that await blob.arrayBuffer() still yields the
original pre-mutation bytes; reference the existing test patterns like Blob
snapshots a nonzero-offset SharedArrayBuffer view and Blob snapshots DataView
parts over SharedArrayBuffer to locate where to add the test and use the same
arrayBuffer() assertion style to verify immutability.
🪄 Autofix (Beta)

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: cce5bebb-1a78-4dd8-9494-d72fe7b5c1ac

📥 Commits

Reviewing files that changed from the base of the PR and between bc79264 and 00e594b.

📒 Files selected for processing (3)
  • src/runtime/webcore/Blob.rs
  • test/js/bun/s3/s3.test.ts
  • test/js/web/fetch/blob.test.ts

Comment thread src/runtime/webcore/Blob.rs
Comment thread test/js/web/fetch/blob.test.ts Outdated
@EffortlessSteven
EffortlessSteven force-pushed the claude/s3-write-stable-bytes branch from 00e594b to 43a5090 Compare June 2, 2026 18:05

@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.

♻️ Duplicate comments (1)
src/runtime/webcore/Blob.rs (1)

7446-7472: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Snapshot coverage is still incomplete.

This helper fixes Blob materialization, but write_file_internal() still has a direct fast path at Lines 5194-5221 that feeds buffer_view.byte_slice() into write_bytes_to_file_fast(). Bun.write(pathOrFd, sharedOrResizableView) can therefore still materialize a Rust &[u8] over mutable JS backing and bypass this new safeguard entirely.

Suggested fix
-            } else if let Some(buffer_view) = data.as_array_buffer(global_this) {
+            } else if let Some(buffer_view) = data.as_array_buffer(global_this) {
                 if buffer_view.byte_len < 256 * 1024 {
                     let pathlike: PathOrFileDescriptor = match &*path_or_blob {
                         PathOrBlob::Path(p) => p.clone(),
                         PathOrBlob::Blob(b) => b
                             .store()
                             .expect("infallible: store present")
                             .data
                             .as_file()
                             .pathlike
                             .clone(),
                     };
+                    let snapshot = if buffer_view.shared || buffer_view.resizable {
+                        Some(snapshot_shared_array_buffer(global_this, &buffer_view)?)
+                    } else {
+                        None
+                    };
+                    let bytes = snapshot
+                        .as_ref()
+                        .map(|snapshot| snapshot.byte_slice())
+                        .unwrap_or_else(|| buffer_view.byte_slice());
                     let result = if matches!(pathlike, PathOrFileDescriptor::Path(_)) {
                         write_bytes_to_file_fast::<true>(
                             global_this,
                             &pathlike,
-                            buffer_view.byte_slice(),
+                            bytes,
                             &mut needs_async,
                         )
                     } else {
                         write_bytes_to_file_fast::<false>(
                             global_this,
                             &pathlike,
-                            buffer_view.byte_slice(),
+                            bytes,
                             &mut needs_async,
                         )
                     };
🤖 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/webcore/Blob.rs` around lines 7446 - 7472, The fast path in
write_file_internal that calls buffer_view.byte_slice() and then
write_bytes_to_file_fast can still create a Rust &[u8] over mutable JS backing;
update that fast path to detect shared or resizable ArrayBuffer-backed views and
snapshot them first using the existing snapshot_shared_array_buffer (which calls
Bun__createArrayBufferForCopy) before calling byte_slice() or pass the copied
ArrayBuffer into the write path so the Rust code never borrows into
shared/mutable JS memory; specifically, in the code handling
buffer_view.byte_slice() inside write_file_internal, branch to call
snapshot_shared_array_buffer(global, array_buffer) and use the returned
ArrayBuffer for subsequent byte slicing/writing instead of the original backing
store.
🤖 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.

Duplicate comments:
In `@src/runtime/webcore/Blob.rs`:
- Around line 7446-7472: The fast path in write_file_internal that calls
buffer_view.byte_slice() and then write_bytes_to_file_fast can still create a
Rust &[u8] over mutable JS backing; update that fast path to detect shared or
resizable ArrayBuffer-backed views and snapshot them first using the existing
snapshot_shared_array_buffer (which calls Bun__createArrayBufferForCopy) before
calling byte_slice() or pass the copied ArrayBuffer into the write path so the
Rust code never borrows into shared/mutable JS memory; specifically, in the code
handling buffer_view.byte_slice() inside write_file_internal, branch to call
snapshot_shared_array_buffer(global, array_buffer) and use the returned
ArrayBuffer for subsequent byte slicing/writing instead of the original backing
store.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8d367d8d-d10f-4892-bd70-0c39fcde585e

📥 Commits

Reviewing files that changed from the base of the PR and between 00e594b and 43a5090.

📒 Files selected for processing (3)
  • src/runtime/webcore/Blob.rs
  • test/js/bun/s3/s3.test.ts
  • test/js/web/fetch/blob.test.ts

@EffortlessSteven
EffortlessSteven force-pushed the claude/s3-write-stable-bytes branch from 74972f4 to 758e462 Compare June 2, 2026 21:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant