Skip to main content

concurrent-ruby CVE-2026-54906

| EUVDEUVD-2026-38811 LOW
Missing Lock Check (CWE-414)
2026-06-19 https://github.com/ruby-concurrency/concurrent-ruby GHSA-6wx8-w4f5-wwcr
2.1
CVSS 4.0 · Vendor: https://github.com/ruby-concurrency/concurrent-ruby
View

Attack ChainAIDerived

Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain

  1. Access
    Execute code as thread in target Ruby process
  2. Delivery
    Obtain reference to shared ReadWriteLock instance
  3. Exploit
    Call release_write_lock from non-owning thread
  4. Execution
    RUNNING_WRITER bit cleared while Writer 1 still runs
  5. Persist
    Second writer acquires lock concurrently
  6. Impact
    Data race corrupts protected shared state

Vulnerability Assessment

Weakness
CWE
Exploitation Conditions

The write-lock bypass requires that the attacker control or influence a Ruby thread executing within the same OS process as the target application, and that thread must hold a reference to the specific Concurrent::ReadWriteLock instance under attack. The application must use the manual acquire_write_lock / release_write_lock API rather than the block form with_write_lock. A second writer thread must be available to acquire the lock after the attacker clears the RUNNING_WRITER bit. External network attackers cannot trigger this without prior arbitrary code execution in the Ruby process. For the counter-corruption denial-of-service path, the only condition is that any thread with access to the lock object calls release_read_lock without a prior acquire_read_lock; no timing coordination or second thread is needed. Both exploitation paths are limited to in-process scenarios and do not require elevated OS-level privileges or special Ruby runtime configuration.

Impact

Real-world risk is moderate and scoped to in-process threat models. No NVD CVSS vector or EPSS score was provided for this CVE; all metric assessments below are independently derived from the advisory description and reproduction evidence. There is no CISA KEV listing, and this is not a remotely exploitable network vulnerability. The primary threat actors are: (a) untrusted code running as threads within the same Ruby process, such as plugin systems or multi-tenant job queues, and (b) application-level coding defects that call release methods outside a proper acquire/release pair. The integrity impact is high when the lock protects shared mutable state, as the write-exclusion bypass directly creates data races. The availability impact is concrete but bounded: only the specific lock instance becomes permanently unusable after counter corruption; other locks and the broader process are unaffected. The DoS tag in the advisory accurately reflects the stray read-release path. This is not a high-CVSS-but-low-real-risk situation - it is a genuine correctness flaw with meaningful impact in concurrent Ruby applications, though its exploitability is substantially constrained by the requirement to execute code within the same process.

Exploit ScenarioDerived

A proof-of-concept is included in the GitHub Security Advisory. An attacker thread running within the same Ruby process obtains a reference to a shared Concurrent::ReadWriteLock instance, waits for a legitimate writer to signal lock acquisition, then immediately calls release_write_lock from the attacker thread - which succeeds without ownership verification. A second writer thread can then acquire the write lock and execute concurrently with the still-running first writer, creating a data race on any shared mutable state the lock was intended to protect. In the counter-corruption path, the attacker calls release_read_lock on a fresh or idle lock, driving the counter to -1, after which all subsequent read acquisitions by all threads fail with Concurrent::ResourceLimitError.

Monitor Track and reassess. Why: LOW severity (CVSS 0.1-3.9).
Patch available 0.1% exploitation probability (EPSS)

Get an e-mail when this CVE is exploited, patched or re-scored. Free account.

Alert me

AnalysisAIv1 – Jun 19, 2026

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.

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.

Affected VersionsENISA

concurrent-ruby < 1.3.7

Affected Packages

rubygems concurrent-ruby fix: 1.3.7

Source: OSV (Open Source Vulnerabilities)

Affects your stack

Affects 1 package in rubygems

Checking this CVE against your own stacks and servers is part of the Basic plan and up.

Technical ContextAI · show details

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.

Know when the next one hits your stack

  • Watch products and get an email when something new is published
  • Check this CVE against your own stack and servers
  • Exploit and PoC references, threat intelligence from 22 sources

No credit card · 7-day full trial

Severity by source

Vendor (https://github.com/ruby-concurrency/concurrent-ruby) PRIMARY 2.1 LOW
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
vuln.today AI 6.1 MEDIUM

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.

3.1 AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L
4.0 AV:L/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:L/SC:N/SI:N/SA:N

Primary rating from Vendor (https://github.com/ruby-concurrency/concurrent-ruby).

CVSS VectorVendor: https://github.com/ruby-concurrency/concurrent-ruby

Attack Vector
Local
Attack Complexity
High
Attack Requirements
None
Privileges Required
None
User Interaction
None
Vulnerable system: Confidentiality
None
Vulnerable system: Integrity
Low
Vulnerable system: Availability
Low
Subsequent system: Confidentiality
None
Subsequent system: Integrity
None
Subsequent system: Availability
None

Lifecycle Timeline

1
CVSS changed
Jun 24, 2026 - 17:22 NVD
2.1 (LOW)

DescriptionCVE.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:

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

Threat Intelligence Overview

EPSS Score
0.1%
Top 100% exploitable
Threat Intel
1
1 sources
Social Media
1
Sightings
Data Quality
7 sources
Confidence: High
EUVD NVD CVE.org EPSS Vulnrichment GHSA MSRC CIRCL GitHub VulDB

How this decision was reached

The engine reads each signal below and applies a fixed list of rules from top to bottom. The first rule that matches sets the recommended action – like a statement, every line is shown.

SignalValue
Severity LOW
CISA KEV No
Public POC No
EPSS 0.1%
Patch available Yes
Active threat No
Deciding rule
LOW severity (CVSS 0.1-3.9)
What every action level means
Emergency Immediate action required. Criteria: CRITICAL + in CISA KEV (actively exploited) without patch, OR CRITICAL + EPSS > 30% without patch, OR CRITICAL + active threat signals without patch.
Act Now Prioritize within 24-48h. Criteria: in KEV with patch available, OR CRITICAL + public POC, OR HIGH + EPSS > 10%, OR CRITICAL + active threat, OR any CRITICAL severity.
This Week Schedule remediation this week. Criteria: HIGH severity (CVSS 7.0-8.9). Patch within normal change window.
This Month Plan within 30 days. Criteria: MEDIUM severity (CVSS 4.0-6.9), lower exploitation probability. Include in regular patch cycle.
Monitor Track and reassess. Criteria: LOW severity (CVSS 0.1-3.9), minimal exploitation risk. No immediate action needed.

SSVC AssessmentENISA

SSVC Decision· derived
Defer
Exploitation
Poc
System Exposure· derived
Small
Automatable
No
Human Impact· derived
Low
Technical Impact
Partial

Priority Score

10
Low Medium High Critical
KEV: 0
EPSS: 0
CVSS: +10
PoC: 0
Weaponized: 0
Threat intel: 0
Gov advisory: 0

Attack Technique

Denial of Service attacks render applications or systems unavailable by overwhelming resources or triggering failure conditions.

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy