CVE-2026-54906: Concurrent Ruby ReadWriteLock Allows Wrong-thread Write Release Vulnerability

LOW
Advisory Identifier: GHSA-6wx8-w4f5-wwcr
CWE-414 CWE-667
Published
Advisory Summary

CVE-2026-54906 is a LOW readwritelock allows wrong-thread write release vulnerability (CWE-414: Missing Lock Check) affecting Concurrent Ruby libraries in the RubyGems ecosystem. The issue involves readwritelock allows wrong-thread write release and stray read-release counter corruption. Remediation is available by upgrading affected packages to version 1.3.7.

CVE-2026-54906 at a Glance

CVE Identifier CVE-2026-54906
GHSA Identifier GHSA-6wx8-w4f5-wwcr
Severity LOW
CVSS v3.1 Base Score N/A
CWE Classification CWE-414: Missing Lock Check
Affected Product Concurrent Ruby
Package Ecosystem RubyGems
Published Date
Last Updated

How to Fix CVE-2026-54906

To remediate this vulnerability, upgrade all affected Concurrent Ruby dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

concurrent-ruby fixed: 1.3.7
Upgrade concurrent-ruby to 1.3.7
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for concurrent-ruby

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-54906, inspect your project dependencies for Concurrent Ruby:

inspect dependencies for concurrent-ruby

Analyze the terminal output to locate any version of Concurrent Ruby in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.3.7), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-414: Missing Lock Check, where a product does not check to see if a lock is present before performing sensitive operations on a resource. In Concurrent Ruby, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### 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>))

CVE-2026-54906 Affected Concurrent Ruby Versions

1 component(s)
Fixed Versions:
1.3.7
Product / Package Ecosystem Affected Versions Fixed Version
concurrent-ruby
RubyGems
< 1.3.7
1.3.7

References

Frequently Asked Questions

