GHSA-H8W8-99G7-QMVJ

Vulnerability from github – Published: 2026-06-19 20:47 – Updated: 2026-06-19 20:47
VLAI
Summary
Concurrent Ruby : `AtomicReference#update` livelocks when the stored value is `Float::NAN`
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)

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "RubyGems",
        "name": "concurrent-ruby"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.3.7"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-54904"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-835"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-19T20:47:35Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "### Summary\n`Concurrent::AtomicReference#update` can enter a permanent busy retry loop when the current value is `Float::NAN`.\n\nThe issue is caused by the interaction between:\n- `AtomicReference#update`, which retries until `compare_and_set(old_value, new_value)` succeeds.\n- Numeric `compare_and_set`, which checks `old == old_value` before attempting the underlying atomic swap.\n- Ruby NaN semantics, where `Float::NAN == Float::NAN` is always `false`.\n\nAs a result, once an `AtomicReference` contains `Float::NAN`, calling `#update` repeatedly evaluates the caller\u0027s 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.\n\n### Version\nSoftware: concurrent-ruby\nVersion: 1.3.6\nCommit: 7a1b78941c081106c20a9ca0144ac73a48d254ab\n### Details\n\n`AtomicReference#update` retries until `compare_and_set` returns true:\n\n```ruby\ndef update\n  true until compare_and_set(old_value = get, new_value = yield(old_value))\n  new_value\nend\n```\n\nFor numeric expected values, `compare_and_set` uses numeric equality before attempting the underlying atomic compare-and-set:\n\n```ruby\ndef compare_and_set(old_value, new_value)\n  if old_value.kind_of? Numeric\n    while true\n      old = get\n\n      return false unless old.kind_of? Numeric\n      return false unless old == old_value\n\n      result = _compare_and_set(old, new_value)\n      return result if result\n    end\n  else\n    _compare_and_set(old_value, new_value)\n  end\nend\n```\n\nWhen 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.\n\nThis is reachable through the public `Concurrent::AtomicReference` API and does not require native extensions or undefined behavior.\n\n### PoC\n\n```ruby\n#!/usr/bin/env ruby\n# frozen_string_literal: true\n\nrequire \u0027concurrent/atomic/atomic_reference\u0027\nrequire \u0027concurrent/version\u0027\n\nputs \"ruby=#{RUBY_DESCRIPTION}\"\nputs \"concurrent_ruby_version=#{Concurrent::VERSION}\"\nputs \"poc=AtomicReference#update livelock when current value is Float::NAN\"\n\nref = Concurrent::AtomicReference.new(Float::NAN)\nattempts = 0\nfinished = false\n\nworker = Thread.new do\n  ref.update do |_old_value|\n    attempts += 1\n    0.0\n  end\n  finished = true\nend\n\nsleep 0.25\n\nputs \"nan_update_attempts_after_250ms=#{attempts}\"\nputs \"nan_update_finished=#{finished}\"\nputs \"nan_update_worker_alive=#{worker.alive?}\"\n\nif worker.alive? \u0026\u0026 !finished \u0026\u0026 attempts \u003e 1000\n  puts \u0027result=REPRODUCED busy retry loop; update did not complete\u0027\nelse\n  puts \u0027result=NOT_REPRODUCED\u0027\nend\n\nworker.kill\nworker.join\n\ncontrol = Concurrent::AtomicReference.new(1.0)\ncontrol_attempts = 0\ncontrol_result = control.update do |old_value|\n  control_attempts += 1\n  old_value + 1.0\nend\n\nputs \"control_update_result=#{control_result.inspect}\"\nputs \"control_update_attempts=#{control_attempts}\"\nputs \"control_update_final_value=#{control.value.inspect}\"\n```\n### Log evidence\n```text\nruby=ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin25]\nconcurrent_ruby_version=1.3.6\npoc=AtomicReference#update livelock when current value is Float::NAN\nnan_update_attempts_after_250ms=1926016\nnan_update_finished=false\nnan_update_worker_alive=true\nresult=REPRODUCED busy retry loop; update did not complete\ncontrol_update_result=2.0\ncontrol_update_attempts=1\ncontrol_update_final_value=2.0\n```\n\n### Impact\nThis 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.\n\n### Credit\nPranjali Thakur - depthfirst ([depthfirst.com](\u003chttp://depthfirst.com\u003e))",
  "id": "GHSA-h8w8-99g7-qmvj",
  "modified": "2026-06-19T20:47:35Z",
  "published": "2026-06-19T20:47:35Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/ruby-concurrency/concurrent-ruby/security/advisories/GHSA-h8w8-99g7-qmvj"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/ruby-concurrency/concurrent-ruby"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Concurrent Ruby : `AtomicReference#update` livelocks when the stored value is `Float::NAN`"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…