Skip to content

blob: make StoreRef::data_mut unsafe, drop Sync - #30812

Open
robobun wants to merge 4 commits into
mainfrom
farm/4a1dfa50/storeref-not-sync
Open

blob: make StoreRef::data_mut unsafe, drop Sync#30812
robobun wants to merge 4 commits into
mainfrom
farm/4a1dfa50/storeref-not-sync

Conversation

@robobun

@robobun robobun commented May 15, 2026

Copy link
Copy Markdown
Collaborator

Closes #30800.

Bug

StoreRef::data_mut(&self) -> &mut Data minted a &mut from a shared
&self via a raw-pointer deref:

pub fn data_mut(&self) -> &mut Data {
    unsafe { &mut (*self.as_ptr()).data }
}

unsafe impl Sync for StoreRef {}

With Sync, two threads can share a &StoreRef and each call data_mut()
— two &mut Data to the same backing allocation, immediate UB through a
safe API. Even single-threaded, two back-to-back data_mut() calls on the
same &StoreRef alias two &mut Data.

Fix

Four complementary changes:

  1. data_mut is unsafe fn with a precondition of exclusivity (no
    other &/&mut to the pointee live for the borrow's duration, on
    this thread or any other). Every call site (~22) gets a SAFETY note
    describing its single-owner context.
  2. unsafe impl Sync for StoreRef removed. Send is retained: the
    intrusive ThreadSafeRefCount makes moving the handle between threads
    sound, and the threadpool tasks that carry a StoreRef require only
    Send, not Sync.
  3. File.last_modified is now AtomicU64 (closes the race flagged
    in review). do_read_file clones the backing StoreRef into each
    ReadFile task it spawns, so N concurrent file.bytes() calls
    schedule N workers that all observe the same Store allocation.
    Previously the workers all wrote file.last_modified on that shared
    Store concurrently — technically UB even though every writer stores
    the same fstat-derived mtime so the race is idempotent. The
    AtomicU64 + shared-borrow (&store.data) path on worker threads
    closes the observable data race; the JS-thread read in
    get_last_modified uses the same atomic load (Relaxed).
  4. Compile-time trip-wire. A __store_ref_not_sync module uses the
    auto-trait-ambiguity pattern (same trick as
    src/runtime/shell/subproc.rs __pipe_reader_thread_confined): if
    StoreRef ever regains Sync, two blanket NotSyncCheck impls
    apply and _NOT_SYNC fails to compile with "conflicting impls".

Remaining narrow aliasing (documented)

resolve_file_stat (called from JS-thread get_last_modified /
resolve_size / resolved_size only on the first access before a
worker has run) still materializes &mut File to write
max_size/mode/seekable. Two worker-pool &File borrowers can
overlap it: ReadFile tasks, which only touch the AtomicU64
last_modified (the atomic closes that observable race), and POSIX
WriteFile::run_with_fd, which reads the non-atomic seekable/mode
on fd-backed destinations — a pre-existing data-level race this PR
documents but does not close. Both overlaps are benign in practice:
every overlapping writer produces the same stat-derived value
(idempotent) and seekable/mode/max_size are small Copy types
written once per Store lifetime. Converting the rest of File to
interior-mutable fields would close both and is legitimate follow-up
work but out of scope for this soundness-audit PR.

Verification

  • cargo check --workspace — clean, no new warnings.
  • bun bd test test/js/web/fetch/blob*.test.ts test/js/web/streams/readable-stream-blob-consumed.test.ts test/js/bun/globals.test.js
    — 44 blob + 20 globals + 1 streams tests pass under ASAN.
  • New test Bun.file().bytes() is safe under high concurrency fans out
    16 files × 8 concurrent reads, each cloning the backing StoreRef
    into an independent ReadFile task — the primary
    worker-thread-reaches-into-shared-Store path — and asserts every
    task returns the full uncorrupted file bytes under bun bd (ASAN).

Regression guards

Like the sibling soundness PRs (#30773 etc.), this is a Rust-API-soundness
change with no user-visible behavioral delta — the existing call sites
produced identical output before and after (every writer stores the same
idempotent mtime, whether through a plain assignment or a Relaxed atomic
store), so a runtime fail→pass demonstration around the JS-visible API is
not possible. The regression guards are:

  • the compile-time __store_ref_not_sync trip-wire in webcore_types.rs
    (any future unsafe impl Sync for StoreRef fails the build with
    conflicting impls);
  • the unsafe fn data_mut signature (forces new call sites to justify
    their exclusivity claim in writing);
  • test/internal/storeref-not-sync.test.ts, a source-level guard in the
    style of test/internal/source-lints/dead-code-escapes.test.ts that asserts the
    three invariants on the text of webcore_types.rs (no
    unsafe impl Sync for StoreRef, data_mut spelled pub unsafe fn,
    trip-wire present). All three assertions fail against the pre-fix
    source and pass with this PR, giving a readable suite-level failure
    alongside the trip-wire's rustc error.

Separately, bun bd bootstrap in the gate container trips on a
pre-existing bake-codegen failure related to #30679 (the bootstrap
release-bun is older than the JSON-lexer fix that lets define: accept
raw CSS). A tiny JSON.stringify wrapper on the OVERLAY_CSS value is
included as a standalone commit to unblock bun bd in stale-bootstrap
environments; it's semantically identical to the current form once the
lexer fix propagates but works with both old and new release-bun
snapshots.

