Repository navigation
Conversation
|
No actionable comments were generated in the recent review. WalkthroughThe semaphore now stores permits in ChangesSemaphore
Suggested reviewers: Merge Risk:
|
There was a problem hiding this comment.
LGTM — mechanical refactor replacing hand-rolled UnsafeCell + Mutex + unsafe impl Send/Sync with the crate's existing Guarded<usize>.
What was reviewed:
Guarded::newisconst fn(guarded.rs:51), soSemaphore::new()→RawRwLock::new()→RwLock::new()stayconstforstaticinitializers.Condition::wait_guardeddelegates towait(guard.mutex()), so the unlock/park/relock sequence is identical to the oldself.cond.wait(&self.mutex).- Lock/signal ordering unchanged:
cond.signal()in bothwait()andpost()runs before the guard drops, matching the old scopeguard placement. Send/Syncnow auto-derive viaGuardedBy'sunsafe impl Sync(bounded onValue: Send, satisfied byusize) andCondition: Sync— no loss of thread-safety marker.
Extended reasoning...
Overview
Single-file change to src/threading/Semaphore.rs (~30 lines net removed). The permit counter moves from a bare UnsafeCell<usize> sitting next to a Mutex into Guarded<usize> (the crate's existing parking_lot::Mutex<T>-style wrapper). wait() and post() now hold a GuardedLock and deref it instead of raw-pointer-dereffing the UnsafeCell under a manually locked mutex with scopeguard::defer! unlock. The two unsafe impl Send/Sync and four unsafe blocks are deleted; the file is now unsafe-free.
Security risks
None. This is an internal threading primitive with no user-facing surface, no parsing of untrusted input, and no I/O. The change tightens type-level invariants (mutex-protects-value is now enforced by the type rather than by SAFETY comments) without altering the locking protocol.
Level of scrutiny
Low-to-medium. It touches a concurrency primitive, so I verified the behavioral equivalence carefully: (1) Guarded::new is const so the const fn new() chain used by RawRwLock and static RwLocks is preserved; (2) wait_guarded(&mut guard) calls self.wait(guard.mutex()), i.e., the exact same Condition::wait(&Mutex) path as before; (3) the guard's Drop unlocks at end-of-scope, so cond.signal() still runs while the lock is held in both methods — identical wake ordering to the scopeguard version; (4) Send/Sync are recovered via GuardedBy's blanket unsafe impl<Value: Send, M: RawMutex + Sync> Sync (guarded.rs:28) plus auto-Send, with usize: Send and Mutex: Sync both holding. No semantic drift.
Other factors
The two Rust callers (RwLock.rs, fs_events.rs) only use new/default/wait/post, all of which keep identical signatures. The PR description explicitly scopes out Channel with a stated reason (padding growth + dead flag), which is the right call for a focused change. This is exactly the kind of "use the in-tree helper instead of hand-rolling the primitive" cleanup the review guidelines encourage. No outstanding reviewer comments; bug-hunting system found nothing.
|
This PR now holds the change rebased on Compared with the first version, it has two additions:
The description describes the PR as it now stands, with how it was checked and what it costs. |
The count was an UnsafeCell<usize> beside a bare Mutex, with an unsafe impl Sync for the whole struct and an unsafe block at each of the four reads and writes. Guarded<usize> carries the same mutex and hands out the count only while it is locked, so the impl and the blocks are gone. Condition::wait_guarded waits on the guard's mutex. Adds three unit tests for wait() and post(), two of them across threads. A lost wake-up fails them by a deadline, not by a hang. Removes the unsafe_send_covers_more_than_pointers entry for src/threading/Semaphore.rs from mordant-baseline.toml.
Semaphore's permit count in a Guarded<usize>
8830bd2 to
d9a462c
Compare
|
@dylan-conway The non-test part of this PR is the
There are two ways to go:
|
There was a problem hiding this comment.
I reviewed the current revision and found no bugs; since this is a core sync primitive used by RwLock writers and the description itself notes an overlapping queued commit and #45818 touching the same baseline section, a maintainer still needs to decide whether and in what order this lands.
What was reviewed:
wait()/post()against the old path:Guarded::lockisMutex::lock,wait_guardedisCondition::waiton the same mutex, and the guard drops (unlocks) aftersignal()as the olddefer!did, so lock/wait/decrement/signal/unlock ordering is unchanged.Sync/SendforSemaphoreandRawRwLocknow come fromGuardedBy's impl (usize: Send,Mutex: Sync), andSemaphore::new()staysconst, so thestaticuses still compile.- The
mordant-baseline.tomlline removal is justified: theunsafe implit tracked no longer exists in the file. - New tests: at most one waiter can return per
post(the woken waiter only re-signals when permits remain), so thetry_recv().is_err()checks are not racy; theSTARTEDspin only waits on spawned threads reaching their first line.
Extended reasoning...
The change rewrites src/threading/Semaphore.rs to store the permit count in the in-tree Guarded instead of UnsafeCell plus a separate Mutex, removing the unsafe impl Sync and four unsafe blocks, and deletes the corresponding mordant-baseline.toml lint entry; it adds three std-thread unit tests. It touches no security-sensitive surface. Reading Guarded and Condition::wait_guarded confirms the lock, wait, decrement, signal, and unlock sequence is identical to the old code. It was not approved outright because Semaphore underpins RwLock writer waits, fs_events, and the sema reader limit, and the author's own description flags an unmerged overlapping commit in a maintainer's queue plus #45818 editing the same baseline section, which is a sequencing decision for a human.
|
The review found no bug and asked for no change. Nothing has moved since the push: |
|
CI on d9a462c is red on lanes this diff does not reach. |
Unsafe-removal and lint-ratchet maintenance, no reachable defect. Merged #46363 has this shape. #37508, #37596 and #39165 were closed as refactors. A maintainer decides whether this is wanted.
Behaviour change: none
Problem
Semaphorekeeps its permit count in anUnsafeCell<usize>beside aMutex. Oneunsafe impl Syncand fourunsafeblocks assert that the mutex protects it.unsafeblocks from the syscall wrappers and the shell #47338 removed theunsafe impl Send. The compiler cannot deriveSyncwhile the count is beside the mutex. The advisory mordant lint reports that impl:unsafe_send_covers_more_than_pointers:src/threading/Semaphore.rs.Fix
permitsis aGuarded<usize>.wait()andpost()hold its guard, andwait()blocks inCondition::wait_guarded. The file has nounsafeleft. Its line leavesmordant-baseline.toml.Guarded::lock()isMutex::lock(), its guard unlocks on drop, andwait_guardedisCondition::waiton that mutex.cargo test -p bun_threading(10 pass, 3 new), Miri,bun scripts/rust-mordant.ts(exit 0), workspace clippy and check.Background
Guarded<T>(src/threading/guarded.rs) is a mutex that owns its value.src/sema/program.rshas this shape:asleep: Guarded<usize>.AtomicUsizecount also removes theunsafe, but the type then hides that the mutex guards it.Downsides
wait()runs 26 instructions, up from 22, andpost()still runs 23. The linked release binary is not measured.maintoo oncepermits()reads the old fields, so they prove no fix.Notes
Every number is from this commit against
mainat 01ec085, on linux-x64. macOS and Windows code was compiled, not run.The same change in a maintainer's queue
SemaphoreandChannelkeep their state in aGuarded". It is in no PR. It makes thisSemaphorechange, convertsChannel, and deletes the whole[bun_threading]section ofmordant-baseline.toml.mod tests,src/threading/Semaphore.rshere is byte-identical to that commit's file. That file merges without a conflict in either order.mordant-baseline.tomlconflicts in one hunk in either order.Left alone
src/threading/channel.rs(Bound the remote image prefetch ofbun file.mdby a deadline and a byte budget #45818),src/resolver/fs.rs(fix(resolver): synchronize mutable entry-cache snapshots #44539),src/runtime/node/fs_events.rsandsrc/runtime/node/path_watcher.rs(fs.watch/fs.watchFile: remove unsafe from the watcher modules #40200).bun file.mdby a deadline and a byte budget #45818 deletes the adjacent[bun_threading]line, and the queued commit deletes the section. The change that lands last must leave neither line nor the empty[bun_threading]header.Users of
SemaphoreRwLockwriters that wait for readers (src/threading/RwLock.rs), the type checker's file reader limit on macOS (src/sema/driver/host.rs), and FSEvents on macOS (src/runtime/node/fs_events.rs).newordefault,waitandpost.Cost
Instructions in the crate's release object,
mainto this PR (cargo rustc --release -p bun_threading --lib --target T -- --emit asm -C codegen-units=1):postwaitscripts/build/rust.tswithout the link-time LTO flags. Linux:-Crelocation-model=static -Cforce-frame-pointers=yes -Cllvm-args=-addrsig -Zshare-generics=y -Ctarget-cpu=nehalem -Zlocation-detail=none -Cforce-unwind-tables=no. macOS and Windows: the same without the first and the last flag, and macOS with-Ctarget-cpu=apple-m1.waiton Linux x64 with Bun's flags has 6 before and 8 after, because the compiler emits the count test three times where it emitted it once.stepifrom entry to return). With Bun's flags:post23 and 23,wait22 and 26. With no flags:post22 and 22,wait21 and 25. Executed calls, conditional branches and atomic instructions are equal.waitarepush %r15,push %rax,add $8, %rspandpop %r15.waitnow keeps three addresses across the call toCondition::wait, becausecondis not at offset 0.-Zprint-type-sizes, Linux x64, macOS arm64, Windows x64):Semaphoreis 24 bytes in release and 32 in dev builds, before and after.RawRwLock, which embeds it, is 40 and 56, before and after.mordant
bun scripts/rust-mordant.ts(564c8da, Linux, Windows and macOS targets) exits 0 with the line removed. The baseline goes from 465 accepted findings to 464, and from 14 to 13 for this lint.mainwith only the line removed exits 101: "unsafe impl Sync for Semaphorecovers fields that are notSyncfor a reason other than a raw pointer" atsrc/threading/Semaphore.rs:21. The lint's help names two remedies: move the cell into the mutex, or use an atomic.Tests
Semaphore::wait(). One is single-threaded and two use threads. A lost wake-up fails them at a 60 second deadline, not by a hang.main'sSemaphore, becausepermits()reads the new field. With that helper changed to read the old fields they pass 300 of 300 runs.cargo clippy -p bun_threading --all-targetsis clean. CI does not lint test targets.post()withoutsignal(): fails 3 of 3 runs.post()adds two permits: fails 10 of 10.wait()keeps the permit: fails 10 of 10.whiletoifinwait(): fails 9 of 10, and 4 of 10 in an earlier measurement, because it depends on scheduling.wait()without its secondsignal()passes 50 of 50 runs. The tests run that line but do not detect its removal. This PR keeps thatsignal().End to end
bun checkon a 41-file project entersSemaphore::wait0 times under gdb. On Linux onlyRwLockwriters that wait for readers reach the semaphore.src/sema/driver/host.rs:526), the same run enterswait41 times andpost47 times and reports the expected type error.test/cli/check/entry-points.test.tsandtest/cli/check/named-files.test.tspass with that edit: 168 pass, 0 fail.Self-review
Done in part:
whiletoifinwait().Channel, are not part of this change.History
wait()andpost()and declaredpermitsbeforecond. It did not touchmordant-baseline.toml.mainbecause Remove 118unsafeblocks from the syscall wrappers and the shell #47338 deleted theunsafe impl Sendlines and the first version deleted that whole block.no test proof · iteration 0 · the description declares no behaviour change, so there is no failing test to prove; the existing suite in CI is the check