puma / puma

A Ruby/Rack web server built for parallelism

Home Page:https://puma.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Puma 6.0.0 does not close the response bodies of hijacked requests

aymeric-ledorze opened this issue · comments

Describe the bug

When upgrading my rails application to puma 6, I noticed that the request hang for a noticeable amount of time every time I modify one of my views and reload my app. After a lot of digging, I concluded that the issue is due to a request opening a websocket for ActionCable that opens a read lock (in a rack middleware looking for changes in the view files) but never releases it. This lock will eventually be released when ActiveSupport notices that the previous request did not properly close, and completes it.

The request opening the websocket is hijacked and therefore, the close method is never called on its body. I assume that this should not happen, based on this comment in the previous version:

puma/lib/puma/request.rb

Lines 190 to 192 in 1b6b8ad

# Whatever happens, we MUST call `close` on the response body.
# Otherwise Rack::BodyProxy callbacks may not fire and lead to various state leaks
res_body.close if res_body.respond_to? :close

So I wonder if this issue reopens CVE-2022-23634. Moreover, I wonder if there is not an issue with these lines, where the old value of res_body is lost without first being closed:

puma/lib/puma/request.rb

Lines 107 to 116 in 03ed6c8

rescue ThreadPool::ForceShutdown => e
@log_writer.unknown_error e, client, "Rack app"
@log_writer.log "Detected force shutdown of a thread"
status, headers, res_body = lowlevel_error(e, env, 503)
rescue Exception => e
@log_writer.unknown_error e, client, "Rack app"
status, headers, res_body = lowlevel_error(e, env, 500)
end

To Reproduce

I defined this simple application:

require 'rack'
available = true
run ->(env) {
  if available
    available = false
    env['rack.hijack'].call
    [200, { 'Content-Type' => 'text/plain' }, ::Rack::BodyProxy.new(['Hello World']){ available = true }]
  else
    [500, { 'Content-Type' => 'text/plain' }, ['Unavailable']]
  end
}

with this simple Dockerfile:

FROM ruby:3.1.2
RUN gem install puma rack
CMD ["puma", "hello.ru"]

That I run with docker run --rm -it -v ``pwd``/hello.ru:/hello.ru -p 9292:9292 puma_test.

Expected behavior

On the first request, a resource is consumed but should be released even if the request is hijacked. On the next request, we should get "Hello World".

curl http://127.0.0.1:9292 # hangs, this is expected
curl http://127.0.0.1:9292 # should return "Hello World" but returns "Unaivalable"

Working in this, writing a test...

Linking #2896 that touched on the code linked above (note that the diff for lib/puma/request.rb is big so you have to click to see it in the PR diff)

@aymeric-ledorze Please see PR #3002 and the test commit 36c3cb2ab2.

I believe this properly closes the app body, including when lowlevel_error is called.

Came across this issue too and can confirm the PR/commit resolves the issue 👍🏾

Hey @nateberkopec, are you planning on releasing a new version of Puma including this fix?

I still have puma version-pinned at 5.6.5 because 6.0 brought my Rails app that is using Active Cable down with just a few clicks. A new release would be highly welcomed!

I am so happy to find this thread! I was just banging my head against this very issue! Thank you all for being so quick to document and fix - open source rules!