What is CVE-2026-54906?
CVE-2026-54906 is a security vulnerability in Concurrent Ruby involving ReadWriteLock allows wrong-thread write release and stray read-release counter corruption under CWE-414: Missing Lock Check.
Which Concurrent Ruby versions are affected?
Affected versions include releases prior to 1.3.7 across affected packages: concurrent-ruby.
What version fixes CVE-2026-54906?
Upgrading to 1.3.7 (or the latest patched release for your respective branch) resolves the vulnerability.
Is concurrent-ruby affected?
Yes, concurrent-ruby is affected in releases prior to 1.3.7. Upgrade your dependency to 1.3.7 or later.
What CWE is associated with CVE-2026-54906?
CVE-2026-54906 is classified under CWE-414: Missing Lock Check (A product does not check to see if a lock is present before performing sensitive operations on a resource.).
How can I check RubyGems dependencies for CVE-2026-54906?
Run the dependency tree command: 'inspect dependencies for concurrent-ruby'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-6wx8-w4f5-wwcr",
  "modified" : "2026-06-19T20:47:41Z",
  "published" : "2026-06-19T20:47:41Z",
  "aliases" : [ "CVE-2026-54906" ],
  "summary" : "Concurrent Ruby: ReadWriteLock allows wrong-thread write release and stray read-release counter corruption",
  "details" : "### Summary\n`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.\n\n`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`.\n\nThis 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.\n\n###  Version\nSoftware: concurrent-ruby\nVersion: 1.3.6\nCommit: 7a1b78941c081106c20a9ca0144ac73a48d254ab\n\n### Details\n\n`release_write_lock` checks only whether the global counter indicates that a writer is running. It does not track or verify ownership:\n\n```ruby\ndef release_write_lock\n  return true unless running_writer?\n  c = @Counter.update { |counter| counter - RUNNING_WRITER }\n  @ReadLock.broadcast\n  @WriteLock.signal if waiting_writers(c) > 0\n  true\nend\n```\n\nBecause 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.\n\n`release_read_lock` unconditionally decrements the shared counter:\n\n```ruby\ndef release_read_lock\n  while true\n    c = @Counter.value\n    if @Counter.compare_and_set(c, c-1)\n      if waiting_writer?(c) && running_readers(c) == 1\n        @WriteLock.signal\n      end\n      break\n    end\n  end\n  true\nend\n```\n\nOn 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.\n\n# Reproduce\n\nFrom the root of a `concurrent-ruby` checkout, run:\n\n```bash\nruby -Ilib/concurrent-ruby - <<'RUBY'\nrequire 'concurrent/atomic/read_write_lock'\nrequire 'concurrent/version'\nrequire 'thread'\n\nputs \"ruby=#{RUBY_DESCRIPTION}\"\nputs \"concurrent_ruby_version=#{Concurrent::VERSION}\"\nputs \"poc=ReadWriteLock release methods corrupt or bypass lock state\"\n\nlock = Concurrent::ReadWriteLock.new\nevents = Queue.new\nwriter1_inside = false\n\nwriter1 = Thread.new do\n  lock.acquire_write_lock\n  writer1_inside = true\n  events << :writer1_acquired\n  sleep 0.5\n  writer1_inside = false\n  lock.release_write_lock\n  events << :writer1_finished\nend\n\nevents.pop\nputs 'writer1_acquired=true'\n\nintruder_result = nil\nintruder = Thread.new do\n  intruder_result = lock.release_write_lock\nend\nintruder.join\n\nputs \"wrong_thread_release_write_lock_returned=#{intruder_result}\"\n\nwriter2_entered_while_writer1_inside = nil\nwriter2 = Thread.new do\n  lock.acquire_write_lock\n  writer2_entered_while_writer1_inside = writer1_inside\n  lock.release_write_lock\nend\n\nwriter2.join(0.25)\n\nputs \"writer2_acquired_while_writer1_inside=#{writer2_entered_while_writer1_inside}\"\n\nwriter1.join\n\nlock2 = Concurrent::ReadWriteLock.new\nstray_read_release_result = lock2.release_read_lock\ncounter_after_stray_read_release = lock2.instance_eval { @Counter.value }\nread_after_stray_release = begin\n  lock2.acquire_read_lock\n  'acquired'\nrescue => error\n  \"#{error.class}: #{error.message}\"\nend\n\nputs \"stray_release_read_lock_returned=#{stray_read_release_result}\"\nputs \"counter_after_stray_read_release=#{counter_after_stray_read_release}\"\nputs \"acquire_read_after_stray_release=#{read_after_stray_release}\"\n\nif intruder_result && writer2_entered_while_writer1_inside && counter_after_stray_read_release == -1\n  puts 'result=REPRODUCED wrong-thread write release and stray read-release corruption'\nelse\n  puts 'result=NOT_REPRODUCED'\nend\n```\nExpected result:\n\n- A second thread successfully calls `release_write_lock` while the first writer still holds the lock.\n- A second writer enters while the first writer is still inside the write critical section.\n- Calling `release_read_lock` on a fresh lock changes the counter to `-1`.\n- A subsequent read acquisition fails with `Concurrent::ResourceLimitError`.\n\n### Log evidence\n\nLocal reproduction output:\n\n```text\nruby=ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin25]\nconcurrent_ruby_version=1.3.6\npoc=ReadWriteLock release methods corrupt or bypass lock state\nwriter1_acquired=true\nwrong_thread_release_write_lock_returned=true\nwriter2_acquired_while_writer1_inside=true\nstray_release_read_lock_returned=true\ncounter_after_stray_read_release=-1\nacquire_read_after_stray_release=Concurrent::ResourceLimitError: Too many reader threads\nresult=REPRODUCED wrong-thread write release and stray read-release corruption\n```\n\n### Impact\nThis can break the write-lock mutual exclusion guarantee and can also leave a lock unusable after a stray read release.\nThe 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.\n\n### Credit\nPranjali Thakur - depthfirst ([depthfirst.com](<http://depthfirst.com>))",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "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"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "RubyGems",
      "name" : "concurrent-ruby"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.3.7"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/ruby-concurrency/concurrent-ruby/security/advisories/GHSA-6wx8-w4f5-wwcr"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/ruby-concurrency/concurrent-ruby"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-414", "CWE-667" ],
    "severity" : "LOW",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-06-19T20:47:41Z",
    "nvd_published_at" : null
  }
}