Action not permitted
Modal body text goes here.
Modal Title
Modal Body
GHSA-6WX8-W4F5-WWCR
Vulnerability from github – Published: 2026-06-19 20:47 – Updated: 2026-06-19 20:47Summary
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
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:
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:
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_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 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)
{
"affected": [
{
"package": {
"ecosystem": "RubyGems",
"name": "concurrent-ruby"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.3.7"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-54906"
],
"database_specific": {
"cwe_ids": [
"CWE-414",
"CWE-667"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-19T20:47:41Z",
"nvd_published_at": null,
"severity": "LOW"
},
"details": "### Summary\n`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.\n\n`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`.\n\nThis 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.\n\n### Version\nSoftware: concurrent-ruby\nVersion: 1.3.6\nCommit: 7a1b78941c081106c20a9ca0144ac73a48d254ab\n\n### Details\n\n`release_write_lock` checks only whether the global counter indicates that a writer is running. It does not track or verify ownership:\n\n```ruby\ndef release_write_lock\n return true unless running_writer?\n c = @Counter.update { |counter| counter - RUNNING_WRITER }\n @ReadLock.broadcast\n @WriteLock.signal if waiting_writers(c) \u003e 0\n true\nend\n```\n\nBecause 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.\n\n`release_read_lock` unconditionally decrements the shared counter:\n\n```ruby\ndef release_read_lock\n while true\n c = @Counter.value\n if @Counter.compare_and_set(c, c-1)\n if waiting_writer?(c) \u0026\u0026 running_readers(c) == 1\n @WriteLock.signal\n end\n break\n end\n end\n true\nend\n```\n\nOn 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.\n\n# Reproduce\n\nFrom the root of a `concurrent-ruby` checkout, run:\n\n```bash\nruby -Ilib/concurrent-ruby - \u003c\u003c\u0027RUBY\u0027\nrequire \u0027concurrent/atomic/read_write_lock\u0027\nrequire \u0027concurrent/version\u0027\nrequire \u0027thread\u0027\n\nputs \"ruby=#{RUBY_DESCRIPTION}\"\nputs \"concurrent_ruby_version=#{Concurrent::VERSION}\"\nputs \"poc=ReadWriteLock release methods corrupt or bypass lock state\"\n\nlock = Concurrent::ReadWriteLock.new\nevents = Queue.new\nwriter1_inside = false\n\nwriter1 = Thread.new do\n lock.acquire_write_lock\n writer1_inside = true\n events \u003c\u003c :writer1_acquired\n sleep 0.5\n writer1_inside = false\n lock.release_write_lock\n events \u003c\u003c :writer1_finished\nend\n\nevents.pop\nputs \u0027writer1_acquired=true\u0027\n\nintruder_result = nil\nintruder = Thread.new do\n intruder_result = lock.release_write_lock\nend\nintruder.join\n\nputs \"wrong_thread_release_write_lock_returned=#{intruder_result}\"\n\nwriter2_entered_while_writer1_inside = nil\nwriter2 = Thread.new do\n lock.acquire_write_lock\n writer2_entered_while_writer1_inside = writer1_inside\n lock.release_write_lock\nend\n\nwriter2.join(0.25)\n\nputs \"writer2_acquired_while_writer1_inside=#{writer2_entered_while_writer1_inside}\"\n\nwriter1.join\n\nlock2 = Concurrent::ReadWriteLock.new\nstray_read_release_result = lock2.release_read_lock\ncounter_after_stray_read_release = lock2.instance_eval { @Counter.value }\nread_after_stray_release = begin\n lock2.acquire_read_lock\n \u0027acquired\u0027\nrescue =\u003e error\n \"#{error.class}: #{error.message}\"\nend\n\nputs \"stray_release_read_lock_returned=#{stray_read_release_result}\"\nputs \"counter_after_stray_read_release=#{counter_after_stray_read_release}\"\nputs \"acquire_read_after_stray_release=#{read_after_stray_release}\"\n\nif intruder_result \u0026\u0026 writer2_entered_while_writer1_inside \u0026\u0026 counter_after_stray_read_release == -1\n puts \u0027result=REPRODUCED wrong-thread write release and stray read-release corruption\u0027\nelse\n puts \u0027result=NOT_REPRODUCED\u0027\nend\n```\nExpected result:\n\n- A second thread successfully calls `release_write_lock` while the first writer still holds the lock.\n- A second writer enters while the first writer is still inside the write critical section.\n- Calling `release_read_lock` on a fresh lock changes the counter to `-1`.\n- A subsequent read acquisition fails with `Concurrent::ResourceLimitError`.\n\n### Log evidence\n\nLocal reproduction output:\n\n```text\nruby=ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin25]\nconcurrent_ruby_version=1.3.6\npoc=ReadWriteLock release methods corrupt or bypass lock state\nwriter1_acquired=true\nwrong_thread_release_write_lock_returned=true\nwriter2_acquired_while_writer1_inside=true\nstray_release_read_lock_returned=true\ncounter_after_stray_read_release=-1\nacquire_read_after_stray_release=Concurrent::ResourceLimitError: Too many reader threads\nresult=REPRODUCED wrong-thread write release and stray read-release corruption\n```\n\n### Impact\nThis can break the write-lock mutual exclusion guarantee and can also leave a lock unusable after a stray read release.\nThe 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.\n\n### Credit\nPranjali Thakur - depthfirst ([depthfirst.com](\u003chttp://depthfirst.com\u003e))",
"id": "GHSA-6wx8-w4f5-wwcr",
"modified": "2026-06-19T20:47:41Z",
"published": "2026-06-19T20:47:41Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/ruby-concurrency/concurrent-ruby/security/advisories/GHSA-6wx8-w4f5-wwcr"
},
{
"type": "PACKAGE",
"url": "https://github.com/ruby-concurrency/concurrent-ruby"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "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",
"type": "CVSS_V4"
}
],
"summary": "Concurrent Ruby: ReadWriteLock allows wrong-thread write release and stray read-release counter corruption"
}
BREW-ASCIIDOCTOR-CVE-202… (GHSA-6WX8-W4F5-WWCR)
Vulnerability from osv_homebrew – Published: 2026-08-13 16:36 – Updated: 2026-08-13 16:36 – Source websiteSummary
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
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:
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:
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_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 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)
{
"affected": [
{
"ecosystem_specific": {
"fix": null,
"range_state": "affected",
"resource": "concurrent-ruby",
"resource_purl": "pkg:gem/concurrent-ruby@1.2.3",
"upstream_fixed_in": "1.3.7"
},
"package": {
"ecosystem": "Homebrew",
"name": "asciidoctor",
"purl": "pkg:brew/asciidoctor"
},
"ranges": [
{
"events": [
{
"introduced": "0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"database_specific": {
"confidence": "high",
"source": "matched",
"strategy": "registry",
"upstream_evidence": [
{
"ecosystem": "RubyGems",
"key": "pkg:gem/concurrent-ruby@1.2.3",
"name": "concurrent-ruby",
"resource": "concurrent-ruby",
"strategy": "registry",
"subject_version": "1.2.3"
}
]
},
"details": "### Summary\n`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.\n\n`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`.\n\nThis 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.\n\n### Version\nSoftware: concurrent-ruby\nVersion: 1.3.6\nCommit: 7a1b78941c081106c20a9ca0144ac73a48d254ab\n\n### Details\n\n`release_write_lock` checks only whether the global counter indicates that a writer is running. It does not track or verify ownership:\n\n```ruby\ndef release_write_lock\n return true unless running_writer?\n c = @Counter.update { |counter| counter - RUNNING_WRITER }\n @ReadLock.broadcast\n @WriteLock.signal if waiting_writers(c) \u003e 0\n true\nend\n```\n\nBecause 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.\n\n`release_read_lock` unconditionally decrements the shared counter:\n\n```ruby\ndef release_read_lock\n while true\n c = @Counter.value\n if @Counter.compare_and_set(c, c-1)\n if waiting_writer?(c) \u0026\u0026 running_readers(c) == 1\n @WriteLock.signal\n end\n break\n end\n end\n true\nend\n```\n\nOn 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.\n\n# Reproduce\n\nFrom the root of a `concurrent-ruby` checkout, run:\n\n```bash\nruby -Ilib/concurrent-ruby - \u003c\u003c\u0027RUBY\u0027\nrequire \u0027concurrent/atomic/read_write_lock\u0027\nrequire \u0027concurrent/version\u0027\nrequire \u0027thread\u0027\n\nputs \"ruby=#{RUBY_DESCRIPTION}\"\nputs \"concurrent_ruby_version=#{Concurrent::VERSION}\"\nputs \"poc=ReadWriteLock release methods corrupt or bypass lock state\"\n\nlock = Concurrent::ReadWriteLock.new\nevents = Queue.new\nwriter1_inside = false\n\nwriter1 = Thread.new do\n lock.acquire_write_lock\n writer1_inside = true\n events \u003c\u003c :writer1_acquired\n sleep 0.5\n writer1_inside = false\n lock.release_write_lock\n events \u003c\u003c :writer1_finished\nend\n\nevents.pop\nputs \u0027writer1_acquired=true\u0027\n\nintruder_result = nil\nintruder = Thread.new do\n intruder_result = lock.release_write_lock\nend\nintruder.join\n\nputs \"wrong_thread_release_write_lock_returned=#{intruder_result}\"\n\nwriter2_entered_while_writer1_inside = nil\nwriter2 = Thread.new do\n lock.acquire_write_lock\n writer2_entered_while_writer1_inside = writer1_inside\n lock.release_write_lock\nend\n\nwriter2.join(0.25)\n\nputs \"writer2_acquired_while_writer1_inside=#{writer2_entered_while_writer1_inside}\"\n\nwriter1.join\n\nlock2 = Concurrent::ReadWriteLock.new\nstray_read_release_result = lock2.release_read_lock\ncounter_after_stray_read_release = lock2.instance_eval { @Counter.value }\nread_after_stray_release = begin\n lock2.acquire_read_lock\n \u0027acquired\u0027\nrescue =\u003e error\n \"#{error.class}: #{error.message}\"\nend\n\nputs \"stray_release_read_lock_returned=#{stray_read_release_result}\"\nputs \"counter_after_stray_read_release=#{counter_after_stray_read_release}\"\nputs \"acquire_read_after_stray_release=#{read_after_stray_release}\"\n\nif intruder_result \u0026\u0026 writer2_entered_while_writer1_inside \u0026\u0026 counter_after_stray_read_release == -1\n puts \u0027result=REPRODUCED wrong-thread write release and stray read-release corruption\u0027\nelse\n puts \u0027result=NOT_REPRODUCED\u0027\nend\n```\nExpected result:\n\n- A second thread successfully calls `release_write_lock` while the first writer still holds the lock.\n- A second writer enters while the first writer is still inside the write critical section.\n- Calling `release_read_lock` on a fresh lock changes the counter to `-1`.\n- A subsequent read acquisition fails with `Concurrent::ResourceLimitError`.\n\n### Log evidence\n\nLocal reproduction output:\n\n```text\nruby=ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin25]\nconcurrent_ruby_version=1.3.6\npoc=ReadWriteLock release methods corrupt or bypass lock state\nwriter1_acquired=true\nwrong_thread_release_write_lock_returned=true\nwriter2_acquired_while_writer1_inside=true\nstray_release_read_lock_returned=true\ncounter_after_stray_read_release=-1\nacquire_read_after_stray_release=Concurrent::ResourceLimitError: Too many reader threads\nresult=REPRODUCED wrong-thread write release and stray read-release corruption\n```\n\n### Impact\nThis can break the write-lock mutual exclusion guarantee and can also leave a lock unusable after a stray read release.\nThe 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.\n\n### Credit\nPranjali Thakur - depthfirst ([depthfirst.com](\u003chttp://depthfirst.com\u003e))",
"id": "BREW-asciidoctor-CVE-2026-54906",
"modified": "2026-08-13T16:36:07Z",
"published": "2026-08-13T16:36:07Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/ruby-concurrency/concurrent-ruby/security/advisories/GHSA-6wx8-w4f5-wwcr"
},
{
"type": "PACKAGE",
"url": "https://github.com/ruby-concurrency/concurrent-ruby"
}
],
"schema_version": "1.7.3",
"severity": [
{
"score": "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",
"type": "CVSS_V4"
}
],
"summary": "Concurrent Ruby: ReadWriteLock allows wrong-thread write release and stray read-release counter corruption",
"upstream": [
"GHSA-6wx8-w4f5-wwcr",
"CVE-2026-54906"
]
}
BREW-IMAP-BACKUP-CVE-202… (GHSA-6WX8-W4F5-WWCR)
Vulnerability from osv_homebrew – Published: 2026-08-13 16:59 – Updated: 2026-08-13 16:59 – Source websiteSummary
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
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:
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:
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_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 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)
{
"affected": [
{
"ecosystem_specific": {
"fix": null,
"range_state": "affected",
"resource": "concurrent-ruby",
"resource_purl": "pkg:gem/concurrent-ruby@1.3.6",
"upstream_fixed_in": "1.3.7"
},
"package": {
"ecosystem": "Homebrew",
"name": "imap-backup",
"purl": "pkg:brew/imap-backup"
},
"ranges": [
{
"events": [
{
"introduced": "0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"database_specific": {
"confidence": "high",
"source": "matched",
"strategy": "registry",
"upstream_evidence": [
{
"ecosystem": "RubyGems",
"key": "pkg:gem/concurrent-ruby@1.3.6",
"name": "concurrent-ruby",
"resource": "concurrent-ruby",
"strategy": "registry",
"subject_version": "1.3.6"
}
]
},
"details": "### Summary\n`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.\n\n`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`.\n\nThis 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.\n\n### Version\nSoftware: concurrent-ruby\nVersion: 1.3.6\nCommit: 7a1b78941c081106c20a9ca0144ac73a48d254ab\n\n### Details\n\n`release_write_lock` checks only whether the global counter indicates that a writer is running. It does not track or verify ownership:\n\n```ruby\ndef release_write_lock\n return true unless running_writer?\n c = @Counter.update { |counter| counter - RUNNING_WRITER }\n @ReadLock.broadcast\n @WriteLock.signal if waiting_writers(c) \u003e 0\n true\nend\n```\n\nBecause 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.\n\n`release_read_lock` unconditionally decrements the shared counter:\n\n```ruby\ndef release_read_lock\n while true\n c = @Counter.value\n if @Counter.compare_and_set(c, c-1)\n if waiting_writer?(c) \u0026\u0026 running_readers(c) == 1\n @WriteLock.signal\n end\n break\n end\n end\n true\nend\n```\n\nOn 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.\n\n# Reproduce\n\nFrom the root of a `concurrent-ruby` checkout, run:\n\n```bash\nruby -Ilib/concurrent-ruby - \u003c\u003c\u0027RUBY\u0027\nrequire \u0027concurrent/atomic/read_write_lock\u0027\nrequire \u0027concurrent/version\u0027\nrequire \u0027thread\u0027\n\nputs \"ruby=#{RUBY_DESCRIPTION}\"\nputs \"concurrent_ruby_version=#{Concurrent::VERSION}\"\nputs \"poc=ReadWriteLock release methods corrupt or bypass lock state\"\n\nlock = Concurrent::ReadWriteLock.new\nevents = Queue.new\nwriter1_inside = false\n\nwriter1 = Thread.new do\n lock.acquire_write_lock\n writer1_inside = true\n events \u003c\u003c :writer1_acquired\n sleep 0.5\n writer1_inside = false\n lock.release_write_lock\n events \u003c\u003c :writer1_finished\nend\n\nevents.pop\nputs \u0027writer1_acquired=true\u0027\n\nintruder_result = nil\nintruder = Thread.new do\n intruder_result = lock.release_write_lock\nend\nintruder.join\n\nputs \"wrong_thread_release_write_lock_returned=#{intruder_result}\"\n\nwriter2_entered_while_writer1_inside = nil\nwriter2 = Thread.new do\n lock.acquire_write_lock\n writer2_entered_while_writer1_inside = writer1_inside\n lock.release_write_lock\nend\n\nwriter2.join(0.25)\n\nputs \"writer2_acquired_while_writer1_inside=#{writer2_entered_while_writer1_inside}\"\n\nwriter1.join\n\nlock2 = Concurrent::ReadWriteLock.new\nstray_read_release_result = lock2.release_read_lock\ncounter_after_stray_read_release = lock2.instance_eval { @Counter.value }\nread_after_stray_release = begin\n lock2.acquire_read_lock\n \u0027acquired\u0027\nrescue =\u003e error\n \"#{error.class}: #{error.message}\"\nend\n\nputs \"stray_release_read_lock_returned=#{stray_read_release_result}\"\nputs \"counter_after_stray_read_release=#{counter_after_stray_read_release}\"\nputs \"acquire_read_after_stray_release=#{read_after_stray_release}\"\n\nif intruder_result \u0026\u0026 writer2_entered_while_writer1_inside \u0026\u0026 counter_after_stray_read_release == -1\n puts \u0027result=REPRODUCED wrong-thread write release and stray read-release corruption\u0027\nelse\n puts \u0027result=NOT_REPRODUCED\u0027\nend\n```\nExpected result:\n\n- A second thread successfully calls `release_write_lock` while the first writer still holds the lock.\n- A second writer enters while the first writer is still inside the write critical section.\n- Calling `release_read_lock` on a fresh lock changes the counter to `-1`.\n- A subsequent read acquisition fails with `Concurrent::ResourceLimitError`.\n\n### Log evidence\n\nLocal reproduction output:\n\n```text\nruby=ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin25]\nconcurrent_ruby_version=1.3.6\npoc=ReadWriteLock release methods corrupt or bypass lock state\nwriter1_acquired=true\nwrong_thread_release_write_lock_returned=true\nwriter2_acquired_while_writer1_inside=true\nstray_release_read_lock_returned=true\ncounter_after_stray_read_release=-1\nacquire_read_after_stray_release=Concurrent::ResourceLimitError: Too many reader threads\nresult=REPRODUCED wrong-thread write release and stray read-release corruption\n```\n\n### Impact\nThis can break the write-lock mutual exclusion guarantee and can also leave a lock unusable after a stray read release.\nThe 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.\n\n### Credit\nPranjali Thakur - depthfirst ([depthfirst.com](\u003chttp://depthfirst.com\u003e))",
"id": "BREW-imap-backup-CVE-2026-54906",
"modified": "2026-08-13T16:59:45Z",
"published": "2026-08-13T16:59:45Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/ruby-concurrency/concurrent-ruby/security/advisories/GHSA-6wx8-w4f5-wwcr"
},
{
"type": "PACKAGE",
"url": "https://github.com/ruby-concurrency/concurrent-ruby"
}
],
"schema_version": "1.7.3",
"severity": [
{
"score": "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",
"type": "CVSS_V4"
}
],
"summary": "Concurrent Ruby: ReadWriteLock allows wrong-thread write release and stray read-release counter corruption",
"upstream": [
"GHSA-6wx8-w4f5-wwcr",
"CVE-2026-54906"
]
}
BREW-KRANE-CVE-2026-54906 (GHSA-6WX8-W4F5-WWCR)
Vulnerability from osv_homebrew – Published: 2026-08-13 17:01 – Updated: 2026-08-13 17:01 – Source websiteSummary
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
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:
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:
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_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 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)
{
"affected": [
{
"ecosystem_specific": {
"fix": null,
"range_state": "affected",
"resource": "concurrent-ruby",
"resource_purl": "pkg:gem/concurrent-ruby@1.3.6",
"upstream_fixed_in": "1.3.7"
},
"package": {
"ecosystem": "Homebrew",
"name": "krane",
"purl": "pkg:brew/krane"
},
"ranges": [
{
"events": [
{
"introduced": "0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"database_specific": {
"confidence": "high",
"source": "matched",
"strategy": "registry",
"upstream_evidence": [
{
"ecosystem": "RubyGems",
"key": "pkg:gem/concurrent-ruby@1.3.6",
"name": "concurrent-ruby",
"resource": "concurrent-ruby",
"strategy": "registry",
"subject_version": "1.3.6"
}
]
},
"details": "### Summary\n`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.\n\n`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`.\n\nThis 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.\n\n### Version\nSoftware: concurrent-ruby\nVersion: 1.3.6\nCommit: 7a1b78941c081106c20a9ca0144ac73a48d254ab\n\n### Details\n\n`release_write_lock` checks only whether the global counter indicates that a writer is running. It does not track or verify ownership:\n\n```ruby\ndef release_write_lock\n return true unless running_writer?\n c = @Counter.update { |counter| counter - RUNNING_WRITER }\n @ReadLock.broadcast\n @WriteLock.signal if waiting_writers(c) \u003e 0\n true\nend\n```\n\nBecause 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.\n\n`release_read_lock` unconditionally decrements the shared counter:\n\n```ruby\ndef release_read_lock\n while true\n c = @Counter.value\n if @Counter.compare_and_set(c, c-1)\n if waiting_writer?(c) \u0026\u0026 running_readers(c) == 1\n @WriteLock.signal\n end\n break\n end\n end\n true\nend\n```\n\nOn 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.\n\n# Reproduce\n\nFrom the root of a `concurrent-ruby` checkout, run:\n\n```bash\nruby -Ilib/concurrent-ruby - \u003c\u003c\u0027RUBY\u0027\nrequire \u0027concurrent/atomic/read_write_lock\u0027\nrequire \u0027concurrent/version\u0027\nrequire \u0027thread\u0027\n\nputs \"ruby=#{RUBY_DESCRIPTION}\"\nputs \"concurrent_ruby_version=#{Concurrent::VERSION}\"\nputs \"poc=ReadWriteLock release methods corrupt or bypass lock state\"\n\nlock = Concurrent::ReadWriteLock.new\nevents = Queue.new\nwriter1_inside = false\n\nwriter1 = Thread.new do\n lock.acquire_write_lock\n writer1_inside = true\n events \u003c\u003c :writer1_acquired\n sleep 0.5\n writer1_inside = false\n lock.release_write_lock\n events \u003c\u003c :writer1_finished\nend\n\nevents.pop\nputs \u0027writer1_acquired=true\u0027\n\nintruder_result = nil\nintruder = Thread.new do\n intruder_result = lock.release_write_lock\nend\nintruder.join\n\nputs \"wrong_thread_release_write_lock_returned=#{intruder_result}\"\n\nwriter2_entered_while_writer1_inside = nil\nwriter2 = Thread.new do\n lock.acquire_write_lock\n writer2_entered_while_writer1_inside = writer1_inside\n lock.release_write_lock\nend\n\nwriter2.join(0.25)\n\nputs \"writer2_acquired_while_writer1_inside=#{writer2_entered_while_writer1_inside}\"\n\nwriter1.join\n\nlock2 = Concurrent::ReadWriteLock.new\nstray_read_release_result = lock2.release_read_lock\ncounter_after_stray_read_release = lock2.instance_eval { @Counter.value }\nread_after_stray_release = begin\n lock2.acquire_read_lock\n \u0027acquired\u0027\nrescue =\u003e error\n \"#{error.class}: #{error.message}\"\nend\n\nputs \"stray_release_read_lock_returned=#{stray_read_release_result}\"\nputs \"counter_after_stray_read_release=#{counter_after_stray_read_release}\"\nputs \"acquire_read_after_stray_release=#{read_after_stray_release}\"\n\nif intruder_result \u0026\u0026 writer2_entered_while_writer1_inside \u0026\u0026 counter_after_stray_read_release == -1\n puts \u0027result=REPRODUCED wrong-thread write release and stray read-release corruption\u0027\nelse\n puts \u0027result=NOT_REPRODUCED\u0027\nend\n```\nExpected result:\n\n- A second thread successfully calls `release_write_lock` while the first writer still holds the lock.\n- A second writer enters while the first writer is still inside the write critical section.\n- Calling `release_read_lock` on a fresh lock changes the counter to `-1`.\n- A subsequent read acquisition fails with `Concurrent::ResourceLimitError`.\n\n### Log evidence\n\nLocal reproduction output:\n\n```text\nruby=ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin25]\nconcurrent_ruby_version=1.3.6\npoc=ReadWriteLock release methods corrupt or bypass lock state\nwriter1_acquired=true\nwrong_thread_release_write_lock_returned=true\nwriter2_acquired_while_writer1_inside=true\nstray_release_read_lock_returned=true\ncounter_after_stray_read_release=-1\nacquire_read_after_stray_release=Concurrent::ResourceLimitError: Too many reader threads\nresult=REPRODUCED wrong-thread write release and stray read-release corruption\n```\n\n### Impact\nThis can break the write-lock mutual exclusion guarantee and can also leave a lock unusable after a stray read release.\nThe 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.\n\n### Credit\nPranjali Thakur - depthfirst ([depthfirst.com](\u003chttp://depthfirst.com\u003e))",
"id": "BREW-krane-CVE-2026-54906",
"modified": "2026-08-13T17:01:45Z",
"published": "2026-08-13T17:01:45Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/ruby-concurrency/concurrent-ruby/security/advisories/GHSA-6wx8-w4f5-wwcr"
},
{
"type": "PACKAGE",
"url": "https://github.com/ruby-concurrency/concurrent-ruby"
}
],
"schema_version": "1.7.3",
"severity": [
{
"score": "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",
"type": "CVSS_V4"
}
],
"summary": "Concurrent Ruby: ReadWriteLock allows wrong-thread write release and stray read-release counter corruption",
"upstream": [
"GHSA-6wx8-w4f5-wwcr",
"CVE-2026-54906"
]
}
BREW-TRAVIS-CVE-2026-54906 (GHSA-6WX8-W4F5-WWCR)
Vulnerability from osv_homebrew – Published: 2026-08-13 17:44 – Updated: 2026-08-13 17:44 – Source websiteSummary
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
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:
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:
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_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 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)
{
"affected": [
{
"ecosystem_specific": {
"fix": null,
"range_state": "affected",
"resource": "concurrent-ruby",
"resource_purl": "pkg:gem/concurrent-ruby@1.3.1",
"upstream_fixed_in": "1.3.7"
},
"package": {
"ecosystem": "Homebrew",
"name": "travis",
"purl": "pkg:brew/travis"
},
"ranges": [
{
"events": [
{
"introduced": "0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"database_specific": {
"confidence": "high",
"source": "matched",
"strategy": "registry",
"upstream_evidence": [
{
"ecosystem": "RubyGems",
"key": "pkg:gem/concurrent-ruby@1.3.1",
"name": "concurrent-ruby",
"resource": "concurrent-ruby",
"strategy": "registry",
"subject_version": "1.3.1"
}
]
},
"details": "### Summary\n`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.\n\n`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`.\n\nThis 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.\n\n### Version\nSoftware: concurrent-ruby\nVersion: 1.3.6\nCommit: 7a1b78941c081106c20a9ca0144ac73a48d254ab\n\n### Details\n\n`release_write_lock` checks only whether the global counter indicates that a writer is running. It does not track or verify ownership:\n\n```ruby\ndef release_write_lock\n return true unless running_writer?\n c = @Counter.update { |counter| counter - RUNNING_WRITER }\n @ReadLock.broadcast\n @WriteLock.signal if waiting_writers(c) \u003e 0\n true\nend\n```\n\nBecause 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.\n\n`release_read_lock` unconditionally decrements the shared counter:\n\n```ruby\ndef release_read_lock\n while true\n c = @Counter.value\n if @Counter.compare_and_set(c, c-1)\n if waiting_writer?(c) \u0026\u0026 running_readers(c) == 1\n @WriteLock.signal\n end\n break\n end\n end\n true\nend\n```\n\nOn 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.\n\n# Reproduce\n\nFrom the root of a `concurrent-ruby` checkout, run:\n\n```bash\nruby -Ilib/concurrent-ruby - \u003c\u003c\u0027RUBY\u0027\nrequire \u0027concurrent/atomic/read_write_lock\u0027\nrequire \u0027concurrent/version\u0027\nrequire \u0027thread\u0027\n\nputs \"ruby=#{RUBY_DESCRIPTION}\"\nputs \"concurrent_ruby_version=#{Concurrent::VERSION}\"\nputs \"poc=ReadWriteLock release methods corrupt or bypass lock state\"\n\nlock = Concurrent::ReadWriteLock.new\nevents = Queue.new\nwriter1_inside = false\n\nwriter1 = Thread.new do\n lock.acquire_write_lock\n writer1_inside = true\n events \u003c\u003c :writer1_acquired\n sleep 0.5\n writer1_inside = false\n lock.release_write_lock\n events \u003c\u003c :writer1_finished\nend\n\nevents.pop\nputs \u0027writer1_acquired=true\u0027\n\nintruder_result = nil\nintruder = Thread.new do\n intruder_result = lock.release_write_lock\nend\nintruder.join\n\nputs \"wrong_thread_release_write_lock_returned=#{intruder_result}\"\n\nwriter2_entered_while_writer1_inside = nil\nwriter2 = Thread.new do\n lock.acquire_write_lock\n writer2_entered_while_writer1_inside = writer1_inside\n lock.release_write_lock\nend\n\nwriter2.join(0.25)\n\nputs \"writer2_acquired_while_writer1_inside=#{writer2_entered_while_writer1_inside}\"\n\nwriter1.join\n\nlock2 = Concurrent::ReadWriteLock.new\nstray_read_release_result = lock2.release_read_lock\ncounter_after_stray_read_release = lock2.instance_eval { @Counter.value }\nread_after_stray_release = begin\n lock2.acquire_read_lock\n \u0027acquired\u0027\nrescue =\u003e error\n \"#{error.class}: #{error.message}\"\nend\n\nputs \"stray_release_read_lock_returned=#{stray_read_release_result}\"\nputs \"counter_after_stray_read_release=#{counter_after_stray_read_release}\"\nputs \"acquire_read_after_stray_release=#{read_after_stray_release}\"\n\nif intruder_result \u0026\u0026 writer2_entered_while_writer1_inside \u0026\u0026 counter_after_stray_read_release == -1\n puts \u0027result=REPRODUCED wrong-thread write release and stray read-release corruption\u0027\nelse\n puts \u0027result=NOT_REPRODUCED\u0027\nend\n```\nExpected result:\n\n- A second thread successfully calls `release_write_lock` while the first writer still holds the lock.\n- A second writer enters while the first writer is still inside the write critical section.\n- Calling `release_read_lock` on a fresh lock changes the counter to `-1`.\n- A subsequent read acquisition fails with `Concurrent::ResourceLimitError`.\n\n### Log evidence\n\nLocal reproduction output:\n\n```text\nruby=ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin25]\nconcurrent_ruby_version=1.3.6\npoc=ReadWriteLock release methods corrupt or bypass lock state\nwriter1_acquired=true\nwrong_thread_release_write_lock_returned=true\nwriter2_acquired_while_writer1_inside=true\nstray_release_read_lock_returned=true\ncounter_after_stray_read_release=-1\nacquire_read_after_stray_release=Concurrent::ResourceLimitError: Too many reader threads\nresult=REPRODUCED wrong-thread write release and stray read-release corruption\n```\n\n### Impact\nThis can break the write-lock mutual exclusion guarantee and can also leave a lock unusable after a stray read release.\nThe 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.\n\n### Credit\nPranjali Thakur - depthfirst ([depthfirst.com](\u003chttp://depthfirst.com\u003e))",
"id": "BREW-travis-CVE-2026-54906",
"modified": "2026-08-13T17:44:53Z",
"published": "2026-08-13T17:44:53Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/ruby-concurrency/concurrent-ruby/security/advisories/GHSA-6wx8-w4f5-wwcr"
},
{
"type": "PACKAGE",
"url": "https://github.com/ruby-concurrency/concurrent-ruby"
}
],
"schema_version": "1.7.3",
"severity": [
{
"score": "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",
"type": "CVSS_V4"
}
],
"summary": "Concurrent Ruby: ReadWriteLock allows wrong-thread write release and stray read-release counter corruption",
"upstream": [
"GHSA-6wx8-w4f5-wwcr",
"CVE-2026-54906"
]
}
BREW-UFFIZZI-CVE-2026-54906 (GHSA-6WX8-W4F5-WWCR)
Vulnerability from osv_homebrew – Published: 2026-08-13 17:46 – Updated: 2026-08-13 17:46 – Source websiteSummary
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
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:
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:
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_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 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)
{
"affected": [
{
"ecosystem_specific": {
"fix": null,
"range_state": "affected",
"resource": "concurrent-ruby",
"resource_purl": "pkg:gem/concurrent-ruby@1.3.5",
"upstream_fixed_in": "1.3.7"
},
"package": {
"ecosystem": "Homebrew",
"name": "uffizzi",
"purl": "pkg:brew/uffizzi"
},
"ranges": [
{
"events": [
{
"introduced": "0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"database_specific": {
"confidence": "high",
"source": "matched",
"strategy": "registry",
"upstream_evidence": [
{
"ecosystem": "RubyGems",
"key": "pkg:gem/concurrent-ruby@1.3.5",
"name": "concurrent-ruby",
"resource": "concurrent-ruby",
"strategy": "registry",
"subject_version": "1.3.5"
}
]
},
"details": "### Summary\n`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.\n\n`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`.\n\nThis 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.\n\n### Version\nSoftware: concurrent-ruby\nVersion: 1.3.6\nCommit: 7a1b78941c081106c20a9ca0144ac73a48d254ab\n\n### Details\n\n`release_write_lock` checks only whether the global counter indicates that a writer is running. It does not track or verify ownership:\n\n```ruby\ndef release_write_lock\n return true unless running_writer?\n c = @Counter.update { |counter| counter - RUNNING_WRITER }\n @ReadLock.broadcast\n @WriteLock.signal if waiting_writers(c) \u003e 0\n true\nend\n```\n\nBecause 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.\n\n`release_read_lock` unconditionally decrements the shared counter:\n\n```ruby\ndef release_read_lock\n while true\n c = @Counter.value\n if @Counter.compare_and_set(c, c-1)\n if waiting_writer?(c) \u0026\u0026 running_readers(c) == 1\n @WriteLock.signal\n end\n break\n end\n end\n true\nend\n```\n\nOn 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.\n\n# Reproduce\n\nFrom the root of a `concurrent-ruby` checkout, run:\n\n```bash\nruby -Ilib/concurrent-ruby - \u003c\u003c\u0027RUBY\u0027\nrequire \u0027concurrent/atomic/read_write_lock\u0027\nrequire \u0027concurrent/version\u0027\nrequire \u0027thread\u0027\n\nputs \"ruby=#{RUBY_DESCRIPTION}\"\nputs \"concurrent_ruby_version=#{Concurrent::VERSION}\"\nputs \"poc=ReadWriteLock release methods corrupt or bypass lock state\"\n\nlock = Concurrent::ReadWriteLock.new\nevents = Queue.new\nwriter1_inside = false\n\nwriter1 = Thread.new do\n lock.acquire_write_lock\n writer1_inside = true\n events \u003c\u003c :writer1_acquired\n sleep 0.5\n writer1_inside = false\n lock.release_write_lock\n events \u003c\u003c :writer1_finished\nend\n\nevents.pop\nputs \u0027writer1_acquired=true\u0027\n\nintruder_result = nil\nintruder = Thread.new do\n intruder_result = lock.release_write_lock\nend\nintruder.join\n\nputs \"wrong_thread_release_write_lock_returned=#{intruder_result}\"\n\nwriter2_entered_while_writer1_inside = nil\nwriter2 = Thread.new do\n lock.acquire_write_lock\n writer2_entered_while_writer1_inside = writer1_inside\n lock.release_write_lock\nend\n\nwriter2.join(0.25)\n\nputs \"writer2_acquired_while_writer1_inside=#{writer2_entered_while_writer1_inside}\"\n\nwriter1.join\n\nlock2 = Concurrent::ReadWriteLock.new\nstray_read_release_result = lock2.release_read_lock\ncounter_after_stray_read_release = lock2.instance_eval { @Counter.value }\nread_after_stray_release = begin\n lock2.acquire_read_lock\n \u0027acquired\u0027\nrescue =\u003e error\n \"#{error.class}: #{error.message}\"\nend\n\nputs \"stray_release_read_lock_returned=#{stray_read_release_result}\"\nputs \"counter_after_stray_read_release=#{counter_after_stray_read_release}\"\nputs \"acquire_read_after_stray_release=#{read_after_stray_release}\"\n\nif intruder_result \u0026\u0026 writer2_entered_while_writer1_inside \u0026\u0026 counter_after_stray_read_release == -1\n puts \u0027result=REPRODUCED wrong-thread write release and stray read-release corruption\u0027\nelse\n puts \u0027result=NOT_REPRODUCED\u0027\nend\n```\nExpected result:\n\n- A second thread successfully calls `release_write_lock` while the first writer still holds the lock.\n- A second writer enters while the first writer is still inside the write critical section.\n- Calling `release_read_lock` on a fresh lock changes the counter to `-1`.\n- A subsequent read acquisition fails with `Concurrent::ResourceLimitError`.\n\n### Log evidence\n\nLocal reproduction output:\n\n```text\nruby=ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin25]\nconcurrent_ruby_version=1.3.6\npoc=ReadWriteLock release methods corrupt or bypass lock state\nwriter1_acquired=true\nwrong_thread_release_write_lock_returned=true\nwriter2_acquired_while_writer1_inside=true\nstray_release_read_lock_returned=true\ncounter_after_stray_read_release=-1\nacquire_read_after_stray_release=Concurrent::ResourceLimitError: Too many reader threads\nresult=REPRODUCED wrong-thread write release and stray read-release corruption\n```\n\n### Impact\nThis can break the write-lock mutual exclusion guarantee and can also leave a lock unusable after a stray read release.\nThe 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.\n\n### Credit\nPranjali Thakur - depthfirst ([depthfirst.com](\u003chttp://depthfirst.com\u003e))",
"id": "BREW-uffizzi-CVE-2026-54906",
"modified": "2026-08-13T17:46:08Z",
"published": "2026-08-13T17:46:08Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/ruby-concurrency/concurrent-ruby/security/advisories/GHSA-6wx8-w4f5-wwcr"
},
{
"type": "PACKAGE",
"url": "https://github.com/ruby-concurrency/concurrent-ruby"
}
],
"schema_version": "1.7.3",
"severity": [
{
"score": "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",
"type": "CVSS_V4"
}
],
"summary": "Concurrent Ruby: ReadWriteLock allows wrong-thread write release and stray read-release counter corruption",
"upstream": [
"GHSA-6wx8-w4f5-wwcr",
"CVE-2026-54906"
]
}
CLEANSTART-2026-DE16221 (CVE-2026-54171)
Vulnerability from cleanstart – Published: 2026-07-21 08:19 – Updated: 2026-09-18 11:59 – Source websiteMultiple security vulnerabilities affect the ruby-fluentd-1.18 package. These issues are resolved in later releases. See references for individual vulnerability details.
{
"affected": [
{
"package": {
"ecosystem": "CleanStart",
"name": "ruby-fluentd-1.18"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.18.0-r5"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"credits": [],
"database_specific": {},
"details": "Multiple security vulnerabilities affect the ruby-fluentd-1.18 package. These issues are resolved in later releases. See references for individual vulnerability details.",
"id": "CLEANSTART-2026-DE16221",
"modified": "2026-09-18T11:59:01.768079Z",
"published": "2026-07-21T08:19:55.189420Z",
"references": [
{
"type": "ADVISORY",
"url": "https://github.com/cleanstart-dev/cleanstart-security-advisories/tree/main/advisories/2026/CLEANSTART-2026-DE16221.json"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2026-54171"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2026-54297"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2026-54522"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2026-54904"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2026-54905"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2026-54906"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-6wx8-w4f5-wwcr"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-98m9-hrrm-r99r"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-h8w8-99g7-qmvj"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-wv3x-4vxv-whpp"
},
{
"type": "WEB",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54171"
},
{
"type": "WEB",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54297"
},
{
"type": "WEB",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54522"
},
{
"type": "WEB",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54904"
},
{
"type": "WEB",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54905"
},
{
"type": "WEB",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54906"
}
],
"related": [],
"schema_version": "1.7.3",
"summary": "Security fixes for CVE-2026-54171, CVE-2026-54297, CVE-2026-54522, CVE-2026-54904, CVE-2026-54905, CVE-2026-54906, ghsa-6wx8-w4f5-wwcr, ghsa-98m9-hrrm-r99r, ghsa-h8w8-99g7-qmvj, ghsa-wv3x-4vxv-whpp applied in versions: 1.18.0-r4, 1.18.0-r5",
"upstream": [
"CVE-2026-54171",
"CVE-2026-54297",
"CVE-2026-54522",
"CVE-2026-54904",
"CVE-2026-54905",
"CVE-2026-54906",
"ghsa-6wx8-w4f5-wwcr",
"ghsa-98m9-hrrm-r99r",
"ghsa-h8w8-99g7-qmvj",
"ghsa-wv3x-4vxv-whpp"
],
"withdrawn": "2026-09-18T11:59:01.768079Z"
}
CLEANSTART-2026-RJ92739 (GHSA-6WX8-W4F5-WWCR)
Vulnerability from cleanstart – Published: 2026-09-18 09:55 – Updated: 2026-07-08 04:44 – Source websiteghsa-6wx8-w4f5-wwcr affects multiple packages. This issue is resolved in later releases. See references for individual vulnerability details.
{
"affected": [
{
"package": {
"ecosystem": "CleanStart",
"name": "logstash-fips"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "9.4.3-r1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "CleanStart",
"name": "ruby-fluentd-1.18"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.18.0-r4"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "CleanStart",
"name": "ruby-fluentd-1.19"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.19.2-r3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"credits": [],
"database_specific": {},
"details": "ghsa-6wx8-w4f5-wwcr affects multiple packages. This issue is resolved in later releases. See references for individual vulnerability details.",
"id": "CLEANSTART-2026-RJ92739",
"modified": "2026-07-08T04:44:58Z",
"published": "2026-09-18T09:55:54.437571Z",
"references": [
{
"type": "ADVISORY",
"url": "https://github.com/cleanstart-dev/cleanstart-security-advisories/tree/main/advisories/2026/CLEANSTART-2026-RJ92739.json"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-6wx8-w4f5-wwcr"
}
],
"related": [],
"schema_version": "1.7.3",
"summary": "Security fix for ghsa-6wx8-w4f5-wwcr applied in: logstash-fips 9.4.3-r1, ruby-fluentd-1.18 1.18.0-r4, ruby-fluentd-1.19 1.19.2-r3",
"upstream": [
"ghsa-6wx8-w4f5-wwcr"
]
}
CLEANSTART-2026-UT74115 (CVE-2026-33637)
Vulnerability from cleanstart – Published: 2026-07-21 08:19 – Updated: 2026-09-18 11:59 – Source websiteMultiple security vulnerabilities affect the ruby-fluentd-1.19 package. These issues are resolved in later releases. See references for individual vulnerability details.
| URL | Type | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{
"affected": [
{
"package": {
"ecosystem": "CleanStart",
"name": "ruby-fluentd-1.19"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.19.2-r4"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"credits": [],
"database_specific": {},
"details": "Multiple security vulnerabilities affect the ruby-fluentd-1.19 package. These issues are resolved in later releases. See references for individual vulnerability details.",
"id": "CLEANSTART-2026-UT74115",
"modified": "2026-09-18T11:59:01.768079Z",
"published": "2026-07-21T08:19:01.995115Z",
"references": [
{
"type": "ADVISORY",
"url": "https://github.com/cleanstart-dev/cleanstart-security-advisories/tree/main/advisories/2026/CLEANSTART-2026-UT74115.json"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2026-33637"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2026-41316"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2026-42245"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2026-42246"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2026-42256"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2026-42257"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2026-42258"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2026-54171"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2026-54297"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2026-54696"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2026-54904"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2026-54905"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/CVE-2026-54906"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-5rv5-xj5j-3484"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-6wx8-w4f5-wwcr"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-75xq-5h9v-w6px"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-87pf-fpwv-p7m7"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-98m9-hrrm-r99r"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-h8w8-99g7-qmvj"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-hm49-wcqc-g2xg"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-q2mw-fvj9-vvcw"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-vcgp-9326-pqcp"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/ghsa-wv3x-4vxv-whpp"
},
{
"type": "WEB",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33637"
},
{
"type": "WEB",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-41316"
},
{
"type": "WEB",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-42245"
},
{
"type": "WEB",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-42246"
},
{
"type": "WEB",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-42256"
},
{
"type": "WEB",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-42257"
},
{
"type": "WEB",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-42258"
},
{
"type": "WEB",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54171"
},
{
"type": "WEB",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54297"
},
{
"type": "WEB",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54696"
},
{
"type": "WEB",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54904"
},
{
"type": "WEB",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54905"
},
{
"type": "WEB",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54906"
}
],
"related": [],
"schema_version": "1.7.3",
"summary": "Security fixes for CVE-2026-33637, CVE-2026-41316, CVE-2026-42245, CVE-2026-42246, CVE-2026-42256, CVE-2026-42257, CVE-2026-42258, CVE-2026-54171, CVE-2026-54297, CVE-2026-54696, CVE-2026-54904, CVE-2026-54905, CVE-2026-54906, ghsa-5rv5-xj5j-3484, ghsa-6wx8-w4f5-wwcr, ghsa-75xq-5h9v-w6px, ghsa-87pf-fpwv-p7m7, ghsa-98m9-hrrm-r99r, ghsa-h8w8-99g7-qmvj, ghsa-hm49-wcqc-g2xg, ghsa-q2mw-fvj9-vvcw, ghsa-vcgp-9326-pqcp, ghsa-wv3x-4vxv-whpp applied in versions: 1.19.2-r2, 1.19.2-r3, 1.19.2-r4",
"upstream": [
"CVE-2026-33637",
"CVE-2026-41316",
"CVE-2026-42245",
"CVE-2026-42246",
"CVE-2026-42256",
"CVE-2026-42257",
"CVE-2026-42258",
"CVE-2026-54171",
"CVE-2026-54297",
"CVE-2026-54696",
"CVE-2026-54904",
"CVE-2026-54905",
"CVE-2026-54906",
"ghsa-5rv5-xj5j-3484",
"ghsa-6wx8-w4f5-wwcr",
"ghsa-75xq-5h9v-w6px",
"ghsa-87pf-fpwv-p7m7",
"ghsa-98m9-hrrm-r99r",
"ghsa-h8w8-99g7-qmvj",
"ghsa-hm49-wcqc-g2xg",
"ghsa-q2mw-fvj9-vvcw",
"ghsa-vcgp-9326-pqcp",
"ghsa-wv3x-4vxv-whpp"
],
"withdrawn": "2026-09-18T11:59:01.768079Z"
}
CLEANSTART-2026-VR87787 (CVE-2026-55831)
Vulnerability from cleanstart – Published: 2026-08-13 12:10 – Updated: 2026-09-18 11:59 – Source websitePackage logstash-fips version 9.4.3-r1 fixes 76 vulnerabilities: CVE-2026-55831, CVE-2026-55833, CVE-2026-56745, CVE-2026-50020, CVE-2026-56746...
| URL | Type | |
|---|---|---|
{
"affected": [
{
"package": {
"ecosystem": "CleanStart",
"name": "logstash-fips"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "9.4.3-r1"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"9.4.3-r1"
]
}
],
"credits": [],
"database_specific": {},
"details": "Package logstash-fips version 9.4.3-r1 fixes 76 vulnerabilities: CVE-2026-55831, CVE-2026-55833, CVE-2026-56745, CVE-2026-50020, CVE-2026-56746...",
"id": "CLEANSTART-2026-VR87787",
"modified": "2026-09-18T11:59:01.768079Z",
"published": "2026-08-13T12:10:09Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/elastic/logstash"
}
],
"related": [],
"schema_version": "1.7.3",
"summary": "Security fixes in logstash-fips 9.4.3-r1",
"upstream": [
"CVE-2026-55831",
"CVE-2026-55833",
"CVE-2026-56745",
"CVE-2026-50020",
"CVE-2026-56746",
"CVE-2026-59898",
"CVE-2026-59899",
"CVE-2026-59921",
"ghsa-jppx-w49h-x2qq",
"ghsa-6jqx-86gh-f27w",
"ghsa-mvh2-crg5-v77c",
"ghsa-6cqp-g7gg-8hr5",
"ghsa-4mp9-239f-g9hg",
"ghsa-gcjf-9mgh-3p7g",
"ghsa-q4f6-jm68-57ww",
"ghsa-hvcg-qmg6-jm4c",
"CVE-2026-59901",
"ghsa-558v-64gr-wgg4",
"CVE-2026-54512",
"CVE-2026-54513",
"CVE-2026-54514",
"CVE-2026-54515",
"CVE-2026-59888",
"CVE-2026-54516",
"CVE-2026-54517",
"CVE-2026-54518",
"CVE-2026-59889",
"ghsa-5gvw-p9qm-jgwh",
"ghsa-5hh8-q8hv-fr38",
"ghsa-rcqc-6cw3-h962",
"ghsa-9fxm-vc8v-hj55",
"ghsa-mhm7-754m-9p8w",
"ghsa-j3rv-43j4-c7qm",
"ghsa-rmj7-2vxq-3g9f",
"ghsa-5jmj-h7xm-6q6v",
"ghsa-3pjw-73gf-8qr5",
"ghsa-hgj6-7826-r7m5",
"CVE-2025-14813",
"CVE-2026-0636",
"CVE-2026-5598",
"ghsa-574f-3g2m-x479",
"ghsa-p93r-85wp-75v3",
"ghsa-c3fc-8qff-9hwx",
"CVE-2026-5588",
"ghsa-wg6q-6289-32hp",
"ghsa-r7wm-3cxj-wff9",
"ghsa-72hv-8253-57qq",
"CVE-2026-59949",
"ghsa-xx22-p4ch-683r",
"CVE-2026-47240",
"CVE-2026-47242",
"CVE-2026-47241",
"ghsa-8p34-64r3-mwg8",
"ghsa-46q3-7gv7-qmgg",
"ghsa-c4fp-cxrr-mj66",
"CVE-2026-47736",
"CVE-2026-47737",
"ghsa-qpgp-93vx-g8v8",
"ghsa-2vqw-3mp8-cgmx",
"CVE-2026-54906",
"CVE-2026-54904",
"CVE-2026-54905",
"ghsa-h8w8-99g7-qmvj",
"ghsa-6wx8-w4f5-wwcr",
"ghsa-wv3x-4vxv-whpp",
"CVE-2026-54696",
"ghsa-x2f5-4prf-w687",
"ghsa-5prr-v3j2-97mh",
"ghsa-5v8h-3h3q-446p",
"ghsa-8678-w3jw-xfc2",
"ghsa-9cv2-cfxc-v4v2",
"ghsa-p67v-3w7g-wjg7",
"ghsa-phwj-rprq-35pp",
"ghsa-wfpw-mmfh-qq69",
"ghsa-wjv4-x9w8-wm3h",
"ghsa-g9g8-vgvw-g3vf"
],
"withdrawn": "2026-09-18T11:59:01.768079Z"
}
CLEANSTART-2026-ZB89546 (GHSA-H8W8-99G7-QMVJ)
Vulnerability from cleanstart – Published: 2026-07-30 07:10 – Updated: 2026-09-18 11:59 – Source websitePackage ruby-fluentd-1.19 version 1.19.2-r3 fixes 9 vulnerabilities: ghsa-h8w8-99g7-qmvj, ghsa-6wx8-w4f5-wwcr, ghsa-wv3x-4vxv-whpp, CVE-2026-54904, CVE-2026-54905...
| URL | Type | |
|---|---|---|
{
"affected": [
{
"package": {
"ecosystem": "CleanStart",
"name": "ruby-fluentd-1.19"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.19.2-r3"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"1.19.2-r3"
]
}
],
"credits": [],
"database_specific": {},
"details": "Package ruby-fluentd-1.19 version 1.19.2-r3 fixes 9 vulnerabilities: ghsa-h8w8-99g7-qmvj, ghsa-6wx8-w4f5-wwcr, ghsa-wv3x-4vxv-whpp, CVE-2026-54904, CVE-2026-54905...",
"id": "CLEANSTART-2026-ZB89546",
"modified": "2026-09-18T11:59:01.768079Z",
"published": "2026-07-30T07:10:53Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/fluent/fluentd"
}
],
"related": [],
"schema_version": "1.7.3",
"summary": "Security fixes in ruby-fluentd-1.19 1.19.2-r3",
"upstream": [
"ghsa-h8w8-99g7-qmvj",
"ghsa-6wx8-w4f5-wwcr",
"ghsa-wv3x-4vxv-whpp",
"CVE-2026-54904",
"CVE-2026-54905",
"CVE-2026-54906",
"ghsa-98m9-hrrm-r99r",
"CVE-2026-54297",
"CVE-2026-54171"
],
"withdrawn": "2026-09-18T11:59:01.768079Z"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.