CVE-2026-54905
concurrent-ruby: `ReentrantReadWriteLock` read-count overflow grants a write lock without exclusivity
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.
Attack profile
How easy it is to exploit
- Attack vector
- Local
- Complexity
- Low
- Privileges needed
- Low
- User interaction
- None
What an attacker can affect
- Confidentiality
- Low
- Integrity
- Low
- Availability
- Low
Exploit likelihood
What each source reports
CVE List Score used
2.0
concurrent-ruby: `ReentrantReadWriteLock` read-count overflow grants a write lock without exclusivity
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.
- Record
CVE-2026-54905- CVSS 4.0
- 2.0 Low
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 - Weakness
- CWE-128
- Updated
- 2026-11-02
EUVDEUVD-2026-39000
2.0
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.
- Record
EUVD-2026-39000- CVSS 4.0
- 2.0 Low
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 - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-wv3x-4vxv-whpp
2.0
Concurrent Ruby: `ReentrantReadWriteLock` read-count overflow grants a write lock without exclusivity
Summary
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.
Version
Software: concurrent-ruby Version: 1.3.6 Commit: 7a1b78941c081106c20a9ca0144ac73a48d254ab
Details
The implementation uses a shared counter to track global readers/writers and a per-thread local counter to support reentrancy:
READER_BITS = 15
WRITER_BITS = 14
WAITING_WRITER = 1 << READER_BITS
RUNNING_WRITER = 1 << (READER_BITS + WRITER_BITS)
MAX_READERS = WAITING_WRITER - 1
MAX_WRITERS = RUNNING_WRITER - MAX_READERS - 1
WRITE_LOCK_HELD = 1 << READER_BITS
READ_LOCK_MASK = WRITE_LOCK_HELD - 1
WRITE_LOCK_MASK = MAX_WRITERS
When a thread already holds a lock, acquire_read_lock increments @HeldCount:
if (held = @HeldCount.value) > 0
if held & READ_LOCK_MASK == 0
@Counter.update { |c| c + 1 }
end
@HeldCount.value = held + 1
return true
end
After 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:
def try_write_lock
if (held = @HeldCount.value) >= WRITE_LOCK_HELD
@HeldCount.value = held + WRITE_LOCK_HELD
return true
else
# normal global writer acquisition path
end
end
This 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.
PoC
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'concurrent/atomic/reentrant_read_write_lock'
require 'concurrent/version'
require 'thread'
def wait_for_queue(queue, timeout_seconds)
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout_seconds
loop do
return queue.pop(true)
rescue ThreadError
return nil if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
sleep 0.001
end
end
puts "ruby=#{RUBY_DESCRIPTION}"
puts "concurrent_ruby_version=#{Concurrent::VERSION}"
puts "poc=ReentrantReadWriteLock read-depth overflow grants write lock without exclusivity"
lock = Concurrent::ReentrantReadWriteLock.new
other_reader_ready = Queue.new
other_reader_stop = Queue.new
other_reader = Thread.new do
lock.acquire_read_lock
other_reader_ready << :held
other_reader_stop.pop
end
wait_for_queue(other_reader_ready, 1)
puts "other_thread_holds_read_lock=true"
depth = Concurrent::ReentrantReadWriteLock::WRITE_LOCK_HELD
depth.times { lock.acquire_read_lock }
held_count = lock.instance_eval { @HeldCount.value }
counter_before = lock.instance_eval { @Counter.value }
puts "main_thread_read_acquisitions=#{depth}"
puts "main_thread_held_count=#{held_count}"
puts "counter_before_try_write=#{counter_before}"
puts "running_writer_bit_before=#{(counter_before & Concurrent::ReentrantReadWriteLock::RUNNING_WRITER) != 0}"
write_granted = lock.try_write_lock
counter_after = lock.instance_eval { @Counter.value }
puts "try_write_lock_returned=#{write_granted}"
puts "counter_after_try_write=#{counter_after}"
puts "running_writer_bit_after=#{(counter_after & Concurrent::ReentrantReadWriteLock::RUNNING_WRITER) != 0}"
third_reader_ready = Queue.new
third_reader = Thread.new do
lock.acquire_read_lock
third_reader_ready << :acquired
end
third_reader_acquired =…
- Record
GHSA-wv3x-4vxv-whpp- CVSS 4.0
- 2.0 Low
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 - CVSS 3.1
- 5.5 Medium
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-128
- Updated
- 2026-11-02
NVD
2.0
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.
- Record
CVE-2026-54905- CVSS 4.0
- 2.0 Low
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 - CVSS 3.1
- 5.5 Medium
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-128
- Updated
- 2026-11-02
OSV
2.0
concurrent-ruby: `ReentrantReadWriteLock` read-count overflow grants a write lock without exclusivity
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.
- Record
CVE-2026-54905- CVSS 4.0
- 2.0 Low
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 - Weakness
- CWE-128
- Updated
- 2026-11-02
OSVDEBIAN-CVE-2026-54905
5.5
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.
- Record
DEBIAN-CVE-2026-54905- CVSS 3.1
- 5.5 Medium
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N - Updated
- 2026-11-02
OSVGHSA-wv3x-4vxv-whpp
2.0
Concurrent Ruby: `ReentrantReadWriteLock` read-count overflow grants a write lock without exclusivity
Summary
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.
Version
Software: concurrent-ruby Version: 1.3.6 Commit: 7a1b78941c081106c20a9ca0144ac73a48d254ab
Details
The implementation uses a shared counter to track global readers/writers and a per-thread local counter to support reentrancy:
READER_BITS = 15
WRITER_BITS = 14
WAITING_WRITER = 1 << READER_BITS
RUNNING_WRITER = 1 << (READER_BITS + WRITER_BITS)
MAX_READERS = WAITING_WRITER - 1
MAX_WRITERS = RUNNING_WRITER - MAX_READERS - 1
WRITE_LOCK_HELD = 1 << READER_BITS
READ_LOCK_MASK = WRITE_LOCK_HELD - 1
WRITE_LOCK_MASK = MAX_WRITERS
When a thread already holds a lock, acquire_read_lock increments @HeldCount:
if (held = @HeldCount.value) > 0
if held & READ_LOCK_MASK == 0
@Counter.update { |c| c + 1 }
end
@HeldCount.value = held + 1
return true
end
After 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:
def try_write_lock
if (held = @HeldCount.value) >= WRITE_LOCK_HELD
@HeldCount.value = held + WRITE_LOCK_HELD
return true
else
# normal global writer acquisition path
end
end
This 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.
PoC
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'concurrent/atomic/reentrant_read_write_lock'
require 'concurrent/version'
require 'thread'
def wait_for_queue(queue, timeout_seconds)
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout_seconds
loop do
return queue.pop(true)
rescue ThreadError
return nil if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
sleep 0.001
end
end
puts "ruby=#{RUBY_DESCRIPTION}"
puts "concurrent_ruby_version=#{Concurrent::VERSION}"
puts "poc=ReentrantReadWriteLock read-depth overflow grants write lock without exclusivity"
lock = Concurrent::ReentrantReadWriteLock.new
other_reader_ready = Queue.new
other_reader_stop = Queue.new
other_reader = Thread.new do
lock.acquire_read_lock
other_reader_ready << :held
other_reader_stop.pop
end
wait_for_queue(other_reader_ready, 1)
puts "other_thread_holds_read_lock=true"
depth = Concurrent::ReentrantReadWriteLock::WRITE_LOCK_HELD
depth.times { lock.acquire_read_lock }
held_count = lock.instance_eval { @HeldCount.value }
counter_before = lock.instance_eval { @Counter.value }
puts "main_thread_read_acquisitions=#{depth}"
puts "main_thread_held_count=#{held_count}"
puts "counter_before_try_write=#{counter_before}"
puts "running_writer_bit_before=#{(counter_before & Concurrent::ReentrantReadWriteLock::RUNNING_WRITER) != 0}"
write_granted = lock.try_write_lock
counter_after = lock.instance_eval { @Counter.value }
puts "try_write_lock_returned=#{write_granted}"
puts "counter_after_try_write=#{counter_after}"
puts "running_writer_bit_after=#{(counter_after & Concurrent::ReentrantReadWriteLock::RUNNING_WRITER) != 0}"
third_reader_ready = Queue.new
third_reader = Thread.new do
lock.acquire_read_lock
third_reader_ready << :acquired
end
third_reader_acquired =…
- Record
GHSA-wv3x-4vxv-whpp- CVSS 4.0
- 2.0 Low
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 - CVSS 3.1
- 5.5 Medium
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-128
- Updated
- 2026-11-02
OSVUBUNTU-CVE-2026-54905
2.0
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.
- Record
UBUNTU-CVE-2026-54905- CVSS 4.0
- 2.0 Low
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 - CVSS 3.1
- 5.5 Medium
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N - Updated
- 2026-11-02
OSVAZL-91290 No score
CVE-2026-54905 affecting package rubygem-concurrent-ruby for versions less than 1.3.7-1
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.
- Record
AZL-91290- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
concurrent-rubyRubyGems |
before 1.3.7 | 1.3.7 |
rubygem-concurrent-rubyAzure Linux:3 |
before 1.3.7-1 | 1.3.7-1 |
ruby-concurrentDebian:12 |
All versions, plus listed versions 1.1.6+dfsg-51.2.3-11.2.3-21.2.3-31.2.3-4 Show 9 moreShow less1.3.4-11.3.4-1~exp11.3.5-11.3.6-11.3.7-11.3.7-21.3.7-31.3.7-41.3.8-1 |
None known |
ruby-concurrentDebian:13 |
All versions, plus listed versions 1.3.5-11.3.6-11.3.7-11.3.7-21.3.7-31.3.7-41.3.8-1 |
None known |
ruby-concurrentDebian:14 |
before 1.3.7-1, plus listed versions 1.3.5-11.3.6-1 |
1.3.7-1 |
concurrent-rubyRubyGems |
before 1.3.7, plus listed versions 0.0.10.1.00.1.10.1.1.pre.10.1.1.pre.2 Show 45 moreShow less0.1.1.pre.30.1.1.pre.40.1.1.pre.50.2.00.2.10.2.20.3.00.3.0.pre.10.3.0.pre.20.3.0.pre.30.3.10.3.1.pre.10.3.1.pre.20.3.20.4.00.4.10.5.00.5.0.pre.10.6.00.6.0.pre.10.6.0.pre.20.6.10.7.00.7.0.rc00.7.0.rc10.7.0.rc20.7.10.7.20.8.00.8.0.pre10.8.0.pre20.9.00.9.0.pre20.9.0.pre30.9.10.9.21.0.01.0.0.pre11.0.0.pre21.0.0.pre31.0.0.pre41.0.0.pre51.0.11.0.21.0.3 |
1.3.7 |
ruby-concurrentUbuntu:16.04:LTS |
All versions, plus listed versions 1.0.0-1 |
None known |
ruby-concurrentUbuntu:18.04:LTS |
All versions, plus listed versions 1.0.5-1 |
None known |
ruby-concurrentUbuntu:20.04:LTS |
All versions, plus listed versions 1.0.5-31.0.5-41.1.6+dfsg-21.1.6+dfsg-3 |
None known |
ruby-concurrentUbuntu:22.04:LTS |
All versions, plus listed versions 1.1.6+dfsg-31.1.6+dfsg-3build11.1.6+dfsg-4 |
None known |
Show 4 moreShow less
| Package | Affected | Fixed |
|---|---|---|
ruby-concurrentUbuntu:24.04:LTS |
All versions, plus listed versions 1.1.6+dfsg-5build21.1.6+dfsg-5build31.2.3-2build1 |
None known |
ruby-concurrentUbuntu:25.10 |
All versions, plus listed versions 1.3.4-1 |
None known |
ruby-concurrentUbuntu:26.10 |
All versions, plus listed versions 1.3.6-11.3.7-41.3.8-1 |
None known |
ruby-concurrentUbuntu:26.04:LTS |
All versions, plus listed versions 1.3.4-11.3.6-1 |
None known |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-54905 is a low vulnerability (CVSS 2.0) in Ruby-concurrency Concurrent-ruby, published June 19, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.15%, the 4th percentile. Fixed in concurrent-ruby 1.3.7.
References
github.com/ruby-concurrency/concurrent-ruby/security/advisories/GHSA-wv3x-4vxv-whpp
nvd.nist.gov/vuln/detail/CVE-2026-54905
github.com/rubysec/ruby-advisory-db/blob/master/gems/concurrent-ruby/CVE-2026-54905.yml
cve.org/CVERecord/SearchResults?query=CVE-2026-54905
github.com/advisories/GHSA-wv3x-4vxv-whpp
github.com/CVEProject/cvelistV5/tree/main/cves/2026/54xxx/CVE-2026-54905.json
Left out here: 27 versions and the rest of 2 source descriptions. The JSON record has them.