Nov 13, 2026·10 min read·2 visits
A resource lock-contention deadlock (CWE-833) in Ruby on Rails' ActionController::Live::Buffer allows unauthenticated remote attackers to permanently hang web server worker threads and controller producer threads by ceasing TCP reads on streaming endpoints. Upgrade to Rails 8.1.4.1 or 8.0.5.2.
CVE-2026-126072 (GHSA-rwvh-fw6r-pwvp) is a concurrency deadlock vulnerability in the Ruby on Rails Action Pack framework affecting applications that utilize ActionController::Live response streaming. The defect occurs due to improper synchronization within ActionController::Live::Buffer when handling full bounded queues during stream closure and connection abortion. An unauthenticated remote attacker can trigger this condition by stalling TCP socket reads on streaming endpoints—including Active Storage proxy mode—resulting in permanent worker thread exhaustion and application-wide denial of service.
Ruby on Rails provides the ActionController::Live module within the Action Pack (actionpack) gem to support persistent HTTP response streaming. This capability allows controller actions to transmit data chunks incrementally over an open HTTP connection rather than buffering the entire payload in memory prior to transmission. Beyond custom streaming controllers, core Rails features rely directly on this mechanism, including ActionController::DataStreaming#send_stream and Active Storage's proxy delivery mode (ActiveStorage::Blobs::ProxyController).
To decouple controller execution from network I/O latency, ActionController::Live operates using a multi-threaded producer-consumer architecture. A spawned producer thread executes the controller action and pushes data chunks into an internal buffer (ActionController::Live::Buffer), while the underlying Rack web server thread (such as a Puma worker thread) acts as the consumer, reading chunks from the buffer and writing them to the client TCP socket.
CVE-2026-126072 (also tracked as GHSA-rwvh-fw6r-pwvp) is a synchronization deadlock (CWE-833) in ActionController::Live::Buffer affecting Rails versions prior to 8.0.5.2 and 8.1.4.1. The vulnerability arises when the internal bounded queue reaches capacity due to a slow or stalled client connection, and the producer thread attempts to close the stream while holding the buffer's mutex lock.
When the web server subsequently detects a socket write timeout or client disconnection and invokes the buffer's abort routine, the consumer thread blocks indefinitely waiting to acquire the mutex held by the stalled producer thread. Because neither thread can proceed, both the controller producer thread and the web server worker thread are permanently leaked. Repeatedly triggering this state across concurrent connections exhausts the application server's thread pool, resulting in a complete Denial of Service (DoS).
The root cause of CVE-2026-126072 lies in the interaction between two synchronization primitives inside ActionController::Live::Buffer: a reentrant monitor lock provided by Ruby's MonitorMixin (synchronize) and a bounded thread-safe queue implemented via Thread::SizedQueue (@buf). The SizedQueue enforces backpressure on the producer thread so that if a client reads slowly from the socket, the controller thread pauses once the queue reaches its configured slot limit rather than consuming unbounded memory.
In vulnerable versions of Rails, stream termination is signaled by pushing a nil sentinel object onto @buf inside ActionController::Live::Buffer#close. Crucially, Buffer#close acquires the MonitorMixin lock via synchronize do ... end before executing @buf.push nil. When @buf is already at full capacity because the client has ceased reading from the TCP socket, the @buf.push nil call blocks the producer thread until a consumer thread removes an item from the queue. Because this blocking operation occurs inside the synchronize block, the producer thread retains ownership of the monitor lock while asleep.
While the producer thread is blocked inside Buffer#close, the consumer thread (the web server worker) eventually encounters a socket timeout or TCP reset from the stalled client. To clean up the stream, the web server invokes ActionController::Live::Buffer#abort. The Buffer#abort method is designed to set @aborted = true and flush the queue via @buf.clear, which would normally unblock any waiting producer.
However, Buffer#abort also wraps its logic inside a synchronize do ... end block tied to the same MonitorMixin instance. When the web server thread calls Buffer#abort, it attempts to acquire the monitor lock already held by the producer thread sleeping inside Buffer#close. The producer thread cannot release the monitor lock until @buf is drained or cleared, and the consumer thread cannot clear @buf until it acquires the monitor lock. This circular wait condition fulfills all requirements for a classic resource deadlock.
The vulnerable implementation resides in actionpack/lib/action_controller/metal/live.rb within the ActionController::Live::Buffer class. Prior to the patch, close and abort both relied on the monitor lock while close performed a potentially blocking queue insertion:
# Vulnerable implementation in actionpack/lib/action_controller/metal/live.rb
def close
synchronize do
super
@buf.push nil # Blocks indefinitely if @buf (SizedQueue) is full
@cv.broadcast
end
end
def abort
synchronize do # Blocks waiting for the monitor lock held by #close
@aborted = true
@buf.clear
end
endRails maintainer Jeremy Daer resolved the vulnerability in commits ccbfc09690fef05fdd2a9969c917a71b92cc98fc (for Rails 8.1.4.1) and 27ad18ed5900dee28ba20e9c6fc718ff751fa215 (for Rails 8.0.5.2). The patch eliminates the blocking sentinel push in Buffer#close and replaces it with Ruby's native Thread::Queue#close (@buf.close).
File: actionpack/lib/action_controller/metal/live.rb
@@ -218,7 +218,11 @@ def write(string)
@response.delete_header "Content-Length"
end
- super
+ begin
+ super
+ rescue ClosedQueueError
+ raise IOError, "closed stream" if connected?
+ end
unless connected?
@buf.clear
@@ -240,14 +244,14 @@ def writeln(string)
def close
synchronize do
super
- @buf.push nil
+ @buf.close # Non-blocking queue closure replaces blocking sentinel push
@cv.broadcast
end
end
@@ -255,7 +259,9 @@ def close
def abort
synchronize do
@aborted = true
+ @buf.close # Immediately closes queue and wakes blocked writers/readers
@buf.clear
+ @cv.broadcast
end
endCalling @buf.close on a Thread::SizedQueue transitions the queue state to closed immediately without inserting an element and without waiting for free capacity. Consequently, Buffer#close executes non-blockingly and releases the synchronize monitor lock immediately. Additionally, when @buf.close is called inside Buffer#abort, any producer thread currently blocked inside @buf.push (via Buffer#write) immediately wakes up with a ClosedQueueError exception.
To handle this exception cleanly, the patch wraps the super call inside Buffer#write with a begin ... rescue ClosedQueueError block. If the queue was closed while the client connection was still considered active, it raises IOError, "closed stream"; if the client disconnected (!connected?), it falls through to clear the buffer and raise ActionController::Live::ClientDisconnected. The patch is complete and directly eliminates the circular wait condition across all producer, consumer, and abort code paths.
Exploiting CVE-2026-126072 requires no authentication if the target application exposes a public streaming endpoint or utilizes Active Storage in proxy mode (ActiveStorage::Blobs::ProxyController). In Active Storage proxy mode—configured via config.active_storage.resolve_model_to_route = :rails_storage_proxy or accessed via rails_storage_proxy_path—Rails streams file blobs from backend storage services through the Rails process using send_blob_stream, which is built on ActionController::Live.
To trigger the deadlock on a target endpoint, an attacker performs the following steps:
GET request to a streaming route (e.g., /rails/active_storage/blobs/proxy/... for a file larger than the stream buffer capacity).RCV.WND) of 0. This stalls socket writes on the server once the OS socket send buffer fills.SizedQueue: While the consumer thread is stalled on the socket, the Rails producer thread continues generating chunks until @buf (Thread::SizedQueue) reaches its slot limit and the producer completes the stream payload, entering Buffer#close.RST packet once the producer thread has entered Buffer#close. The web server invokes Buffer#abort, deadlocking both threads.> [!NOTE]
> For the deadlock to occur, the total response payload generated by the controller must be small enough to finish while filling the kernel socket buffer and the SizedQueue, or the producer must enter Buffer#close precisely while @buf is full. If the payload is infinitely large, the producer blocks inside Buffer#write (which does not hold the synchronize lock across @buf.push) rather than Buffer#close.
Because a default Puma deployment typically configures 5 to 16 threads per worker process, an attacker only needs to establish a small number of concurrent connections (e.g., 16 to 64 requests) to permanently lock every worker thread across all Puma cluster processes. Unlike standard slowloris attacks where connections release once the client disconnects or times out, threads deadlocked via CVE-2026-126072 remain hung permanently even after the attacker closes their sockets, requiring a server process restart to recover.
The primary impact of CVE-2026-126072 is a persistent Denial of Service affecting application availability. The vulnerability is rated 5.9 (Medium) under CVSS v3.1 (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H). The High Attack Complexity (AC:H) metric reflects the requirement that the response stream must fill the bounded SizedQueue and call Buffer#close prior to Buffer#abort being invoked, whereas Confidentiality (C:N) and Integrity (I:N) remain unaffected.
When successfully triggered, each exploit cycle permanently removes one web server worker thread and one ActionController::Live thread from service. Because Ruby web servers such as Puma rely on fixed-size thread pools to process all incoming HTTP requests, exhausting these threads prevents the server from answering any further requests—including static routes or non-streaming controller actions.
Furthermore, because the threads are blocked on an internal Ruby MonitorMixin mutex rather than an I/O select loop, standard application-level request timeout gems or socket timeouts cannot free the threads. Unless an external process supervisor or Puma's master process watchdog detects that the entire worker process has stopped responding and terminates it via SIGKILL, the application remains unresponsive indefinitely.
As of November 2026, CVE-2026-126072 has an EPSS score of 0.00411 (35.91st percentile) and is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog. No weaponized public exploit tooling has been observed in the wild, though the concurrency test cases included in the official Rails patch clearly demonstrate the mechanics required to reproduce the hang.
The definitive remediation is to upgrade Ruby on Rails (rails and actionpack gems) to a patched release that replaces blocking sentinel pushes with non-blocking queue closures (@buf.close).
Update your application's Gemfile and execute Bundler to apply the patched version:
# Update Gemfile constraint to >= 8.1.4.1 or >= 8.0.5.2, then run:
bundle update rails actionpackFor environments where an immediate Rails upgrade cannot be deployed, operators can implement several compensating controls to reduce exposure:
:rails_storage_redirect). Redirect mode offloads file streaming directly to cloud object storage (such as Amazon S3 or Google Cloud Storage) via signed URLs, bypassing ActionController::Live entirely:# config/environments/production.rb
config.active_storage.resolve_model_to_route = :rails_storage_redirectEnable Reverse Proxy Response Buffering: Deploy a reverse proxy such as NGINX in front of the Rails application server with proxy_buffering on; enabled. When proxy buffering is active, NGINX reads the entire streamed response from Puma at local loopback/LAN speeds into memory or disk buffers before transmitting it to the remote client. This isolates the Rails SizedQueue from client-side TCP window manipulation, provided the streamed response fits within NGINX's configured proxy_max_temp_file_size.
Configure Process-Level Worker Watchdogs: In clustered Puma deployments, ensure worker_timeout is configured so that the cluster master process forcibly terminates (SIGKILL) and respawns worker processes whose threads become completely unresponsive.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
Action Pack / Ruby on Rails Ruby on Rails | < 8.0.5.2 | 8.0.5.2 |
Action Pack / Ruby on Rails Ruby on Rails | >= 8.1, < 8.1.4.1 | 8.1.4.1 |
| Attribute | Detail |
|---|---|
| Vulnerability Type | CWE-833: Deadlock |
| Attack Vector | Network (HTTP/HTTPS Streaming) |
| CVSS v3.1 Score | 5.9 Medium (AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H) |
| EPSS Score | 0.41% (35.91st Percentile) |
| Impact | Permanent Worker Thread Exhaustion / Denial of Service |
| Affected Component | ActionController::Live::Buffer (actionpack) |
| Patched Versions | Rails 8.1.4.1, Rails 8.0.5.2 |
| CISA KEV Status | Not Listed |
The software contains two or more concurrent execution paths that compete for the same shared resources, and each execution path waits for another execution path to release a resource, causing all execution paths to stop making progress.
CVE-2026-126192 is an algorithmic complexity vulnerability in Soup Sieve (soupsieve), the default CSS selector engine for Beautiful Soup 4 in Python. Evaluating a CSS selector that contains a general sibling combinator (~) nested inside a :has() relational pseudo-class against attacker-controlled HTML markup triggers O(n^2) quadratic evaluation complexity when the right-hand sibling target is absent. An unauthenticated attacker can supply small HTML documents containing thousands of sibling elements to exhaust CPU resources and stall application worker threads.
CVE-2026-126190 (GHSA-mmhr-3c5m-f64f) is an inefficient algorithmic complexity vulnerability (CWE-407) in Soup Sieve, the default CSS selector library integrated into Beautiful Soup 4. In versions prior to 4.0, evaluating CSS selectors that contain descendant combinators (whitespace between compound selectors) executes an unmemoized upward traversal of the document tree. When evaluated against deeply nested HTML or XML documents, the time complexity scales polynomially at O(n^(k+1)), where n is the tree depth and k is the number of descendant combinators. Remote attackers can supply compact, deeply nested markup (1 KB to 4 KB) to trigger prolonged CPU starvation and denial of service in Python applications.
CVE-2026-122006 is a critical vulnerability in the Ruby on Rails Active Storage framework affecting versions prior to 8.0.5.2 and 8.1.4.1. When an application processes uploaded file attachments using the ImageMagick variant processor (:mini_magick), the framework invokes ImageMagick's metadata analyzer without applying a restrictive security policy. An unauthenticated attacker can upload a crafted file containing directives for unrestricted ImageMagick coders, such as Magick Scripting Language (MSL), resulting in arbitrary server-side file read and write operations. In deployments using the Disk storage service, attackers can exfiltrate sensitive application configuration files, including secret_key_base and master encryption keys, which enables remote code execution via signed payload forgery.
CVE-2026-128065 (also tracked as GHSA-gg5j-6742-v827) is an authorization bypass vulnerability affecting the @grpc/grpc-js-xds library in the Node.js gRPC ecosystem. In versions 1.14.0 and 1.14.1, XdsServer acknowledges Route and VirtualHost resources from an xDS control plane while failing to parse, store, or apply RBACPerRoute override configurations. When experimental RBAC is enabled and a broad top-level ALLOW policy is paired with restrictive VirtualHost or Route overrides, the server evaluates requests strictly against the top-level policy, permitting unauthorized RPC execution.
CVE-2026-126188 (GHSA-3f4q-8x5m-76v8) is an information disclosure vulnerability in the Active Storage component of Ruby on Rails prior to versions 8.0.5.2 and 8.1.4.1. When generating previews or extracting metadata from uploaded media files, Active Storage invokes the external command-line tools ffmpeg and ffprobe without specifying a format whitelist. An attacker who can upload a crafted HTTP Live Streaming (HLS) playlist while declaring a standard video or audio MIME type can cause FFmpeg to engage the HLS demuxer, resolve file:// segment URIs, and disclose visual frames or metadata from local media files readable by the Rails application process.
CVE-2026-126206 (GHSA-jcff-cj73-4w4f) is a Cross-Site Scripting (XSS) vulnerability in version 2.25.2 of the Loofah Ruby gem. The vulnerability stems from an incomplete character reference normalization routine in the `Loofah::HTML5::Scrub.allowed_uri?` method, which fails to decode the HTML5 named character reference `/` (representing the solidus character `/`). Consequently, a crafted `data:` URI such as `data:text/html,<script>alert(1)</script>` bypasses mediatype validation by triggering the WHATWG MIME parser fallback to `text/plain`, while web browsers later resolve `/` to `/` and execute the payload as `text/html`.