Repository navigation
RacyCell: replace the unconditional Sync impl with an owner-thread runtime check - #31178
Jarred-Sumner wants to merge 4 commits into
Conversation
…ntime check RacyCell<T> was unconditionally Sync over an unsynchronized Cell<T>; the single-thread contract lived only in comments, so any static reachable from a worker thread was one unsafe deref away from a silent data race. RacyCell now carries an owner-thread latch (AtomicU64). The first thread to call a checked accessor (get/read/write) claims the cell; any later call from a different thread panics, in release builds too. Cross-thread access through safe-callable accessors is now a deterministic panic instead of UB. Sites whose synchronization is genuinely external (write-once-before-spawn, mutex-guarded, Once-published, address-only) move to new unsafe *_unsync accessors whose call sites document the happens-before edge. Every RacyCell static was audited and classified; the two layout-sensitive FFI statics (ValueUndefined/ValueTrue exported for TinyCC, the Bun__FFI__offsets extern static) get dedicated repr(transparent) wrappers since RacyCell is no longer repr(transparent). Cost: one TLS read + one relaxed atomic load + compare per checked access, plus 8 bytes per static.
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughThis PR refactors ChangesRacyCell core refactoring and widespread call-site updates
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
|
Updated 5:10 AM PT - May 21st, 2026
❌ @Jarred-Sumner, your commit 1c25170 has 1 failures in 🧪 To try this PR locally: bunx bun-pr 31178That installs a local version of the PR into your bun-31178 --bun |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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/bun_core/util.rs`:
- Around line 2436-2448: The match in claim_slow incorrectly panics on
compare_exchange returning Err(me) for reentrant same-thread access; update the
handling of compare_exchange on self.owner to treat Err(owner) where owner == me
as a successful claim (i.e., return/continue) and only panic when owner != me.
Locate claim_slow and the call to self.owner.compare_exchange and change the
Err(owner) branch to check owner == me (allowing the reentrant path) otherwise
keep the existing panic with the same message.
In `@src/crash_handler/lib.rs`:
- Around line 336-340: The change replaces the previous checked accessor for
SELF_DEBUG_INFO with an unsafe get_unsync(), which reintroduces cross-thread
races for lazy init and mutation (notably SelfInfo::address_map) because
dump_stack_trace() and other non-crash paths can still reach this helper; revert
to using the original checked accessor (the safe/get_checked-style API used
previously for SELF_DEBUG_INFO) instead of get_unsync(), remove the unsafe block
around accessing the slot, and keep the deterministic failure semantics so
concurrent access and lazy init remain protected.
In `@src/resolver/resolver.rs`:
- Around line 6081-6084: The BIN_FOLDERS initialization is racy because callers
(e.g., dir_info_uncached() invoked from dir_info_for_resolution()) may not hold
RESOLVER_MUTEX; replace the manual check/write of BIN_FOLDERS when
!BIN_FOLDERS_LOADED with a proper single‑time guard: either use a dedicated
BIN_FOLDERS_LOCK (acquire before checking/writing BIN_FOLDERS and store
BIN_FOLDERS_LOADED under the lock) or use a std::sync::Once to run the one‑time
initializer that writes
(*BIN_FOLDERS.get_unsync()).write(BinFolderArray::default()) and sets the loaded
flag; apply the same change to the other occurrence around lines 6127–6133 so
both sites perform atomic one‑time initialization of BIN_FOLDERS instead of the
current racy pattern.
- Around line 4039-4043: bin_dirs() currently reads BIN_FOLDERS without
synchronization which races with append() writes; fix by acquiring
BIN_FOLDERS_LOCK inside bin_dirs() and produce an immutable snapshot while
holding the lock (e.g., clone the BoundedArray contents into a Vec or other
owned collection) and return that snapshot instead of returning a direct slice
into the unsynchronized BoundedArray; reference BIN_FOLDERS, BIN_FOLDERS_LOCK,
BIN_FOLDERS_LOADED, append(), and the bin_dirs() function when making the
change.
🪄 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: 80150ff2-f81c-4498-869c-bff83b083b23
📒 Files selected for processing (19)
src/bun_core/output.rssrc/bun_core/util.rssrc/bundler/ThreadPool.rssrc/collections/bit_set.rssrc/crash_handler/lib.rssrc/install/PackageInstall.rssrc/install/repository.rssrc/install/windows-shim/main.rssrc/io/source.rssrc/jsc/FFI.rssrc/parsers/json.rssrc/perf/hw_timer.rssrc/resolver/lib.rssrc/resolver/node_fallbacks.rssrc/resolver/resolver.rssrc/runtime/cli/create_command.rssrc/runtime/cli/upgrade_command.rssrc/runtime/ffi/ffi_body.rssrc/runtime/test_runner/jest.rs
| // SAFETY: BIN_FOLDERS protected by BIN_FOLDERS_LOCK at write sites; | ||
| // `BIN_FOLDERS_LOADED` (acquire) guarantees init. | ||
| unsafe { (*BIN_FOLDERS.get()).assume_init_ref().const_slice() } | ||
| // `BIN_FOLDERS_LOADED` (acquire) guarantees init. Unsync — readers and | ||
| // writers are on different resolver threads; the lock + acquire flag | ||
| // are the synchronization. | ||
| unsafe { (*BIN_FOLDERS.get_unsync()).assume_init_ref().const_slice() } |
There was a problem hiding this comment.
bin_dirs() still reads BIN_FOLDERS without synchronization.
BIN_FOLDERS_LOADED only publishes the one-time default initialization. Later append() calls mutate the same BoundedArray under BIN_FOLDERS_LOCK, but this method returns a slice without taking that lock or reading from an immutable published snapshot. That leaves a real read/write race on the array contents and length.
🤖 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/resolver/resolver.rs` around lines 4039 - 4043, bin_dirs() currently
reads BIN_FOLDERS without synchronization which races with append() writes; fix
by acquiring BIN_FOLDERS_LOCK inside bin_dirs() and produce an immutable
snapshot while holding the lock (e.g., clone the BoundedArray contents into a
Vec or other owned collection) and return that snapshot instead of returning a
direct slice into the unsynchronized BoundedArray; reference BIN_FOLDERS,
BIN_FOLDERS_LOCK, BIN_FOLDERS_LOADED, append(), and the bin_dirs() function when
making the change.
There was a problem hiding this comment.
This is the pre-existing race called out in the PR description under "Multi-thread findings" (#2): bin_dirs() reads const_slice() without taking BIN_FOLDERS_LOCK while append() mutates under the lock on another resolver thread. This PR is a mechanical audit/conversion of the RacyCell accessors and intentionally does not change the synchronization of any site — fixing this properly (lock + owned snapshot, or an append-only/RCU structure) changes behavior and allocation patterns and belongs in a follow-up. The race is not introduced by this change; get_unsync preserves the exact prior semantics.
There was a problem hiding this comment.
@Jarred-Sumner Got it—thanks for the clarification. I’ll treat this as an acknowledged, pre-existing race and not a change request for this PR. The switch to get_unsync in bin_dirs() at Lines 4039–4043 preserves the prior semantics as intended.
If you’d like, I can open a follow-up GitHub issue to track a proper fix (e.g., lock+owned snapshot or an append-only/RCU snapshot) and link back to this PR and thread. Want me to file that now?
✏️ Learnings added
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 31178
File: src/resolver/resolver.rs:4039-4043
Timestamp: 2026-05-21T10:02:46.380Z
Learning: Repo oven-sh/bun: In src/resolver/resolver.rs, bin_dirs()’s read of BIN_FOLDERS via const_slice() without BIN_FOLDERS_LOCK is a known, pre-existing race. For PR `#31178` (RacyCell refactor), maintainers prefer not to change synchronization; get_unsync is used to preserve prior semantics, and any fix (snapshot/RCU or similar) should be deferred to a follow-up issue/PR.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: robobun
Repo: oven-sh/bun PR: 30522
File: src/runtime/cli/publish_command.rs:914-916
Timestamp: 2026-05-15T11:58:16.766Z
Learning: In this repo’s Rust code, do not flag a “typed accessor missing” or similar violation when reading `npm_config_*` environment variables. For `npm_config_*` keys (e.g. `NPM_CONFIG_REGISTRY`, `NPM_CONFIG_PROVENANCE`), the accepted pattern is to read them via `bun_core::getenv_z` using both upper-case and lower-case forms combined with `.or_else(...)`. This dual `getenv_z` approach is intentional and consistent even when `src/bun_core/env_var.rs` does not provide typed accessors for specific `npm_config_*` variables (e.g., in `PackageManagerOptions::load` and `publish_command.rs`).
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 31176
File: src/io/uv_handle.rs:92-100
Timestamp: 2026-05-21T09:34:31.660Z
Learning: In the oven-sh/bun repo, review allocator-mismatch/safety warnings carefully around raw `Box` round-trips. `bun_core::heap::{into_raw, take, destroy, alloc}` are `#[inline(always)]` thin aliases with identical machine code to `Box::into_raw`/`Box::from_raw` and do not provide additional allocator-mismatch protection or ASAN-risk reduction.
Therefore, when a value is wrapped in a *typed owner* that owns both halves of the raw round-trip (e.g., similar to patterns like `UvHandle<H,T>`, `WorkPool::schedule_owned`, etc.), it is the correct idiomatic form to use `Box::leak` and then `Box::from_raw` (open-coded) inside that wrapper. In this case, do not flag `Box::leak`/`Box::from_raw` usage as an allocator-mismatch risk and do not suggest replacing it with `heap::into_raw`/`take`.
| if !BIN_FOLDERS_LOADED.load(core::sync::atomic::Ordering::Acquire) { | ||
| // SAFETY: callers hold RESOLVER_MUTEX; first init. | ||
| unsafe { (*BIN_FOLDERS.get()).write(BinFolderArray::default()) }; | ||
| unsafe { (*BIN_FOLDERS.get_unsync()).write(BinFolderArray::default()) }; | ||
| BIN_FOLDERS_LOADED.store(true, core::sync::atomic::Ordering::Release); |
There was a problem hiding this comment.
Guard the first BIN_FOLDERS initialization with BIN_FOLDERS_LOCK or a Once.
The safety comment here assumes callers hold RESOLVER_MUTEX, but Line 3571 calls dir_info_uncached() from dir_info_for_resolution() without taking that mutex. Two threads can therefore reach this !BIN_FOLDERS_LOADED branch concurrently and both write(BinFolderArray::default()) before either stores true.
Also applies to: 6127-6133
🤖 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/resolver/resolver.rs` around lines 6081 - 6084, The BIN_FOLDERS
initialization is racy because callers (e.g., dir_info_uncached() invoked from
dir_info_for_resolution()) may not hold RESOLVER_MUTEX; replace the manual
check/write of BIN_FOLDERS when !BIN_FOLDERS_LOADED with a proper single‑time
guard: either use a dedicated BIN_FOLDERS_LOCK (acquire before checking/writing
BIN_FOLDERS and store BIN_FOLDERS_LOADED under the lock) or use a
std::sync::Once to run the one‑time initializer that writes
(*BIN_FOLDERS.get_unsync()).write(BinFolderArray::default()) and sets the loaded
flag; apply the same change to the other occurrence around lines 6127–6133 so
both sites perform atomic one‑time initialization of BIN_FOLDERS instead of the
current racy pattern.
There was a problem hiding this comment.
Same as the bin_dirs() thread: this is the pre-existing synchronization scheme (RESOLVER_MUTEX for first init per the existing SAFETY comments, BIN_FOLDERS_LOCK for appends, an Acquire/Release flag for readers), reported in the PR description as finding #2. This PR only converts the accessor from the implicitly-unsynchronized get() to the explicitly-unsafe get_unsync() without changing which locks are held where; restructuring the init into a Once is a behavioral follow-up.
There was a problem hiding this comment.
@Jarred-Sumner Got it — thanks for the clarification. I’ll treat this as pre-existing and out of scope for this PR; no change requested here.
If it helps, I can open a follow-up GitHub issue to track “harden BIN_FOLDERS first-init (Once or lock-guarded check/write), keep BIN_FOLDERS_LOCK for appends; readers via Acquire/Release,” and link it back to #31178 and this discussion. Want me to file that?
✏️ Learnings added
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 31178
File: src/resolver/resolver.rs:6081-6084
Timestamp: 2026-05-21T10:02:39.847Z
Learning: Repo oven-sh/bun: For BIN_FOLDERS in src/resolver/resolver.rs, the pre-existing synchronization scheme is: RESOLVER_MUTEX for the first initialization, BIN_FOLDERS_LOCK protecting subsequent appends, and BIN_FOLDERS_LOADED as an Acquire/Release flag for readers. When a PR only refactors access (e.g., RacyCell get() → get_unsync()), do not push a locking/Once restructure in the same PR; suggest it as a follow-up instead.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: robobun
Repo: oven-sh/bun PR: 30522
File: src/runtime/cli/publish_command.rs:914-916
Timestamp: 2026-05-15T11:58:16.766Z
Learning: In this repo’s Rust code, do not flag a “typed accessor missing” or similar violation when reading `npm_config_*` environment variables. For `npm_config_*` keys (e.g. `NPM_CONFIG_REGISTRY`, `NPM_CONFIG_PROVENANCE`), the accepted pattern is to read them via `bun_core::getenv_z` using both upper-case and lower-case forms combined with `.or_else(...)`. This dual `getenv_z` approach is intentional and consistent even when `src/bun_core/env_var.rs` does not provide typed accessors for specific `npm_config_*` variables (e.g., in `PackageManagerOptions::load` and `publish_command.rs`).
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 31176
File: src/io/uv_handle.rs:92-100
Timestamp: 2026-05-21T09:34:31.660Z
Learning: In the oven-sh/bun repo, review allocator-mismatch/safety warnings carefully around raw `Box` round-trips. `bun_core::heap::{into_raw, take, destroy, alloc}` are `#[inline(always)]` thin aliases with identical machine code to `Box::into_raw`/`Box::from_raw` and do not provide additional allocator-mismatch protection or ASAN-risk reduction.
Therefore, when a value is wrapped in a *typed owner* that owns both halves of the raw round-trip (e.g., similar to patterns like `UvHandle<H,T>`, `WorkPool::schedule_owned`, etc.), it is the correct idiomatic form to use `Box::leak` and then `Box::from_raw` (open-coded) inside that wrapper. In this case, do not flag `Box::leak`/`Box::from_raw` usage as an allocator-mismatch risk and do not suggest replacing it with `heap::into_raw`/`take`.
…ust stay unsynchronized prompt() is exposed on Worker global scopes, so the shared 4 KiB buffered stdin can be requested from more than one thread. Route buffered_stdin() through get_unsync to preserve the existing (already racy) behavior instead of panicking; the underlying shared-buffer race is listed as an audit finding.
Unreachable via check_owner (the fast path already returns when the owner matches), but kept so a direct claim from the owning thread is a no-op instead of a panic, matching ThreadCell::claim.
There was a problem hiding this comment.
I didn't find any bugs, but this redesigns a core unsafe impl Sync primitive and audits ~40 call sites across 19 files with per-site checked-vs-unsync safety judgments (plus an intentional UB→panic behavior change and two new FFI layout wrappers), so it warrants human eyes on the audit table and the *_unsync classifications.
Extended reasoning...
Overview
This PR replaces RacyCell<T>'s unconditional unsafe impl Sync with an owner-thread runtime latch: the first thread to call a checked accessor (get/read/write) claims the cell, and any later cross-thread access panics in release. New unsafe fn *_unsync accessors bypass the check for externally-synchronized state. Every RacyCell static in the tree (~40) is audited and classified as either thread-confined (checked) or cross-thread (unsync, with a documented happens-before edge). Two layout-sensitive FFI statics (ValueUndefined/ValueTrue in jsc/FFI.rs, Bun__FFI__offsets in ffi_body.rs) get dedicated #[repr(transparent)] wrappers because RacyCell is no longer repr(transparent). Touches 19 files across bundler, install, resolver, crash handler, FFI, test runner, CLI, and core output/util.
Security risks
No direct security surface (no auth, crypto, input parsing). The risk class is memory-safety/soundness: each *_unsync site asserts a happens-before edge in a SAFETY comment, and a misclassification preserves a pre-existing data race rather than introducing a new one. The two new unsafe impl Sync wrappers (ImmutableJSValue, ForeignOffsets) are narrowly scoped and well-justified (immutable POD / C++-written-once extern), but the FFI layout change deserves a second pair of eyes against the TinyCC/C++ consumers.
Level of scrutiny
High. This is a deliberate, well-documented hardening change, but: (1) it's an intentional behavior change — code that previously raced silently will now panic deterministically, which could surface in production paths the author didn't smoke-test; (2) the audit table embeds ~40 per-site judgments about thread affinity and synchronization edges, several of which (crash handler, Jest::RUNNER, BUFFERED_STDIN) the author explicitly flags as preserving known pre-existing races; (3) the new unit tests don't run in CI (bun_core test target doesn't link standalone) and Miri doesn't cover this crate; (4) the unsafe count goes up (1048→1080), which is the right direction for this change but means more surface to review.
Other factors
CodeRabbit raised four points; two were addressed (idempotent claim_slow arm, SELF_DEBUG_INFO rationale) and two (BIN_FOLDERS races) were correctly deferred as pre-existing and out of scope. The PR description is exceptionally thorough with a complete audit table. CI shows musl build-cpp failures on commit 00a1c6a — likely unrelated infra, but worth confirming the latest commit (1c25170) is green before merge.
What
unsafe impl<T: ?Sized> Sync for RacyCell<T>made any payload shareable across threads while the single-thread contract lived only in comments. AstaticRacyCell reachable from a worker thread was one (already-required)unsafederef away from a silent data race, and nothing checked the comment.RacyCell<T>is now{ owner: AtomicU64, value: UnsafeCell<T> }:get/read/write): the first thread to call one claims the cell; any later call from a different thread panics — in release builds too. Theunsafe impl Syncis now justified by this runtime check: safe code can never observe the payload from two threads.unsafe fn get_unsync/read_unsync/write_unsync): for state whose synchronization is external and can't be expressed in the type system (write-once-before-thread-spawn,Once-published, mutex-guarded, address-only). Each call site documents its happens-before edge under the existing// SAFETY:comment.Every
RacyCellstatic in the tree was audited and classified (table below). The two layout-sensitive FFI statics get dedicated#[repr(transparent)]wrapper types becauseRacyCellis no longerrepr(transparent).Named cost
Relaxedatomic load + compare per checked access (plus a one-time CAS on the first access). The unchecked accessors are zero-cost.AtomicU64) perRacyCellstatic.Audit
Kept on the checked accessors (thread-confined; the comment is now enforced)
TEMP_HOSTNAME,SOCKET_ASYNC_HTTP_ABORT_TRACKER,SHARED_REQUEST_HEADERS_BUF,SHARED_RESPONSE_HEADERS_BUF,SINGLE_PACKET_SMALL_BUFFERhttp/lib.rsCACHEhttp/h3_client/AltSvc.rsCUSTOM_SSL_CONTEXT_MAPhttp/HTTPThread.rsWTF8_ENV_BUF,ORIG_ENVIRONsys/windows/env.rsFAILURE_REASON_DATAinstall/windows-shim/bun_shim_impl.rsARENA(install_runner_arena)install/lib.rsCWD_BUF,ROOT_PACKAGE_JSON_PATH_BUF,ROOT_PACKAGE_JSON_PATHinstall/PackageManager.rsHANDLES_BEFOREresolver/fs.rsRUNNER(the.write()in test_command.rs /.read()in snapshot.rs)runtime/test_runner/jest.rsHOME_DIR_BUF,URL_,APP_NAME_BUF,GITHUB_REPOSITORY_URL_BUF,NPM_REGISTRY_URL_BUF,THREADruntime/cli/create_command.rsARENA(exec_arena)runtime/cli/exec_command.rsCLI_ARENA,CMD,LOG_,CONTEXT_DATAruntime/cli/mod.rsWORKER_FRAME,WORKER_CMDSruntime/cli/test/parallel/runner.rsPREV_INT,PREV_TERMruntime/cli/test/parallel/Coordinator.rsSHELL_BUF,RUN,DIRECT_LAUNCH_BUFFER,ENVIRONMENT_BUFFERruntime/cli/run_command.rsLIB_DIR_Zruntime/ffi/mod.rsCHILD_SINGLETONruntime/node/node_cluster_binding.rsINSTANCEio/ParentDeathWatchdog.rsThese need no call-site changes; the existing
unsafe { *X.get() }shapes are unchanged and the "X-thread-only" comment is now a checked invariant.Moved to the
unsafe*_unsyncaccessors (genuinely cross-thread; left unsafe, listed per rule)ARGVbun_core/util.rsOnce/ during startup, read from any thread viaargv()STDOUT_STREAM,STDERR_STREAMbun_core/output.rsconfigure_threadBUFFERED_STDINbun_core/output.rsprompt()is exposed onWorkerglobal scopes, so the shared stdin buffer is reachable from more than one thread (see findings)SCOPED_FILE_WRITERbun_core/output.rsEMPTY_MASKS_DATAcollections/bit_set.rsSELF_DEBUG_INFOcrash_handler/lib.rspanickinglatchSIGALTSTACKcrash_handler/lib.rsHARDLINK_QUEUEinstall/PackageInstall.rsSHARED_ENVinstall/repository.rsTHREAD_POOLbundler/ThreadPool.rsEMPTY_OBJECT/ARRAY/STRINGparsers/json.rsCALIBRATIONperf/hw_timer.rsOnce-published, read bynow_ns()on any threadINSTANCE(FileSystem)resolver/lib.rsMODULES,MAPresolver/node_fallbacks.rsOnce-published, read from resolver threadsBIN_FOLDERSresolver/resolver.rsDATA(stdin tty)io/source.rsBUN_PATH_BUFruntime/cli/create_command.rsTEMPDIR_FDruntime/cli/upgrade_command.rsRUNNER(the reads inJest::runner()/runner_ptr())runtime/test_runner/jest.rsReplaced with a dedicated wrapper type (layout-sensitive FFI)
ValueUndefined,ValueTruejsc/FFI.rs#[repr(transparent)] ImmutableJSValue— never mutated, no interior mutability at all;unsafe impl Syncover immutable PODBun__FFI__offsetsextern staticruntime/ffi/ffi_body.rs#[repr(transparent)] ForeignOffsets(UnsafeCell<Offsets>)— written once by C++ before any Rust read; every access is alreadyunsafeLeft as-is
install/windows-shim/main.rshas its own minimalRacyCellcopy for the standalone (freestanding, ntdll-only, single-threaded)bun_shim_impl.exebuild. There is no thread-id API available there and no second thread ever exists, so theSyncimpl is never exercised across threads. Itsread/writeare nowunsafe fnso every payload access requiresunsafe;get()already only returns a raw pointer.Multi-thread findings (pre-existing races surfaced by the audit)
Jest::RUNNERis reachable fromWorkerthreads.console_on_before_print(invoked by everyconsole.log) callsJest::runner(), which returns&'static mut TestRunnerderived from the main test thread's pointer. AWorkerthat logs duringbun testgets an aliasing&mutto the test runner state on a different thread. The owner check caught this immediately in a smoke test (workerconsole.logpanicked); the reads inrunner()/runner_ptr()are nowread_unsyncto preserve current behavior, but the underlying cross-thread&mut TestRunneris a real bug that needs a JS-thread-affinity guard or aNonefast-path for non-owner threads.BIN_FOLDERSreads race its writes.bin_dirs()readsconst_slice()after anAcquireflag load but without takingBIN_FOLDERS_LOCK, whileappend()mutates the array under the lock on another resolver thread. A reader can observe a torn length/contents pair.BUFFERED_STDINis reachable fromWorkerthreads.prompt()is exposed onWorkerglobal scopes and reads the single process-global 4 KiB buffered-stdin struct.prompt()on the main thread followed byprompt()in aWorkerhands two threads a&mutto the same buffer (concurrent calls would interleave reads and corrupt the buffer state). The owner check caught this in a smoke test;buffered_stdin()isget_unsyncto preserve the existing behavior.THREAD_POOLrefcount: the pre-existing PORT NOTE inacquire()already documents that a racing acquirer's reference is not counted (mirrored from Zig). Not introduced or fixed here.Before/after unsafe count
rg -o -e 'unsafe \{' -e 'unsafe fn' -e 'unsafe extern' -e 'unsafe impl' -e 'unsafe trait' <touched files> | wc -l: 1048 → 1080.The count goes up because the fix converts wrongly-safe surface into correctly-
unsafesurface and adds checked machinery: 3 newunsafe fnaccessors + their internal blocks, 2 new narrowly-scopedunsafe impl Syncwrappers for the FFI statics, the windows-shimread/writebecomingunsafe fn, ~7 call sites that previously called the safeget()outside anyunsafeblock and now callunsafe { get_unsync() }, and the new#[test]s. The deliverable is not a lower token count — it is that no safe call path can reach an unsynchronized payload.Tests / Miri
#[test]s inbun_core::util::racy_cell_testscover same-thread access, cross-thread panic, the unsync escape hatch, and first-accessor-claims semantics. They type-check (cargo check -p bun_core --all-targets) butbun_core's test target does not link standalone (it depends on FFI symbols supplied by higher-tier crates), andbun_coreis not in the Miri allowlist for the same reason — so, like the existingatomic_cell.rstests, they do not run in CI. Miri does not cover this crate.bun bdbuilds; smoke-testedbun run,Bun.serve+fetch,Worker+console.logfrom a worker,bun install,bun build, andbun test(with snapshots and an in-testWorker).bun bd test test/js/web/workers/worker.test.tsandtest/js/web/fetch/fetch.test.tsshow the same pass/fail set as the unmodified main build (the failures in both are pre-existing/network-dependent).bun run rust:check-all: 10/10 targets pass.