Rebase on main (conflict resolution)

The review iterations were collapsed into a single commit so the diff can
be rebased cleanly onto main's evolving Blob refactors; the net change
is the six webcore source files plus the two regression tests.

The one non-trivial conflict was with #33599 ("encode content_type
ownership in the type"), which refactored the exact mechanism this PR's
Body.rs changes touched: it replaced the manual content_type raw
pointer + content_type_allocated flag with a BlobContentType type and
dropped the allocated parameter from set_blob_content_type. Took
main's superseding mechanism wholesale (blob.content_type.set(BlobContentType::from(mime_type)),
set_blob_content_type(blob, mime_type)) and re-applied only this PR's
soundness layer on top: blob_store_mut and set_blob_content_type are
unsafe fn, and their call sites (the body-consumer content-type and
MimeType::TEXT sites) write unsafe { … } + SAFETY. No behavioral
change.

Earlier rebases took main's deletion of src/parsers/json_lexer.rs
(JSON-parser rewrite #33032) as-is (this PR's only change there was a
comment fix) and dropped an unrelated bake-codegen.ts bootstrap tweak,
and reconciled with the Delete PathString refactor (#31579) in the
structured-clone deserialization site.

Verified after the final rebase: bun bd test green on the two guards
(14 pass) plus body.test.ts + blob.test.ts (376 pass, including
#33599's "does not free a static pointer" ownership tests), and the
source-level guard still fails 0/3 against main's webcore_types.rs.


[review] gate passed · iteration 44 · 8 files touched

fails on main (without fix)
ASAN without fix: 3 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/storeref-not-sync.test.ts test/js/web/fetch/blob-write.test.ts
bun test v1.4.0 (3c38b4bc1)

test/internal/source-lints/storeref-not-sync.test.ts:
29 |   //
30 |   // Anchored to the start of a line (`^\s*unsafe`) so `// `-prefixed prose —
31 |   // e.g. the trip-wire comment in `webcore_types.rs`, which quotes the exact
32 |   // phrase — can never match.
33 |   const hasSyncImpl = /^\s*unsafe impl\s+Sync\s+for\s+StoreRef\b/m.test(source);
34 |   expect(hasSyncImpl).toBe(false);
                           ^
error: expect(received).toBe(expected)

Expected: false
Received: true

      at <anonymous> (/workspace/bun/test/internal/source-lints/storeref-not-sync.test.ts:34:23)
(fail) StoreRef does not implement Sync [5.74ms]
36 | 
37 | test("StoreRef::data_mut is an unsafe fn", () => {
38 |   // The precondition-bearing signature: every call site must assert, in an
39 |   // `unsafe` block, that no aliasing `&`/`&mut` to the pointee is live.
40 |   const hasUnsafeDataMut = /pub unsafe fn data_mut\s*\(\s*&self
... (truncated)

release without fix: 3 FAILED
bun test v1.4.0-canary.1 (c7807973a)

test/internal/source-lints/storeref-not-sync.test.ts:
29 |   //
30 |   // Anchored to the start of a line (`^\s*unsafe`) so `// `-prefixed prose —
31 |   // e.g. the trip-wire comment in `webcore_types.rs`, which quotes the exact
32 |   // phrase — can never match.
33 |   const hasSyncImpl = /^\s*unsafe impl\s+Sync\s+for\s+StoreRef\b/m.test(source);
34 |   expect(hasSyncImpl).toBe(false);
                           ^
error: expect(received).toBe(expected)

Expected: false
Received: true

      at <anonymous> (/workspace/bun/test/internal/source-lints/storeref-not-sync.test.ts:34:23)
(fail) StoreRef does not implement Sync [0.27ms]
36 | 
37 | test("StoreRef::data_mut is an unsafe fn", () => {
38 |   // The precondition-bearing signature: every call site must assert, in an
39 |   // `unsafe` block, that no aliasing `&`/`&mut` to the pointee is live.
40 |   const hasUnsafeDataMut = /pub unsafe fn data_mut\s*\(\s*&self\s*\)/.test(source);
41 |   expect(hasUnsafeDataMut).toBe(true);
                                ^
error: expect(received).toBe(expected)

Expected: true
Received: false

      at <anonymous> (/workspace/bun/test/i
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/storeref-not-sync.test.ts test/js/web/fetch/blob-write.test.ts
bun test v1.4.0 (3c38b4bc1)

test/internal/source-lints/storeref-not-sync.test.ts:
(pass) StoreRef does not implement Sync [4.65ms]
(pass) StoreRef::data_mut is an unsafe fn [4.35ms]
(pass) the __store_ref_not_sync compile-time trip-wire is present [4.92ms]

test/js/web/fetch/blob-write.test.ts:
(pass) blob.write() throws for data-backed blob [4.92ms]
(pass) Bun.write() throws for a data-backed blob destination [5.60ms]
(pass) Bun.file(path).write() does not throw [21.73ms]
(pass) blob.unlink() throws for data-backed blob [3.72ms]
(pass) blob.delete() throws for data-backed blob [3.09ms]
(pass) Bun.file(path).unlink() does not throw [26.28ms]
(pass) Bun.file(path).delete() does not throw [11.96ms]
(pass) blob.writer() throws for data-backed blob [3.31ms]
(pass) Bun.file(path).writer() does not throw [335.53ms]
(pass) blob.stat() returns undefined for data-backed blob [4.92ms]
(pass) Bun.file(path).stat() returns stats [11.79ms]
(pass) Bun.file(pat
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 648ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/6] gen generated_host_exports.rs
generated_host_exports.rs: 92 exports (host=3, lazy=10, generic=79, rust=0); 240 extern-C blocks audited
[1/6] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m   Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m   Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m   Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m   Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
�[1m�[92m   Compiling�[0m bun_brotli v
... (truncated)
diff hotspot
src/jsc/webcore_types.rs                           |  79 ++++++--
 src/runtime/webcore/Blob.rs                        | 211 +++++++++++++++------
 src/runtime/webcore/Body.rs                        |  60 ++++--
 src/runtime/webcore/FileReader.rs                  |  27 ++-
 src/runtime/webcore/blob/copy_file.rs              |  12 +-
 src/runtime/webcore/blob/read_file.rs              |  19 +-
 .../source-lints/storeref-not-sync.test.ts         |  54 ++++++
 test/js/web/fetch/blob-write.test.ts               |  42 ++++
 8 files changed, 390 insertions(+), 114 deletions(-)

gate history · 2 passed · 0 rejected · iteration 44

evidence per changed file
file                                                  reads  edits  tests
src/jsc/webcore_types.rs                                 25     23     30
src/runtime/webcore/Blob.rs                              60     59     27
src/runtime/webcore/Body.rs                              23     23     26
src/runtime/webcore/FileReader.rs                        16     12     27
src/runtime/webcore/blob/copy_file.rs                     7      6     26
src/runtime/webcore/blob/read_file.rs                    15      8     26
test/internal/source-lints/storeref-not-sync.test.ts      0      0      0
test/js/web/fetch/blob-write.test.ts                      5      6     26

root cause · written by the author bot

The root cause was that StoreRef declared unsafe impl Sync while its safe method data_mut(&self) minted a &mut Data from a shared reference via a raw pointer, so two threads sharing a &StoreRef could obtain aliasing mutable references, which is undefined behavior, and File::last_modified writes could also race. The fix removes the Sync impl, makes data_mut an unsafe fn with a documented exclusivity contract, and converts call sites to shared borrows or tightly scoped unsafe blocks so mutable access only occurs where exclusivity actually holds. It also changes File::last_modified to an Atomi…

@coderabbitai

coderabbitai Bot commented May 15, 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

This PR makes StoreRef::data_mut unsafe, removes Sync, replaces File::last_modified with AtomicU64, updates blob/file call sites to use shared borrows or scoped unsafe with atomic stores, and adds a concurrency test; it also stringifies OVERLAY_CSS to avoid older build-cache JSON issues.

Changes

StoreRef Safety and Atomic Timestamp Fix

Layer / File(s) Summary
StoreRef safety contract and File atomic timestamp
src/jsc/webcore_types.rs
StoreRef::data_mut becomes unsafe fn with expanded exclusivity/aliasing preconditions. unsafe impl Sync is removed (keeping Send) with a compile-time trip-wire. File::last_modified changes from JSTimeType to AtomicU64; Default and Clone snapshot via Ordering::Relaxed.
Blob access: shared borrows, scoped unsafe, atomics
src/runtime/webcore/Blob.rs
Read-only paths switch to shared &store.data. Mutation paths snapshot tag/need from shared borrows, release before calling resolve_file_stat, then re-borrow. Sites that mutate bytes or files use tightly-scoped unsafe { store.data_mut() }. last_modified updates use atomic .store(..., Relaxed).
ReadFile and FileReader borrow-safety changes
src/runtime/webcore/FileReader.rs, src/runtime/webcore/blob/read_file.rs
FileReader::on_start clones File locally for Lazy::open_file_blob to avoid mutable aliasing. ReadFile::resolve_size_and_last_modified and ReadFileUV::on_file_initial_stat use shared &store.data and update last_modified via atomic .store(..., Relaxed).
CopyFileWindows unsafe borrowing for fd preparation
src/runtime/webcore/blob/copy_file.rs
prepare_read_write_loop wraps destination and source file_store.data_mut() in unsafe blocks with SAFETY comments, passing .as_file_mut().pathlike into prepare_pathlike while preserving existing error handling and mkdirp behavior.
Bytes-backed mutations via scoped unsafe
src/runtime/webcore/Blob.rs (multiple sites)
shared_view_raw, structured-clone deserialization, Blob__setAsFile, jsdom_file_construct_, S3BlobDownloadTask::on_s3_download_resolved, and Any::to_internal_blob_if_possible switch to narrowly-scoped unsafe { store.data_mut() } for stored_name/bytes installs.
resolve_file_stat and write_file_internal atomics
src/runtime/webcore/Blob.rs
resolve_file_stat obtains mutable File via unsafe { store.data_mut() }.as_file_mut() and updates last_modified using atomic store(stat_to_js_mtime(...), Relaxed). write_file_internal resets last_modified via atomic store.
Concurrent file bytes access safety test
test/js/web/fetch/blob-write.test.ts
New regression test Bun.file().bytes() is safe under high concurrency creates 16 temp files and runs 8 overlapping file.bytes() reads per file, asserting decoded bytes match expected content.
OVERLAY_CSS JSON stringification for build-cache compatibility
src/codegen/bake-codegen.ts
OVERLAY_CSS in the Bun build define mapping is wrapped with JSON.stringify() to avoid JSON lexing/build-cache failures in older Bun revisions when raw CSS begins with unquoted *{…}.

Suggested reviewers

  • Jarred-Sumner
  • RiskyMH
  • dylan-conway
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR fully addresses all coding requirements from #30800: (1) makes data_mut unsafe with exclusivity precondition and ~22 call site SAFETY notes; (2) removes unsafe impl Sync while retaining Send; (3) converts File.last_modified to AtomicU64 with Relaxed atomic operations; (4) adds compile-time trip-wire to prevent Sync reintroduction.
Out of Scope Changes check ✅ Passed All code changes align with the PR objectives: core soundness fixes (StoreRef/File.last_modified), test additions for concurrency safety, and a minimal bake-codegen JSON.stringify wrapper to unblock bootstrap—no unrelated modifications detected.
Description check ✅ Passed The description clearly explains the bug, fix, scope, regression guards, and verification results, although it uses different headings from the template.
Title check ✅ Passed The title is concise and clearly identifies the primary API soundness changes to StoreRef.

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

@robobun

robobun commented May 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 4:06 PM PT - Aug 15th, 2026

🔄 @robobun, the build for your commit eee5f40c (Build #98737) was cancelled — waiting for the next build...

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. fix(StoreRef): remove unsound Sync impl #30805 - Also closes StoreRef Sync is unsound: data_mut(&self) enables mutable aliasing via safe API #30800 by removing the unsound Sync impl on StoreRef due to data_mut(&self) aliasing

🤖 Generated with Claude Code

Comment thread src/runtime/webcore/blob/read_file.rs Outdated
Comment thread src/runtime/webcore/blob/copy_file.rs Outdated
Comment thread src/jsc/webcore_types.rs Outdated
Comment thread src/runtime/webcore/Blob.rs Outdated

@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

Caution

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

⚠️ Outside diff range comments (1)
src/runtime/webcore/blob/copy_file.rs (1)

1376-1407: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Remove the unnecessary data_mut() borrows from this setup path.

Lines 1385 and 1405 mint &mut Data only to pass pathlike into prepare_pathlike(), but that helper never mutates the PathOrFileDescriptor. On a shared StoreRef, this unnecessary mutable borrow reopens the aliasing hazard the PR is trying to eliminate.

♻️ Suggested fix
     fn prepare_pathlike(
-        pathlike: &mut PathOrFileDescriptor,
+        pathlike: &PathOrFileDescriptor,
         must_close: &mut bool,
         is_reading: bool,
     ) -> bun_sys::Result<Fd> {
         if let PathOrFileDescriptor::Path(path) = pathlike {
             let fd = match bun_sys::openat_windows_a(

     self.read_write_loop.destination_fd = match Self::prepare_pathlike(
-            &mut unsafe { self.destination_file_store.data_mut() }
-                .as_file_mut()
+            &self.destination_file_store
+                .data
+                .as_file()
                 .pathlike,
             &mut self.read_write_loop.must_close_destination_fd,
             false,
         ) {

     self.read_write_loop.source_fd = match Self::prepare_pathlike(
-            &mut unsafe { self.source_file_store.data_mut() }
-                .as_file_mut()
+            &self.source_file_store
+                .data
+                .as_file()
                 .pathlike,
             &mut self.read_write_loop.must_close_source_fd,
             true,
         ) {
🤖 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/copy_file.rs` around lines 1376 - 1407, The mutable
borrows from destination_file_store.data_mut() and source_file_store.data_mut()
are unnecessary and reintroduce aliasing risk; change these to immutable borrows
(use the non-mutating accessor, e.g., data()/as_file() instead of
data_mut()/as_file_mut()) when extracting .pathlike and call
Self::prepare_pathlike(...) with that shared reference while preserving the
mutable out-parameter read_write_loop.must_close_destination_fd (and the source
equivalent); update both assignments to self.read_write_loop.destination_fd and
self.read_write_loop.source_fd to use the immutable accessors and keep error
handling/throw/mkdirp logic unchanged for CopyFileWindows and prepare_pathlike.
🤖 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/FileReader.rs`:
- Around line 360-367: The SAFETY comment is insufficient to justify calling
unsafe { store.data_mut() } because data_mut() requires proving no &Data/&mut
Data borrows exist on any thread (worker threads may hold &Data via
StoreRef/Blob clones); fix by removing the unsafe store.data_mut() usage in
on_start and instead clone the File/Blob instance and mutate the cloned File
(take &mut file on the cloned File) so you never call store.data_mut() on the
shared Store; update open_file_blob, ReadFile spawn points, and the on_start
path to operate on the cloned File (or otherwise ensure tasks that spawn
ReadFile run after on_start) to eliminate the aliasing risk and make the SAFETY
reasoning trivial for store.data_mut().

---

Outside diff comments:
In `@src/runtime/webcore/blob/copy_file.rs`:
- Around line 1376-1407: The mutable borrows from
destination_file_store.data_mut() and source_file_store.data_mut() are
unnecessary and reintroduce aliasing risk; change these to immutable borrows
(use the non-mutating accessor, e.g., data()/as_file() instead of
data_mut()/as_file_mut()) when extracting .pathlike and call
Self::prepare_pathlike(...) with that shared reference while preserving the
mutable out-parameter read_write_loop.must_close_destination_fd (and the source
equivalent); update both assignments to self.read_write_loop.destination_fd and
self.read_write_loop.source_fd to use the immutable accessors and keep error
handling/throw/mkdirp logic unchanged for CopyFileWindows and prepare_pathlike.
🪄 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: 53bfa435-5921-4484-a1bd-2bd2e8e97d10

📥 Commits

Reviewing files that changed from the base of the PR and between e4523d1 and 82e4873.

📒 Files selected for processing (7)
  • src/codegen/bake-codegen.ts
  • src/jsc/webcore_types.rs
  • src/runtime/webcore/Blob.rs
  • src/runtime/webcore/FileReader.rs
  • src/runtime/webcore/blob/copy_file.rs
  • src/runtime/webcore/blob/read_file.rs
  • test/js/web/fetch/blob-write.test.ts

Comment thread src/runtime/webcore/FileReader.rs Outdated
Comment thread src/jsc/webcore_types.rs Outdated
Comment thread src/runtime/webcore/FileReader.rs Outdated
@robobun

robobun commented May 15, 2026

Copy link
Copy Markdown
Collaborator Author

CI status

Build #98737 (sha eee5f40, latest) ran 177 lanes green with zero test failures before being canceled by a maintainer; the evidence check (fail-before/pass-after on ASAN and release) is green.

The one red lane on the previous build (#98731, debian-13-x64-asan) was two main-side issues, neither in this diff: the known Fd::close() EBADF double-close assertion crash tracked in #38630 (it SIGABRTed blob.test.ts and took nine sibling shards with it), and the Bun.file(path).slice(start, end) streams only the slice failure, which reproduces on a pure-main debug build (slice(0,5).stream() reads the whole file; release clamps correctly) and has been reported to main-break triage.

This PR is the one kept for #30800 (#30805 was closed as contained in it); it is rebased onto current main, MERGEABLE, with all review threads resolved.

@robobun
robobun force-pushed the farm/4a1dfa50/storeref-not-sync branch from 958ecea to 66617cf Compare May 23, 2026 12:12

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

No new issues found, but this is a substantial Rust soundness audit (unsafe-fn migration across ~22 data_mut/blob_store_mut call sites, !Sync drop, AtomicU64 field conversion, plus an unrelated bake-codegen bootstrap workaround) that's gone through several rounds of correction — worth a human pass before merge. There's also one minor unaddressed inline note on FileReader.rs:382.

Extended reasoning...

Overview

This PR closes #30800 by making StoreRef::data_mut an unsafe fn, removing unsafe impl Sync for StoreRef, converting File.last_modified from JSTimeType (u64) to AtomicU64, and adding a compile-time !Sync trip-wire. It touches 8 files: src/jsc/webcore_types.rs (core type definitions), src/runtime/webcore/{Blob,Body,FileReader}.rs, src/runtime/webcore/blob/{copy_file,read_file}.rs, a new concurrency stress test, and an unrelated src/codegen/bake-codegen.ts bootstrap workaround. Roughly 22 call sites are wrapped in unsafe { } with per-site SAFETY justifications, and several read paths were refactored from data_mut() to shared &store.data borrows.

Security risks

None in the conventional sense — no auth, crypto, network, or input-parsing surface. The change is a Rust memory-model soundness audit. The risk profile is correctness of unsafe-code reasoning: each SAFETY comment is a claim about aliasing/threading that, if wrong, masks UB. Several such claims were already corrected during review (false "sole holder" / "JS thread blocked" justifications, the blob_store_mut sibling back-door, the last_modified data race), and the PR description itself documents a remaining "benign" &mut File vs &File overlap in resolve_file_stat as out-of-scope follow-up.

Level of scrutiny

High. This is production-critical unsafe Rust in the core Blob/Store types that back Bun.file(), fetch bodies, FileReader, and S3. The PR went through four rounds of substantive correction (three from the bug-hunting system, one from CodeRabbit), each of which found real inaccuracies in the SAFETY reasoning. The FileReader::on_start refactor introduces a small behavioral divergence from the Zig original (dropped is_atty write-back to the shared Store) that the author has not yet acknowledged in the comment or addressed. The bundled bake-codegen.ts change is logically unrelated to the soundness fix and should at minimum be called out to the human reviewer.

Other factors

  • One unresolved 🟡 inline comment from a prior run (FileReader.rs:382) flags the dropped is_atty cache write-back; impact is negligible but the PR's "no user-visible behavioral delta" claim should account for it.
  • CI build #54800 has one failing lane (html-rewriter-leak.test.ts on debian-13-x64-asan) that the author argues is a known flake unrelated to this diff; that determination should be confirmed by a maintainer.
  • The AtomicU64 conversion is type-correct (JSTimeType = u64 in Rust per src/jsc/lib.rs:2299) and the new Clone for File snapshots it via Relaxed load, which is sound.
  • Test coverage: a new 16×8 concurrency stress test exercises the worker-thread ReadFile path under ASAN; existing blob/streams/globals suites pass per the description.

Given the complexity, the iterative correction history, the documented residual aliasing, and the bundled unrelated codegen change, this warrants human sign-off rather than auto-approval.

Comment thread src/jsc/webcore_types.rs Outdated
Comment thread src/runtime/webcore/FileReader.rs Outdated

@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 `@src/jsc/webcore_types.rs`:
- Around line 1238-1253: The unsafe Send impl on StoreRef is relying on a
runtime convention because Data::S3 contains Option<Rc<S3Credentials>> which is
not Send; file a follow-up issue to make this sound by replacing
Rc<S3Credentials> with Arc<S3Credentials>, update all uses (including
S3::init_with_referenced_credentials and any places that clone or drop
S3Credentials) to use Arc cloning semantics, remove the runtime-only caveat from
the unsafe impl once conversion is done, and add tests exercising cross-thread
StoreRef drop paths to ensure no Rc->Arc regressions.
🪄 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: fa7076cb-0b7b-4904-b52a-2a8eb0ab51f3

📥 Commits

Reviewing files that changed from the base of the PR and between 66617cf and 7b3b1d6.

📒 Files selected for processing (4)
  • src/jsc/webcore_types.rs
  • src/runtime/webcore/Blob.rs
  • src/runtime/webcore/FileReader.rs
  • test/js/web/fetch/blob-write.test.ts

Comment thread src/jsc/webcore_types.rs Outdated
Comment thread src/codegen/bake-codegen.ts Outdated
Comment thread src/runtime/webcore/Blob.rs Outdated
Comment thread src/jsc/webcore_types.rs Outdated

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

All prior review feedback has been addressed and I found nothing further on fbf4013, but this is a substantial unsafe-Rust soundness change to core StoreRef/Blob threading semantics (Send/Sync impls, AtomicU64 field conversion, ~22 unsafe call-site justifications across 10 files) that warrants a human sign-off.

Extended reasoning...

Overview

This PR closes #30800 by making StoreRef::data_mut an unsafe fn, removing unsafe impl Sync for StoreRef, converting File::last_modified from JSTimeType to AtomicU64, and adding a compile-time !Sync trip-wire. It touches src/jsc/webcore_types.rs (the canonical Store/StoreRef/File definitions), Blob.rs, Body.rs, FileReader.rs, copy_file.rs, read_file.rs, plus a new concurrency test, two stale-comment fixups in json_lexer.rs/bun-build-api.test.ts, and a bootstrap workaround in bake-codegen.ts.

Security risks

None in the conventional sense (no auth/crypto/input-handling). The risk surface is memory-safety: this PR rewrites the Send/Sync story for an intrusively-refcounted heap type that crosses the JS↔worker-pool thread boundary, converts a field to atomic, and restructures borrow discipline at ~22 data_mut call sites plus 4 blob_store_mut sites. An error here could introduce UB (aliased &mut, torn reads) rather than a logic bug.

Level of scrutiny

High. This is exactly the category the approval guidelines flag: complex, large, touches critical code paths, and centers on unsafe Rust semantics. The PR went through seven rounds of inline review from me (false SAFETY claims, the last_modified worker-thread race, the blob_store_mut sibling hole, the dropped is_atty write-back, the Rc<S3Credentials> Send caveat, stale cross-references), all of which were addressed — but the volume of subtle corrections itself indicates this needs a human maintainer's eyes on the final state, particularly the documented-but-unfixed resolve_file_stat &mut File vs worker-thread &File overlap and the FileReader::on_start behavior change (dropped is_atty cache write-back).

Other factors

  • A sibling PR #30805 was flagged as a potential duplicate also closing #30800; a maintainer should reconcile which lands.
  • The bake-codegen.ts JSON.stringify wrapper is an unrelated bootstrap workaround riding along in this PR.
  • CI on the prior push had one known-flaky failure (html-rewriter-leak.test.ts) unrelated to this diff.
  • The bug hunting system found nothing on the current revision (fbf4013).

@robobun
robobun force-pushed the farm/4a1dfa50/storeref-not-sync branch from fbf4013 to 0e8c2f6 Compare May 27, 2026 23:20
Comment thread src/runtime/webcore/Body.rs Outdated
@robobun
robobun force-pushed the farm/4a1dfa50/storeref-not-sync branch from ea7da7d to 9243c97 Compare June 2, 2026 05:42

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

No remaining issues from the automated pass, but this is a substantial Rust soundness change (unsafe-fn boundary moves, !Sync, AtomicU64 field, ~22 SAFETY justifications, an acknowledged &mut File/&File overlap in resolve_file_stat, and an intentional behavioral divergence in FileReader::on_start) that warrants a human sign-off.

Extended reasoning...

Overview

This PR closes #30800 by making StoreRef::data_mut an unsafe fn, removing unsafe impl Sync for StoreRef, converting File::last_modified to AtomicU64, and adding a compile-time !Sync trip-wire. It touches 10 files: the core webcore_types.rs type definitions, ~22 data_mut call sites across Blob.rs/Body.rs/FileReader.rs/read_file.rs/copy_file.rs (each wrapped in unsafe + SAFETY or refactored to shared borrows), the sibling blob_store_mut/set_blob_content_type helpers in Body.rs (also made unsafe fn), a new concurrency stress test, and a small bake-codegen.ts bootstrap workaround plus two stale-comment cleanups.

Security risks

None in the conventional sense — no auth, crypto, network input parsing, or data-exposure surfaces are touched. The risk class here is memory-safety / UB: the PR rearranges &mut-from-&self boundaries on an intrusively-refcounted heap type that crosses threads, and explicitly leaves one acknowledged compiler-level &mut File vs worker-thread &File overlap in resolve_file_stat (documented as idempotent/benign, with full interior-mutability conversion deferred as follow-up). It also documents-but-does-not-fix the pre-existing Rc<S3Credentials> !Send gap under unsafe impl Send for StoreRef.

Level of scrutiny

High. This is a soundness audit of unsafe Rust touching threading, atomics, and aliasing rules in a core webcore type. The PR went through ~8 rounds of review feedback, each surfacing subtle inaccuracies in SAFETY justifications (handle-uniqueness vs borrow-exclusivity conflation, wrong threading-model claims, stale cross-references). All were addressed and resolved, but the iteration count itself demonstrates the subtlety. There is also one intentional behavioral change: FileReader::on_start now clones File and discards open_file_blob's is_atty write-back (diverging from the Zig original), which the PR documents as a deliberate trade-off.

Other factors

  • All prior inline comments (mine and CodeRabbit's) are resolved; the bug-hunting system found nothing new on this revision.
  • A potential-duplicate bot flagged #30805 as also closing #30800.
  • The PR description itself flags maintainer-level decisions: the remaining resolve_file_stat aliasing, the Blob: Sync follow-up, and the Rc→Arc S3 credentials follow-up.
  • CI history includes one unrelated flake (html-rewriter-leak.test.ts) on a single lane.

Given the scope, the unsafe-code surface, and the explicit design trade-offs, this should not be auto-approved.

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

All my prior feedback has been addressed and I found nothing new this pass — but this is a substantial unsafe-Rust soundness audit (drops Sync, rewrites ~22 data_mut call sites' aliasing contracts, converts File.last_modified to AtomicU64, and leaves a documented-but-unclosed &mut File&File overlap in resolve_file_stat), so it warrants a human sign-off.

Extended reasoning...

Overview

This PR closes #30800 by hardening the StoreRef mutable-access contract: data_mut becomes unsafe fn, unsafe impl Sync for StoreRef is removed (with a compile-time trip-wire), File.last_modified becomes AtomicU64 to close a real cross-thread race on the worker-pool ReadFile path, and ~22 call sites across Blob.rs / Body.rs / FileReader.rs / read_file.rs / copy_file.rs are rewritten to either (a) use shared &store.data borrows where mutation isn't needed, or (b) wrap data_mut() in narrowly-scoped unsafe with per-site SAFETY justifications. Two ancillary changes ride along: a JSON.stringify wrapper on OVERLAY_CSS in bake-codegen.ts (bootstrap workaround) and a stale-comment cleanup in json_lexer.rs.

Security risks

None identified. This is an internal Rust soundness/aliasing audit with no user-facing API surface, no auth/crypto/permissions, and no untrusted-input handling changes. The bake-codegen.ts change is a build-time string-quoting tweak.

Level of scrutiny

High. The change is large (~9 files), touches production-critical webcore Blob/Store/file-I/O paths that run on both the JS thread and worker-pool threads, and consists almost entirely of unsafe-block edits whose correctness depends on subtle threading/aliasing invariants. The PR has gone through ~8 rounds of review feedback (mine and CodeRabbit's) correcting SAFETY-comment factual claims — handle-uniqueness vs borrow-exclusivity conflations, wrong thread-model citations, the Rc<S3Credentials> !Send caveat, the blob_store_mut/set_blob_content_type safe back-doors — all now addressed. The remaining resolve_file_stat &mut File vs worker-thread &File overlap is explicitly documented as out-of-scope follow-up rather than closed.

Other factors

  • All 12 of my prior inline comments are resolved; the most recent commits (9243c979, 26a4daf9, 90ee114c, 90d446ac, 23982cdd) directly address them.
  • A sibling PR #30805 also closes #30800 — a maintainer should decide which lands.
  • CI history shows one flaky-test failure (html-rewriter-leak.test.ts) unrelated to this diff.
  • The PR's own description flags two acknowledged loose ends for follow-up: Blob: Sync tightening and Rc<S3Credentials>Arc.
  • No CODEOWNERS hit that I'm aware of, but the scope (core webcore unsafe Rust + cross-thread invariants) is well outside what I'd approve without a maintainer look.

@robobun
robobun force-pushed the farm/4a1dfa50/storeref-not-sync branch from 23982cd to f378b91 Compare June 7, 2026 13:44
Comment thread src/runtime/webcore/Blob.rs Outdated
Comment thread src/jsc/webcore_types.rs Outdated
Comment thread src/runtime/webcore/FileReader.rs Outdated
Comment thread test/internal/source-lints/storeref-not-sync.test.ts
Comment thread src/runtime/webcore/FileReader.rs
Comment thread src/runtime/webcore/FileReader.rs Outdated
@robobun
robobun force-pushed the farm/4a1dfa50/storeref-not-sync branch from b8db52c to 6c329fe Compare July 2, 2026 07:32
Comment thread src/runtime/webcore/Blob.rs

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

All prior inline feedback has been addressed and I found nothing new on this revision, but this is a substantial soundness audit of StoreRef/Store (unsafe-fn boundary changes across ~22 call sites, AtomicU64 conversion, a documented remaining data race in resolve_file_stat, and an intentional behavioral divergence in FileReader::on_start) — worth a maintainer's eyes before merge.

Extended reasoning...

Overview

This PR closes #30800 by making StoreRef::data_mut an unsafe fn, removing unsafe impl Sync for StoreRef, converting File::last_modified to AtomicU64, and adding a compile-time !Sync trip-wire. It touches six source files in src/jsc/ and src/runtime/webcore/ (Blob, Body, FileReader, read_file, copy_file, webcore_types) plus two tests. The change is a Rust-API soundness audit with no intended user-visible behavioral delta — every data_mut() call site (~22) is wrapped in unsafe {} with a SAFETY comment, several read-only sites are converted to shared &store.data borrows, blob_store_mut/set_blob_content_type/shared_view_raw/set_is_ascii_flag are also made unsafe fn, and FileReader::on_start is refactored to clone File locally instead of mutating the shared Store.

Security risks

No direct security-sensitive surface (auth/crypto/permissions). However, this is memory-safety code in a core intrusively-refcounted data structure that crosses thread boundaries via the work pool. The PR explicitly documents a remaining data-level race (resolve_file_stat's JS-thread &mut File vs. WriteFile::run_with_fd's worker-thread &File reading non-atomic seekable/mode) as benign-in-practice and out of scope; and the unsafe impl Send for StoreRef still relies on a runtime convention (S3 stores never cross threads) rather than a type-system guarantee. Both are pre-existing, honestly documented, and flagged for follow-up — but they are the kind of thing a maintainer should sign off on rather than a bot.

Level of scrutiny

High. This is not a mechanical change: it redraws the unsafe boundary for a shared-mutable data structure used by Blob/File/Response/Request/S3/streams, and the correctness of each SAFETY comment depends on non-local threading and re-entrancy invariants. The PR went through ~18 rounds of inline review feedback (all now resolved) correcting SAFETY-comment accuracy, stale cross-references, and one structural fix (the AtomicU64 conversion after I flagged a real cross-thread race). That iteration history itself indicates the subtlety involved.

Other factors

  • All prior inline threads resolved. The latest commit (a46029c) addressed my most recent comment about shared_view_raw/set_is_ascii_flag being safe back-doors; no new issues found on re-review.
  • Intentional behavioral divergence. FileReader::on_start now discards open_file_blob's is_atty write-back to the shared Store (documented as a deliberate trade-off vs. reintroducing the aliasing hazard). Impact is negligible but it is a divergence from the Zig original.
  • Duplicate PR. github-actions flagged #30805 as also closing #30800; a maintainer should confirm which one lands.
  • Test coverage. The new concurrency test exercises the primary cross-thread path under ASAN; the source-level storeref-not-sync.test.ts and compile-time trip-wire guard the invariants. The PR description is candid that a runtime fail→pass demonstration isn't possible for a soundness-only change.

Given the scope, the documented-but-unclosed races, and the potential duplicate, this warrants human review rather than auto-approval.

Comment thread src/runtime/webcore/Blob.rs Outdated
Comment thread src/runtime/webcore/Blob.rs Outdated
Comment thread src/runtime/webcore/Blob.rs Outdated
Comment thread src/runtime/webcore/Blob.rs Outdated
Comment thread src/runtime/webcore/Blob.rs Outdated
Comment thread src/runtime/webcore/Blob.rs
Comment thread src/runtime/webcore/Body.rs
Comment thread src/runtime/webcore/Body.rs
Comment thread src/runtime/webcore/FileReader.rs
Comment thread src/runtime/webcore/FileReader.rs Outdated
Comment thread src/runtime/webcore/blob/copy_file.rs
Comment thread src/runtime/webcore/blob/read_file.rs Outdated
Comment thread src/runtime/webcore/blob/read_file.rs Outdated
Comment thread src/runtime/webcore/Body.rs Outdated
Comment thread test/internal/storeref-not-sync.test.ts Outdated
Comment thread src/runtime/webcore/Blob.rs Outdated
…t unsafe fn

Comment-cop flagged the paragraph-length justifications; cut them to the
load-bearing SAFETY contracts and cross-references (the canonical borrow
discipline lives on resolve_size). resolve_file_stat was the remaining
safe fn discharging data_mut's precondition with a caller-context claim,
so it now carries the contract in its signature like its four siblings;
its three callers discharge it at the call site. Also drop a stray
reject-path qualifier and fix the dead-code-escapes guard path.
Comment thread src/runtime/webcore/Blob.rs
Comment thread src/runtime/webcore/Blob.rs
Comment thread src/runtime/webcore/Blob.rs
Comment thread src/runtime/webcore/Body.rs
Comment thread src/runtime/webcore/FileReader.rs
Comment thread src/runtime/webcore/blob/read_file.rs
Comment thread src/runtime/webcore/Blob.rs
Comment thread src/jsc/webcore_types.rs Outdated
Comment thread test/internal/source-lints/storeref-not-sync.test.ts
…dress review nits

The previous commit's fail-before simulation restored webcore_types.rs
from HEAD before committing, silently reverting its comment trims; this
re-applies them. Also: drop the nonexistent init_with_referenced_credentials
citation (the real Rc-sharing site is Rc::clone(s3.get_credentials()) in
Blob.rs), make get_last_modified's dead non-File arm unreachable! like its
five siblings, and move the source-level guard to test/internal/source-lints/
where pure source-text lints belong.
Comment thread src/jsc/webcore_types.rs
Comment thread src/jsc/webcore_types.rs Outdated
Comment thread src/jsc/webcore_types.rs
Comment thread src/jsc/webcore_types.rs
Comment thread src/jsc/webcore_types.rs Outdated
Comment thread src/jsc/webcore_types.rs
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.

StoreRef Sync is unsound: data_mut(&self) enables mutable aliasing via safe API

1 participant