Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
openSUSE:Step:15-SP4
python-waitress.36295
CVE-2024-49768.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File CVE-2024-49768.patch of Package python-waitress.36295
From 6943dcf556610ece2ff3cddb39e59a05ef110661 Mon Sep 17 00:00:00 2001 From: Delta Regeer <bertjw@regeer.org> Date: Sat, 26 Oct 2024 22:10:36 -0600 Subject: [PATCH 1/4] Make DummySock() look more like an actual socket This forces DummySock() to look like a properly connected socket where there is a buffer that is read from by the remote, and a buffer that is written to by the remote. The local side does the opposite, this way data written by the local side can be read by the remote without operating on the same buffer. --- tests/test_channel.py | 57 +++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/tests/test_channel.py b/tests/test_channel.py index 8467ae7a..7d677e91 100644 --- a/tests/test_channel.py +++ b/tests/test_channel.py @@ -18,7 +18,7 @@ def _makeOneWithMap(self, adj=None): map = {} inst = self._makeOne(sock, "127.0.0.1", adj, map=map) inst.outbuf_lock = DummyLock() - return inst, sock, map + return inst, sock.local(), map def test_ctor(self): inst, _, map = self._makeOneWithMap() @@ -218,7 +218,7 @@ def test_write_soon_nonempty_byte(self): def send(_): return 0 - sock.send = send + sock.remote.send = send wrote = inst.write_soon(b"a") self.assertEqual(wrote, 1) @@ -236,7 +236,7 @@ def test_write_soon_filewrapper(self): def send(_): return 0 - sock.send = send + sock.remote.send = send outbufs = inst.outbufs wrote = inst.write_soon(wrapper) @@ -270,7 +270,7 @@ def test_write_soon_rotates_outbuf_on_overflow(self): def send(_): return 0 - sock.send = send + sock.remote.send = send inst.adj.outbuf_high_watermark = 3 inst.current_outbuf_count = 4 @@ -286,7 +286,7 @@ def test_write_soon_waits_on_backpressure(self): def send(_): return 0 - sock.send = send + sock.remote.send = send inst.adj.outbuf_high_watermark = 3 inst.total_outbufs_len = 4 @@ -315,7 +315,7 @@ def send(_): inst.connected = False raise Exception() - sock.send = send + sock.remote.send = send inst.adj.outbuf_high_watermark = 3 inst.total_outbufs_len = 4 @@ -345,7 +345,7 @@ def send(_): inst.connected = False raise Exception() - sock.send = send + sock.remote.send = send wrote = inst.write_soon(b"xyz") self.assertEqual(wrote, 3) @@ -376,7 +376,7 @@ def test_handle_write_no_notify_after_flush(self): inst.total_outbufs_len = len(inst.outbufs[0]) inst.adj.send_bytes = 1 inst.adj.outbuf_high_watermark = 2 - sock.send = lambda x, do_close=True: False + sock.remote.send = lambda x, do_close=True: False inst.will_close = False inst.last_activity = 0 result = inst.handle_write() @@ -400,7 +400,7 @@ def test__flush_some_full_outbuf_socket_returns_nonzero(self): def test__flush_some_full_outbuf_socket_returns_zero(self): inst, sock, map = self._makeOneWithMap() - sock.send = lambda x: False + sock.remote.send = lambda x: False inst.outbufs[0].append(b"abc") inst.total_outbufs_len = sum(len(x) for x in inst.outbufs) result = inst._flush_some() @@ -907,7 +907,8 @@ class DummySock: closed = False def __init__(self): - self.sent = b"" + self.local_sent = b"" + self.remote_sent = b"" def setblocking(self, *arg): self.blocking = True @@ -925,14 +926,44 @@ def close(self): self.closed = True def send(self, data): - self.sent += data + self.remote_sent += data return len(data) def recv(self, buffer_size): - result = self.sent[:buffer_size] - self.sent = self.sent[buffer_size:] + result = self.local_sent[:buffer_size] + self.local_sent = self.local_sent[buffer_size:] return result + def local(self): + outer = self + + class LocalDummySock: + def send(self, data): + outer.local_sent += data + return len(data) + + def recv(self, buffer_size): + result = outer.remote_sent[:buffer_size] + outer.remote_sent = outer.remote_sent[buffer_size:] + return result + + def close(self): + outer.closed = True + + @property + def sent(self): + return outer.remote_sent + + @property + def closed(self): + return outer.closed + + @property + def remote(self): + return outer + + return LocalDummySock() + class DummyLock: notified = False From 7e7f11e61d358ab1cb853fcadf2b46b1f00f5993 Mon Sep 17 00:00:00 2001 From: Delta Regeer <bertjw@regeer.org> Date: Sat, 26 Oct 2024 22:12:14 -0600 Subject: [PATCH 2/4] Add a new test to validate the lookahead race condition --- tests/test_channel.py | 55 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/tests/test_channel.py b/tests/test_channel.py index 7d677e91..d798091d 100644 --- a/tests/test_channel.py +++ b/tests/test_channel.py @@ -805,11 +805,12 @@ def app_check_disconnect(self, environ, start_response): ) return [body] - def _make_app_with_lookahead(self): + def _make_app_with_lookahead(self, recv_bytes=8192): """ Setup a channel with lookahead and store it and the socket in self """ adj = DummyAdjustments() + adj.recv_bytes = recv_bytes adj.channel_request_lookahead = 5 channel, sock, map = self._makeOneWithMap(adj=adj) channel.server.application = self.app_check_disconnect @@ -901,6 +902,58 @@ def test_lookahead_continue(self): self.assertEqual(data.split("\r\n")[-1], "finished") self.assertEqual(self.request_body, b"x") + def test_lookahead_bad_request_drop_extra_data(self): + """ + Send two requests, the first one being bad, split on the recv_bytes + limit, then emulate a race that could happen whereby we read data from + the socket while the service thread is cleaning up due to an error + processing the request. + """ + + invalid_request = [ + "GET / HTTP/1.1", + "Host: localhost:8080", + "Content-length: -1", + "", + ] + + invalid_request_len = len("".join([x + "\r\n" for x in invalid_request])) + + second_request = [ + "POST / HTTP/1.1", + "Host: localhost:8080", + "Content-Length: 1", + "", + "x", + ] + + full_request = invalid_request + second_request + + self._make_app_with_lookahead(recv_bytes=invalid_request_len) + self._send(*full_request) + self.channel.handle_read() + self.assertEqual(len(self.channel.requests), 1) + self.channel.server.tasks[0].service() + self.assertTrue(self.channel.close_when_flushed) + # Read all of the next request + self.channel.handle_read() + self.channel.handle_read() + # Validate that there is no more data to be read + self.assertEqual(self.sock.remote.local_sent, b"") + # Validate that we dropped the data from the second read, and did not + # create a new request + self.assertEqual(len(self.channel.requests), 0) + data = self.sock.recv(256).decode("ascii") + self.assertFalse(self.channel.readable()) + self.assertTrue(self.channel.writable()) + + # Handle the write, which will close the socket + self.channel.handle_write() + self.assertTrue(self.sock.closed) + + data = self.sock.recv(256) + self.assertEqual(len(data), 0) + class DummySock: blocking = False From f4ba1c260cf17156b582c6252496213ddc96b591 Mon Sep 17 00:00:00 2001 From: Delta Regeer <bertjw@regeer.org> Date: Sat, 26 Oct 2024 22:13:08 -0600 Subject: [PATCH 3/4] Fix a race condition on recv_bytes boundary when request is invalid A remote client may send a request that is exactly recv_bytes long, followed by a secondary request using HTTP pipelining. When request lookahead is disabled (default) we won't read any more requests, and when the first request fails due to a parsing error, we simply close the connection. However when request lookahead is enabled, it is possible to process and receive the first request, start sending the error message back to the client while we read the next request and queue it. This will allow the secondar request to be serviced by the worker thread while the connection should be closed. The fix here checks if we should not have read the data in the first place (because the conection is going to be torn down) while we hold the `requests_lock` which means the service thread can't be in the middle of flipping the `close_when_flushed` flag. --- src/waitress/channel.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/waitress/channel.py b/src/waitress/channel.py index 3860ed51..f4d96776 100644 --- a/src/waitress/channel.py +++ b/src/waitress/channel.py @@ -140,7 +140,7 @@ def readable(self): # 1. We're not already about to close the connection. # 2. We're not waiting to flush remaining data before closing the # connection - # 3. There are not too many tasks already queued + # 3. There are not too many tasks already queued (if lookahead is enabled) # 4. There's no data in the output buffer that needs to be sent # before we potentially create a new task. @@ -196,6 +196,15 @@ def received(self, data): return False with self.requests_lock: + # Don't bother processing anymore data if this connection is about + # to close. This may happen if readable() returned True, on the + # main thread before the service thread set the close_when_flushed + # flag, and we read data but our service thread is attempting to + # shut down the connection due to an error. We want to make sure we + # do this while holding the request_lock so that we can't race + if self.will_close or self.close_when_flushed: + return False + while data: if self.request is None: self.request = self.parser_class(self.adj) From 810a435f9e9e293bd3446a5ce2df86f59c4e7b1b Mon Sep 17 00:00:00 2001 From: Delta Regeer <bertjw@regeer.org> Date: Sat, 26 Oct 2024 22:22:32 -0600 Subject: [PATCH 4/4] Add documentation for channel_request_lookahead --- docs/arguments.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/arguments.rst b/docs/arguments.rst index 0b6ca458..b8a856aa 100644 --- a/docs/arguments.rst +++ b/docs/arguments.rst @@ -314,3 +314,17 @@ url_prefix be stripped of the prefix. Default: ``''`` + +channel_request_lookahead + Sets the amount of requests we can continue to read from the socket, while + we are processing current requests. The default value won't allow any + lookahead, increase it above ``0`` to enable. + + When enabled this inserts a callable ``waitress.client_disconnected`` into + the environment that allows the task to check if the client disconnected + while waiting for the response at strategic points in the execution and to + cancel the operation. + + Default: ``0`` + + .. versionadded:: 2.0.0
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor