Monitor vulnerabilities like this one. Sign up free to get alerted when software you use is affected.
2.1

CVE-2026-54906: Concurrent Ruby: Wrong Thread Can Release Lock and Cause Errors

GHSA-6wx8-w4f5-wwcr CVE-2026-54906 GHSA-6wx8-w4f5-wwcr
Summary

A bug in Concurrent Ruby's ReadWriteLock can allow one thread to release a lock held by another thread, potentially causing errors. This can happen when a thread tries to release a lock without owning it. To fix this, make sure to update to the latest version of Concurrent Ruby and follow best practices for thread-safe programming.

What to do
  • Update jerry d'antonio concurrent-ruby to version 1.3.7.
  • Update concurrent-ruby to version 1.3.7.
Affected software
Ecosystem VendorProductAffected versions
rubygems jerry d'antonio concurrent-ruby < 1.3.7
Fix: upgrade to 1.3.7
RubyGems concurrent-ruby < 1.3.7
Fix: upgrade to 1.3.7
rubyconcurrency concurrent_ruby < 1.3.7
cpe:2.3:a:rubyconcurrency:concurrent_ruby:*:*:*:*:*:ruby:*:*
Original title
Concurrent Ruby: ReadWriteLock allows wrong-thread write release and stray read-release counter corruption
Original description
### 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>))
ghsa CVSS4.0 2.1
Vulnerability type
CWE-414
CWE-667 Improper Locking
Published: 19 Jun 2026 · Updated: 4 Jul 2026 · First seen: 19 Jun 2026