Vulnerable Library - jekyll-4.4.1.gem
Path to dependency file: /Gemfile.lock
Path to vulnerable library: /tmp/containerbase/cache/.ruby/cache/concurrent-ruby-1.3.6.gem
Vulnerabilities
| Vulnerability |
Severity |
CVSS |
Dependency |
Type |
Fixed in (jekyll version) |
Remediation Possible** |
| CVE-2026-54904 |
High |
7.5 |
concurrent-ruby-1.3.6.gem |
Transitive |
N/A* |
❌ |
| CVE-2026-54905 |
Medium |
5.3 |
concurrent-ruby-1.3.6.gem |
Transitive |
N/A* |
❌ |
| CVE-2026-54906 |
Medium |
4.0 |
concurrent-ruby-1.3.6.gem |
Transitive |
N/A* |
❌ |
*For some transitive vulnerabilities, there is no version of direct dependency with a fix. Check the "Details" section below to see if there is a version of transitive dependency where vulnerability is fixed.
**In some cases, Remediation PR cannot be created automatically for a vulnerability despite the availability of remediation
Details
CVE-2026-54904
Vulnerable Library - concurrent-ruby-1.3.6.gem
Modern concurrency tools including agents, futures, promises, thread pools, actors, supervisors, and more.
Inspired by Erlang, Clojure, Go, JavaScript, actors, and classic concurrency patterns.
Library home page: https://rubygems.org/gems/concurrent-ruby-1.3.6.gem
Path to dependency file: /Gemfile.lock
Path to vulnerable library: /tmp/containerbase/cache/.ruby/cache/concurrent-ruby-1.3.6.gem
Dependency Hierarchy:
- jekyll-4.4.1.gem (Root Library)
- i18n-1.14.8.gem
- ❌ concurrent-ruby-1.3.6.gem (Vulnerable Library)
Found in base branch: main
Vulnerability Details
Summary "Concurrent::AtomicReference#update" can enter a permanent busy retry loop when the current value is "Float::NAN". The issue is caused by the interaction between: - "AtomicReference#update", which retries until "compare_and_set(old_value, new_value)" succeeds. - Numeric "compare_and_set", which checks "old == old_value" before attempting the underlying atomic swap. - Ruby NaN semantics, where "Float::NAN == Float::NAN" is always "false". As a result, once an "AtomicReference" contains "Float::NAN", calling "#update" repeatedly evaluates the caller's block and never returns. In services that store externally derived numeric values in an "AtomicReference", this can cause CPU exhaustion or permanent request/job hangs. Version Software: concurrent-ruby Version: 1.3.6 Commit: 7a1b78941c081106c20a9ca0144ac73a48d254ab Details "AtomicReference#update" retries until "compare_and_set" returns true: def update true until compare_and_set(old_value = get, new_value = yield(old_value)) new_value end For numeric expected values, "compare_and_set" uses numeric equality before attempting the underlying atomic compare-and-set: def compare_and_set(old_value, new_value) if old_value.kind_of? Numeric while true old = get return false unless old.kind_of? Numeric return false unless old == old_value result = _compare_and_set(old, new_value) return result if result end else _compare_and_set(old_value, new_value) end end When the stored value is "Float::NAN", "old_value = get" returns NaN. The later comparison "old == old_value" is false because NaN is not equal to itself. "compare_and_set" therefore returns false every time. "AtomicReference#update" treats that as a failed concurrent update and retries forever. This is reachable through the public "Concurrent::AtomicReference" API and does not require native extensions or undefined behavior. PoC #!/usr/bin/env ruby frozen_string_literal: true require 'concurrent/atomic/atomic_reference' require 'concurrent/version' puts "ruby=#{RUBY_DESCRIPTION}" puts "concurrent_ruby_version=#{Concurrent::VERSION}" puts "poc=AtomicReference#update livelock when current value is Float::NAN" ref = Concurrent::AtomicReference.new(Float::NAN) attempts = 0 finished = false worker = Thread.new do ref.update do |_old_value| attempts += 1 0.0 end finished = true end sleep 0.25 puts "nan_update_attempts_after_250ms=#{attempts}" puts "nan_update_finished=#{finished}" puts "nan_update_worker_alive=#{worker.alive?}" if worker.alive? && !finished && attempts > 1000 puts 'result=REPRODUCED busy retry loop; update did not complete' else puts 'result=NOT_REPRODUCED' end worker.kill worker.join control = Concurrent::AtomicReference.new(1.0) control_attempts = 0 control_result = control.update do |old_value| control_attempts += 1 old_value + 1.0 end puts "control_update_result=#{control_result.inspect}" puts "control_update_attempts=#{control_attempts}" puts "control_update_final_value=#{control.value.inspect}" Log evidence ruby=ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin25] concurrent_ruby_version=1.3.6 poc=AtomicReference#update livelock when current value is Float::NAN nan_update_attempts_after_250ms=1926016 nan_update_finished=false nan_update_worker_alive=true result=REPRODUCED busy retry loop; update did not complete control_update_result=2.0 control_update_attempts=1 control_update_final_value=2.0 Impact This is an application-level denial of service issue. If an application stores externally derived numeric data in a "Concurrent::AtomicReference", an attacker or faulty upstream data source may be able to cause the stored value to become "Float::NAN". Any later call to "AtomicReference#update" on that reference will spin indefinitely, repeatedly executing the update block and consuming CPU. Credit Pranjali Thakur - depthfirst ("depthfirst.com" (http://depthfirst.com))
Publish Date: 2026-06-19
URL: CVE-2026-54904
CVSS 3 Score Details (7.5)
Base Score Metrics:
- Exploitability Metrics:
- Attack Vector: Network
- Attack Complexity: Low
- Privileges Required: None
- User Interaction: None
- Scope: Unchanged
- Impact Metrics:
- Confidentiality Impact: None
- Integrity Impact: None
- Availability Impact: High
For more information on CVSS3 Scores, click here.
Suggested Fix
Type: Upgrade version
Origin: GHSA-h8w8-99g7-qmvj
Release Date: 2026-06-19
Fix Resolution: concurrent-ruby - 1.3.7
Step up your Open Source Security Game with Mend here
CVE-2026-54905
Vulnerable Library - concurrent-ruby-1.3.6.gem
Modern concurrency tools including agents, futures, promises, thread pools, actors, supervisors, and more.
Inspired by Erlang, Clojure, Go, JavaScript, actors, and classic concurrency patterns.
Library home page: https://rubygems.org/gems/concurrent-ruby-1.3.6.gem
Path to dependency file: /Gemfile.lock
Path to vulnerable library: /tmp/containerbase/cache/.ruby/cache/concurrent-ruby-1.3.6.gem
Dependency Hierarchy:
- jekyll-4.4.1.gem (Root Library)
- i18n-1.14.8.gem
- ❌ concurrent-ruby-1.3.6.gem (Vulnerable Library)
Found in base branch: main
Vulnerability Details
Summary "Concurrent::ReentrantReadWriteLock" can incorrectly grant a write lock after one thread acquires the read lock 32,768 times. The lock stores a thread's local read and write hold counts in one integer. The low 15 bits are used for the read hold count, and bit 15 is used as "WRITE_LOCK_HELD". After 32,768 reentrant read acquisitions, the local read count crosses into the write-lock bit. "try_write_lock" then treats the thread as already holding a write lock and returns "true" without setting the global "RUNNING_WRITER" bit. This breaks the core mutual-exclusion guarantee: the caller is told it has a write lock, but other threads can still hold or acquire read locks at the same time. Version Software: concurrent-ruby Version: 1.3.6 Commit: 7a1b78941c081106c20a9ca0144ac73a48d254ab Details The implementation uses a shared counter to track global readers/writers and a per-thread local counter to support reentrancy: READER_BITS = 15 WRITER_BITS = 14 WAITING_WRITER = 1 << READER_BITS RUNNING_WRITER = 1 << (READER_BITS + WRITER_BITS) MAX_READERS = WAITING_WRITER - 1 MAX_WRITERS = RUNNING_WRITER - MAX_READERS - 1 WRITE_LOCK_HELD = 1 << READER_BITS READ_LOCK_MASK = WRITE_LOCK_HELD - 1 WRITE_LOCK_MASK = MAX_WRITERS When a thread already holds a lock, "acquire_read_lock" increments "@HeldCount": if (held = @HeldCount.value) > 0 if held & READ_LOCK_MASK == 0 @Counter.update { |c| c + 1 } end @HeldCount.value = held + 1 return true end After 32,768 read acquisitions, the per-thread held count becomes "32768", which is equal to "WRITE_LOCK_HELD". Then "try_write_lock" returns success through its "already have a write lock" branch: def try_write_lock if (held = @HeldCount.value) >= WRITE_LOCK_HELD @HeldCount.value = held + WRITE_LOCK_HELD return true else # normal global writer acquisition path end end This branch does not set the global "RUNNING_WRITER" bit. Other threads therefore do not observe an active writer and can continue holding or acquiring read locks while the caller believes it owns the write lock. PoC #!/usr/bin/env ruby frozen_string_literal: true require 'concurrent/atomic/reentrant_read_write_lock' require 'concurrent/version' require 'thread' def wait_for_queue(queue, timeout_seconds) deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout_seconds loop do return queue.pop(true) rescue ThreadError return nil if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline sleep 0.001 end end puts "ruby=#{RUBY_DESCRIPTION}" puts "concurrent_ruby_version=#{Concurrent::VERSION}" puts "poc=ReentrantReadWriteLock read-depth overflow grants write lock without exclusivity" lock = Concurrent::ReentrantReadWriteLock.new other_reader_ready = Queue.new other_reader_stop = Queue.new other_reader = Thread.new do lock.acquire_read_lock other_reader_ready << :held other_reader_stop.pop end wait_for_queue(other_reader_ready, 1) puts "other_thread_holds_read_lock=true" depth = Concurrent::ReentrantReadWriteLock::WRITE_LOCK_HELD depth.times { lock.acquire_read_lock } held_count = lock.instance_eval { @HeldCount.value } counter_before = lock.instance_eval { @Counter.value } puts "main_thread_read_acquisitions=#{depth}" puts "main_thread_held_count=#{held_count}" puts "counter_before_try_write=#{counter_before}" puts "running_writer_bit_before=#{(counter_before & Concurrent::ReentrantReadWriteLock::RUNNING_WRITER) != 0}" write_granted = lock.try_write_lock counter_after = lock.instance_eval { @Counter.value } puts "try_write_lock_returned=#{write_granted}" puts "counter_after_try_write=#{counter_after}" puts "running_writer_bit_after=#{(counter_after & Concurrent::ReentrantReadWriteLock::RUNNING_WRITER) != 0}" third_reader_ready = Queue.new third_reader = Thread.new do lock.acquire_read_lock third_reader_ready << :acquired end third_reader_acquired = wait_for_queue(third_reader_ready, 0.25) == :acquired puts "new_reader_acquired_while_write_claimed=#{third_reader_acquired}" if write_granted && third_reader_acquired && (counter_after & Concurrent::ReentrantReadWriteLock::RUNNING_WRITER).zero? puts 'result=REPRODUCED write lock granted without setting global writer state' else puts 'result=NOT_REPRODUCED' end third_reader.kill other_reader_stop << :stop other_reader.kill Log evidence ruby=ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin25] concurrent_ruby_version=1.3.6 poc=ReentrantReadWriteLock read-depth overflow grants write lock without exclusivity other_thread_holds_read_lock=true main_thread_read_acquisitions=32768 main_thread_held_count=32768 counter_before_try_write=2 running_writer_bit_before=false try_write_lock_returned=true counter_after_try_write=2 running_writer_bit_after=false new_reader_acquired_while_write_claimed=true result=REPRODUCED write lock granted without setting global writer state Impact This breaks the write-lock exclusivity guarantee. After the overflow, a thread can be told it has acquired the write lock while other threads can still hold or acquire read locks, allowing races and inconsistent reads of protected mutable state. Credit Pranjali Thakur - depthfirst ("depthfirst.com" (http://depthfirst.com))
Publish Date: 2026-06-19
URL: CVE-2026-54905
CVSS 3 Score Details (5.3)
Base Score Metrics:
- Exploitability Metrics:
- Attack Vector: Local
- Attack Complexity: Low
- Privileges Required: Low
- User Interaction: None
- Scope: Unchanged
- Impact Metrics:
- Confidentiality Impact: Low
- Integrity Impact: Low
- Availability Impact: Low
For more information on CVSS3 Scores, click here.
Suggested Fix
Type: Upgrade version
Origin: GHSA-wv3x-4vxv-whpp
Release Date: 2026-06-19
Fix Resolution: concurrent-ruby - 1.3.7
Step up your Open Source Security Game with Mend here
CVE-2026-54906
Vulnerable Library - concurrent-ruby-1.3.6.gem
Modern concurrency tools including agents, futures, promises, thread pools, actors, supervisors, and more.
Inspired by Erlang, Clojure, Go, JavaScript, actors, and classic concurrency patterns.
Library home page: https://rubygems.org/gems/concurrent-ruby-1.3.6.gem
Path to dependency file: /Gemfile.lock
Path to vulnerable library: /tmp/containerbase/cache/.ruby/cache/concurrent-ruby-1.3.6.gem
Dependency Hierarchy:
- jekyll-4.4.1.gem (Root Library)
- i18n-1.14.8.gem
- ❌ concurrent-ruby-1.3.6.gem (Vulnerable Library)
Found in base branch: main
Vulnerability Details
Summary "Concurrent::ReadWriteLock#release_write_lock" does not verify that the calling thread acquired the write lock. Any thread with access to the lock object can release an active write lock held by another thread. A second writer can then enter its critical section while the first writer is still running. "Concurrent::ReadWriteLock#release_read_lock" also decrements the shared counter even when no read lock is held. Calling it on a fresh lock changes the counter from "0" to "-1", after which normal read acquisition raises "Concurrent::ResourceLimitError". This is a synchronization correctness issue in the public "Concurrent::ReadWriteLock" API. It should not be framed as an authorization bypass; the lock is an in-process concurrency primitive, not an access-control boundary. Version Software: concurrent-ruby Version: 1.3.6 Commit: 7a1b78941c081106c20a9ca0144ac73a48d254ab Details "release_write_lock" checks only whether the global counter indicates that a writer is running. It does not track or verify ownership: def release_write_lock return true unless running_writer? c = @Counter.update { |counter| counter - RUNNING_WRITER } @ReadLock.broadcast @WriteLock.signal if waiting_writers(c) > 0 true 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_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: 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))
Publish Date: 2026-06-19
URL: CVE-2026-54906
CVSS 3 Score Details (4.0)
Base Score Metrics:
- Exploitability Metrics:
- Attack Vector: Local
- Attack Complexity: High
- Privileges Required: None
- User Interaction: None
- Scope: Unchanged
- Impact Metrics:
- Confidentiality Impact: None
- Integrity Impact: Low
- Availability Impact: Low
For more information on CVSS3 Scores, click here.
Suggested Fix
Type: Upgrade version
Origin: GHSA-6wx8-w4f5-wwcr
Release Date: 2026-06-19
Fix Resolution: concurrent-ruby - 1.3.7
Step up your Open Source Security Game with Mend here
Path to dependency file: /Gemfile.lock
Path to vulnerable library: /tmp/containerbase/cache/.ruby/cache/concurrent-ruby-1.3.6.gem
Vulnerabilities
*For some transitive vulnerabilities, there is no version of direct dependency with a fix. Check the "Details" section below to see if there is a version of transitive dependency where vulnerability is fixed.
**In some cases, Remediation PR cannot be created automatically for a vulnerability despite the availability of remediation
Details
Vulnerable Library - concurrent-ruby-1.3.6.gem
Modern concurrency tools including agents, futures, promises, thread pools, actors, supervisors, and more. Inspired by Erlang, Clojure, Go, JavaScript, actors, and classic concurrency patterns.
Library home page: https://rubygems.org/gems/concurrent-ruby-1.3.6.gem
Path to dependency file: /Gemfile.lock
Path to vulnerable library: /tmp/containerbase/cache/.ruby/cache/concurrent-ruby-1.3.6.gem
Dependency Hierarchy:
Found in base branch: main
Vulnerability Details
Summary "Concurrent::AtomicReference#update" can enter a permanent busy retry loop when the current value is "Float::NAN". The issue is caused by the interaction between: - "AtomicReference#update", which retries until "compare_and_set(old_value, new_value)" succeeds. - Numeric "compare_and_set", which checks "old == old_value" before attempting the underlying atomic swap. - Ruby NaN semantics, where "Float::NAN == Float::NAN" is always "false". As a result, once an "AtomicReference" contains "Float::NAN", calling "#update" repeatedly evaluates the caller's block and never returns. In services that store externally derived numeric values in an "AtomicReference", this can cause CPU exhaustion or permanent request/job hangs. Version Software: concurrent-ruby Version: 1.3.6 Commit: 7a1b78941c081106c20a9ca0144ac73a48d254ab Details "AtomicReference#update" retries until "compare_and_set" returns true: def update true until compare_and_set(old_value = get, new_value = yield(old_value)) new_value end For numeric expected values, "compare_and_set" uses numeric equality before attempting the underlying atomic compare-and-set: def compare_and_set(old_value, new_value) if old_value.kind_of? Numeric while true old = get return false unless old.kind_of? Numeric return false unless old == old_value result = _compare_and_set(old, new_value) return result if result end else _compare_and_set(old_value, new_value) end end When the stored value is "Float::NAN", "old_value = get" returns NaN. The later comparison "old == old_value" is false because NaN is not equal to itself. "compare_and_set" therefore returns false every time. "AtomicReference#update" treats that as a failed concurrent update and retries forever. This is reachable through the public "Concurrent::AtomicReference" API and does not require native extensions or undefined behavior. PoC #!/usr/bin/env ruby frozen_string_literal: true require 'concurrent/atomic/atomic_reference' require 'concurrent/version' puts "ruby=#{RUBY_DESCRIPTION}" puts "concurrent_ruby_version=#{Concurrent::VERSION}" puts "poc=AtomicReference#update livelock when current value is Float::NAN" ref = Concurrent::AtomicReference.new(Float::NAN) attempts = 0 finished = false worker = Thread.new do ref.update do |_old_value| attempts += 1 0.0 end finished = true end sleep 0.25 puts "nan_update_attempts_after_250ms=#{attempts}" puts "nan_update_finished=#{finished}" puts "nan_update_worker_alive=#{worker.alive?}" if worker.alive? && !finished && attempts > 1000 puts 'result=REPRODUCED busy retry loop; update did not complete' else puts 'result=NOT_REPRODUCED' end worker.kill worker.join control = Concurrent::AtomicReference.new(1.0) control_attempts = 0 control_result = control.update do |old_value| control_attempts += 1 old_value + 1.0 end puts "control_update_result=#{control_result.inspect}" puts "control_update_attempts=#{control_attempts}" puts "control_update_final_value=#{control.value.inspect}" Log evidence ruby=ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin25] concurrent_ruby_version=1.3.6 poc=AtomicReference#update livelock when current value is Float::NAN nan_update_attempts_after_250ms=1926016 nan_update_finished=false nan_update_worker_alive=true result=REPRODUCED busy retry loop; update did not complete control_update_result=2.0 control_update_attempts=1 control_update_final_value=2.0 Impact This is an application-level denial of service issue. If an application stores externally derived numeric data in a "Concurrent::AtomicReference", an attacker or faulty upstream data source may be able to cause the stored value to become "Float::NAN". Any later call to "AtomicReference#update" on that reference will spin indefinitely, repeatedly executing the update block and consuming CPU. Credit Pranjali Thakur - depthfirst ("depthfirst.com" (http://depthfirst.com))
Publish Date: 2026-06-19
URL: CVE-2026-54904
CVSS 3 Score Details (7.5)
Base Score Metrics:
- Exploitability Metrics:
- Attack Vector: Network
- Attack Complexity: Low
- Privileges Required: None
- User Interaction: None
- Scope: Unchanged
- Impact Metrics:
- Confidentiality Impact: None
- Integrity Impact: None
- Availability Impact: High
For more information on CVSS3 Scores, click here.Suggested Fix
Type: Upgrade version
Origin: GHSA-h8w8-99g7-qmvj
Release Date: 2026-06-19
Fix Resolution: concurrent-ruby - 1.3.7
Step up your Open Source Security Game with Mend here
Vulnerable Library - concurrent-ruby-1.3.6.gem
Modern concurrency tools including agents, futures, promises, thread pools, actors, supervisors, and more. Inspired by Erlang, Clojure, Go, JavaScript, actors, and classic concurrency patterns.
Library home page: https://rubygems.org/gems/concurrent-ruby-1.3.6.gem
Path to dependency file: /Gemfile.lock
Path to vulnerable library: /tmp/containerbase/cache/.ruby/cache/concurrent-ruby-1.3.6.gem
Dependency Hierarchy:
Found in base branch: main
Vulnerability Details
Summary "Concurrent::ReentrantReadWriteLock" can incorrectly grant a write lock after one thread acquires the read lock 32,768 times. The lock stores a thread's local read and write hold counts in one integer. The low 15 bits are used for the read hold count, and bit 15 is used as "WRITE_LOCK_HELD". After 32,768 reentrant read acquisitions, the local read count crosses into the write-lock bit. "try_write_lock" then treats the thread as already holding a write lock and returns "true" without setting the global "RUNNING_WRITER" bit. This breaks the core mutual-exclusion guarantee: the caller is told it has a write lock, but other threads can still hold or acquire read locks at the same time. Version Software: concurrent-ruby Version: 1.3.6 Commit: 7a1b78941c081106c20a9ca0144ac73a48d254ab Details The implementation uses a shared counter to track global readers/writers and a per-thread local counter to support reentrancy: READER_BITS = 15 WRITER_BITS = 14 WAITING_WRITER = 1 << READER_BITS RUNNING_WRITER = 1 << (READER_BITS + WRITER_BITS) MAX_READERS = WAITING_WRITER - 1 MAX_WRITERS = RUNNING_WRITER - MAX_READERS - 1 WRITE_LOCK_HELD = 1 << READER_BITS READ_LOCK_MASK = WRITE_LOCK_HELD - 1 WRITE_LOCK_MASK = MAX_WRITERS When a thread already holds a lock, "acquire_read_lock" increments "@HeldCount": if (held = @HeldCount.value) > 0 if held & READ_LOCK_MASK == 0 @Counter.update { |c| c + 1 } end @HeldCount.value = held + 1 return true end After 32,768 read acquisitions, the per-thread held count becomes "32768", which is equal to "WRITE_LOCK_HELD". Then "try_write_lock" returns success through its "already have a write lock" branch: def try_write_lock if (held = @HeldCount.value) >= WRITE_LOCK_HELD @HeldCount.value = held + WRITE_LOCK_HELD return true else # normal global writer acquisition path end end This branch does not set the global "RUNNING_WRITER" bit. Other threads therefore do not observe an active writer and can continue holding or acquiring read locks while the caller believes it owns the write lock. PoC #!/usr/bin/env ruby frozen_string_literal: true require 'concurrent/atomic/reentrant_read_write_lock' require 'concurrent/version' require 'thread' def wait_for_queue(queue, timeout_seconds) deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout_seconds loop do return queue.pop(true) rescue ThreadError return nil if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline sleep 0.001 end end puts "ruby=#{RUBY_DESCRIPTION}" puts "concurrent_ruby_version=#{Concurrent::VERSION}" puts "poc=ReentrantReadWriteLock read-depth overflow grants write lock without exclusivity" lock = Concurrent::ReentrantReadWriteLock.new other_reader_ready = Queue.new other_reader_stop = Queue.new other_reader = Thread.new do lock.acquire_read_lock other_reader_ready << :held other_reader_stop.pop end wait_for_queue(other_reader_ready, 1) puts "other_thread_holds_read_lock=true" depth = Concurrent::ReentrantReadWriteLock::WRITE_LOCK_HELD depth.times { lock.acquire_read_lock } held_count = lock.instance_eval { @HeldCount.value } counter_before = lock.instance_eval { @Counter.value } puts "main_thread_read_acquisitions=#{depth}" puts "main_thread_held_count=#{held_count}" puts "counter_before_try_write=#{counter_before}" puts "running_writer_bit_before=#{(counter_before & Concurrent::ReentrantReadWriteLock::RUNNING_WRITER) != 0}" write_granted = lock.try_write_lock counter_after = lock.instance_eval { @Counter.value } puts "try_write_lock_returned=#{write_granted}" puts "counter_after_try_write=#{counter_after}" puts "running_writer_bit_after=#{(counter_after & Concurrent::ReentrantReadWriteLock::RUNNING_WRITER) != 0}" third_reader_ready = Queue.new third_reader = Thread.new do lock.acquire_read_lock third_reader_ready << :acquired end third_reader_acquired = wait_for_queue(third_reader_ready, 0.25) == :acquired puts "new_reader_acquired_while_write_claimed=#{third_reader_acquired}" if write_granted && third_reader_acquired && (counter_after & Concurrent::ReentrantReadWriteLock::RUNNING_WRITER).zero? puts 'result=REPRODUCED write lock granted without setting global writer state' else puts 'result=NOT_REPRODUCED' end third_reader.kill other_reader_stop << :stop other_reader.kill Log evidence ruby=ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin25] concurrent_ruby_version=1.3.6 poc=ReentrantReadWriteLock read-depth overflow grants write lock without exclusivity other_thread_holds_read_lock=true main_thread_read_acquisitions=32768 main_thread_held_count=32768 counter_before_try_write=2 running_writer_bit_before=false try_write_lock_returned=true counter_after_try_write=2 running_writer_bit_after=false new_reader_acquired_while_write_claimed=true result=REPRODUCED write lock granted without setting global writer state Impact This breaks the write-lock exclusivity guarantee. After the overflow, a thread can be told it has acquired the write lock while other threads can still hold or acquire read locks, allowing races and inconsistent reads of protected mutable state. Credit Pranjali Thakur - depthfirst ("depthfirst.com" (http://depthfirst.com))
Publish Date: 2026-06-19
URL: CVE-2026-54905
CVSS 3 Score Details (5.3)
Base Score Metrics:
- Exploitability Metrics:
- Attack Vector: Local
- Attack Complexity: Low
- Privileges Required: Low
- User Interaction: None
- Scope: Unchanged
- Impact Metrics:
- Confidentiality Impact: Low
- Integrity Impact: Low
- Availability Impact: Low
For more information on CVSS3 Scores, click here.Suggested Fix
Type: Upgrade version
Origin: GHSA-wv3x-4vxv-whpp
Release Date: 2026-06-19
Fix Resolution: concurrent-ruby - 1.3.7
Step up your Open Source Security Game with Mend here
Vulnerable Library - concurrent-ruby-1.3.6.gem
Modern concurrency tools including agents, futures, promises, thread pools, actors, supervisors, and more. Inspired by Erlang, Clojure, Go, JavaScript, actors, and classic concurrency patterns.
Library home page: https://rubygems.org/gems/concurrent-ruby-1.3.6.gem
Path to dependency file: /Gemfile.lock
Path to vulnerable library: /tmp/containerbase/cache/.ruby/cache/concurrent-ruby-1.3.6.gem
Dependency Hierarchy:
Found in base branch: main
Vulnerability Details
Summary "Concurrent::ReadWriteLock#release_write_lock" does not verify that the calling thread acquired the write lock. Any thread with access to the lock object can release an active write lock held by another thread. A second writer can then enter its critical section while the first writer is still running. "Concurrent::ReadWriteLock#release_read_lock" also decrements the shared counter even when no read lock is held. Calling it on a fresh lock changes the counter from "0" to "-1", after which normal read acquisition raises "Concurrent::ResourceLimitError". This is a synchronization correctness issue in the public "Concurrent::ReadWriteLock" API. It should not be framed as an authorization bypass; the lock is an in-process concurrency primitive, not an access-control boundary. Version Software: concurrent-ruby Version: 1.3.6 Commit: 7a1b78941c081106c20a9ca0144ac73a48d254ab Details "release_write_lock" checks only whether the global counter indicates that a writer is running. It does not track or verify ownership: def release_write_lock return true unless running_writer? c = @Counter.update { |counter| counter - RUNNING_WRITER } @ReadLock.broadcast @WriteLock.signal if waiting_writers(c) > 0 true 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_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: 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))
Publish Date: 2026-06-19
URL: CVE-2026-54906
CVSS 3 Score Details (4.0)
Base Score Metrics:
- Exploitability Metrics:
- Attack Vector: Local
- Attack Complexity: High
- Privileges Required: None
- User Interaction: None
- Scope: Unchanged
- Impact Metrics:
- Confidentiality Impact: None
- Integrity Impact: Low
- Availability Impact: Low
For more information on CVSS3 Scores, click here.Suggested Fix
Type: Upgrade version
Origin: GHSA-6wx8-w4f5-wwcr
Release Date: 2026-06-19
Fix Resolution: concurrent-ruby - 1.3.7
Step up your Open Source Security Game with Mend here