Loading...
Skip to main content

CVE-2026-54906 — Vulnerability in concurrent-ruby — CVE Details

LOW

Concurrent Ruby: ReadWriteLock allows wrong-thread write release and stray read-release counter corruption

Published June 19, 2026Updated September 10, 2026Source: osv

Summary

### 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: ```ruby 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: ```ruby 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: ```bash 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: ```text 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>))

Advisory Timeline

  • First published June 19, 2026 (115 days ago).
  • Last revised September 10, 2026 — 82 days after initial publication.

Remediation

Upgrade to the fixed version using your package manager.

Bundler
Update concurrent-ruby to 1.3.7 or later
gem install concurrent-ruby -v 1.3.7

After upgrading, run your dependency scanner again to confirm the vulnerability is resolved.

Affected Packages (1)

Affects 1 ecosystem: rubygems (1).

PackageEcosystemAffectedFixed In
concurrent-ruby
rubygems
0.0.1, 0.1.0, 0.1.1, 0.1.1.pre.1 (+73 more)1.3.7

Vulnerability Classification

Common Weakness Enumeration (CWE) identifiers for this vulnerability type.

CVSS Score Breakdown

What the CVSS (Common Vulnerability Scoring System) 4.0 score means for each attack dimension.

Attack Vector
Local
Attack Complexity
High
Privileges Required
None
User Interaction
None
Scope
—
Confidentiality
—
Integrity
—
Availability
—

CVSS:4.0/AV:L/AC:H/AT:N/PR:N/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N

Frequently Asked Questions

What is CVE-2026-54906?
Concurrent Ruby: ReadWriteLock allows wrong-thread write release and stray read-release counter corruption This vulnerability has been assigned a severity rating of LOW (CVSS score: 4.0/10).
How do I check if my project is affected by CVE-2026-54906?
CVE-2026-54906 affects concurrent-ruby. Use GeekWala's free vulnerability scanner to check your dependencies against CVE-2026-54906 and 200,000+ other known vulnerabilities.

Severity & Exploitability

CVSS Score
4.0

Exploitation requires specific conditions or has limited impact. Remediate within weeks.

Also Known As

GHSA-6wx8-w4f5-wwcr

Related CVEs

  • CVE-2026-54904
    HIGH

    Concurrent Ruby : `AtomicReference#update` livelocks when the stored value is `Float::NAN`

  • CVE-2026-54619
    LOW

    sqlite3-ruby has Use-After-Free When Redefining SQLite Functions with Different Arity

  • CVE-2026-54620
    LOW

    sqlite3-ruby has Use-After-Free in SQLite Aggregate Function Callbacks

  • CVE-2026-54905
    LOW

    Concurrent Ruby: `ReentrantReadWriteLock` read-count overflow grants a write lock without exclusivity

Check if you're affected

Scan your dependencies to see if this vulnerability affects your projects.

Scan Your Dependencies