about summary refs log tree commit homepage
diff options
context:
space:
mode:
-rw-r--r--lib/yahns/proxy_http_response.rb35
-rw-r--r--test/test_proxy_pass.rb10
2 files changed, 42 insertions, 3 deletions
diff --git a/lib/yahns/proxy_http_response.rb b/lib/yahns/proxy_http_response.rb
index af8d8cc..4801008 100644
--- a/lib/yahns/proxy_http_response.rb
+++ b/lib/yahns/proxy_http_response.rb
@@ -227,11 +227,40 @@ module Yahns::HttpResponse # :nodoc:
     proxy_busy_mod_done(wbuf.wbuf_persist) # returns nil
   end
 
+  def proxy_wait_next(qflags)
+    # We must allocate a new, empty request object here to avoid a TOCTTOU
+    # in the following timeline
+    #
+    # original thread:                                 | another thread
+    # HttpClient#yahns_step                            |
+    # r = k.app.call(env = @hs.env)  # socket hijacked into epoll queue
+    # <thread is scheduled away>                       | epoll_wait readiness
+    #                                                  | ReqRes#yahns_step
+    #                                                  | proxy dispatch ...
+    #                                                  | proxy_busy_mod_done
+    # ************************** DANGER BELOW ********************************
+    #                                                  | HttpClient#yahns_step
+    #                                                  | # clears env
+    # sees empty env:                                  |
+    # return :ignore if env.include?('rack.hijack_io') |
+    #
+    # In other words, we cannot touch the original env seen by the
+    # original thread since it must see the 'rack.hijack_io' value
+    # because both are operating in the same Yahns::HttpClient object.
+    # This will happen regardless of GVL existence
+    hs = Unicorn::HttpRequest.new
+    hs.buf.replace(@hs.buf)
+    @hs = hs
+
+    # n.b. we may not touch anything in this object once we call queue_mod,
+    # another thread is likely to take it!
+    Thread.current[:yahns_queue].queue_mod(self, qflags)
+  end
+
   def proxy_busy_mod_done(alive)
-    q = Thread.current[:yahns_queue]
     case http_response_done(alive)
-    when :wait_readable then q.queue_mod(self, Yahns::Queue::QEV_RD)
-    when :wait_writable then q.queue_mod(self, Yahns::Queue::QEV_WR)
+    when :wait_readable then proxy_wait_next(Yahns::Queue::QEV_RD)
+    when :wait_writable then proxy_wait_next(Yahns::Queue::QEV_WR)
     when :close then Thread.current[:yahns_fdmap].sync_close(self)
     end
 
diff --git a/test/test_proxy_pass.rb b/test/test_proxy_pass.rb
index 840ad1c..a27a45e 100644
--- a/test/test_proxy_pass.rb
+++ b/test/test_proxy_pass.rb
@@ -437,6 +437,16 @@ class TestProxyPass < Testcase
       end
       re = /^Date:[^\r\n]+/
       assert_equal after_up.sub(re, ''), r1.sub(re, '')
+
+      pl.write "GET / HTTP/1.1\r\nHost: example.com"
+      sleep 0.1 # hope epoll wakes up and reads in this time
+      pl.write "\r\n\r\nGET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
+      burst = pl.readpartial(666)
+      until burst.scan(/^hi$/).size == 2
+        burst << pl.readpartial(666)
+      end
+      assert_equal 2, burst.scan(/^hi$/).size
+      assert_match %r{\r\n\r\nhi\n\z}, burst
     end
     r1 = r1.split("\r\n").reject { |x| x =~ /^Date: / }
     r2 = r2.split("\r\n").reject { |x| x =~ /^Date: / }