From 41df591a3028b703a5d8a14bb74dd98db79e7dac Mon Sep 17 00:00:00 2001 From: Sandro Mani Date: Tue, 19 Nov 2024 08:18:55 +0100 Subject: [PATCH] Backport fixes for CVE-2024-49768 and CVE-2024-49769 --- ...e894c9f76543bee06584001583fc6fa8c95c.patch | 286 ++++++++++++++++++ ...dcf556610ece2ff3cddb39e59a05ef110661.patch | 142 +++++++++ ...1c260cf17156b582c6252496213ddc96b591.patch | 28 ++ mingw-python-waitress.spec | 11 +- 4 files changed, 466 insertions(+), 1 deletion(-) create mode 100644 1ae4e894c9f76543bee06584001583fc6fa8c95c.patch create mode 100644 6943dcf556610ece2ff3cddb39e59a05ef110661.patch create mode 100644 f4ba1c260cf17156b582c6252496213ddc96b591.patch diff --git a/1ae4e894c9f76543bee06584001583fc6fa8c95c.patch b/1ae4e894c9f76543bee06584001583fc6fa8c95c.patch new file mode 100644 index 0000000..83e60f1 --- /dev/null +++ b/1ae4e894c9f76543bee06584001583fc6fa8c95c.patch @@ -0,0 +1,286 @@ +diff -rupN --no-dereference waitress-2.1.2-nodocs/src/waitress/channel.py waitress-2.1.2-nodocs-new/src/waitress/channel.py +--- waitress-2.1.2-nodocs/src/waitress/channel.py 2024-11-19 08:08:49.359774947 +0100 ++++ waitress-2.1.2-nodocs-new/src/waitress/channel.py 2024-11-19 08:08:49.365774972 +0100 +@@ -67,8 +67,7 @@ class HTTPChannel(wasyncore.dispatcher): + self.outbuf_lock = threading.Condition() + + wasyncore.dispatcher.__init__(self, sock, map=map) +- +- # Don't let wasyncore.dispatcher throttle self.addr on us. ++ self.connected = True + self.addr = addr + self.requests = [] + +@@ -92,13 +91,7 @@ class HTTPChannel(wasyncore.dispatcher): + # Precondition: there's data in the out buffer to be sent, or + # there's a pending will_close request + +- if not self.connected: +- # we dont want to close the channel twice +- +- return +- + # try to flush any pending output +- + if not self.requests: + # 1. There are no running tasks, so we don't need to try to lock + # the outbuf before sending +diff -rupN --no-dereference waitress-2.1.2-nodocs/src/waitress/wasyncore.py waitress-2.1.2-nodocs-new/src/waitress/wasyncore.py +--- waitress-2.1.2-nodocs/src/waitress/wasyncore.py 2022-05-30 21:03:51.000000000 +0200 ++++ waitress-2.1.2-nodocs-new/src/waitress/wasyncore.py 2024-11-19 08:08:49.366774976 +0100 +@@ -298,22 +298,6 @@ class dispatcher: + # get a socket from a blocking source. + sock.setblocking(0) + self.set_socket(sock, map) +- self.connected = True +- # The constructor no longer requires that the socket +- # passed be connected. +- try: +- self.addr = sock.getpeername() +- except OSError as err: +- if err.args[0] in (ENOTCONN, EINVAL): +- # To handle the case where we got an unconnected +- # socket. +- self.connected = False +- else: +- # The socket is broken in some unknown way, alert +- # the user and remove it from the map (to prevent +- # polling of broken sockets). +- self.del_channel(map) +- raise + else: + self.socket = None + +@@ -395,23 +379,6 @@ class dispatcher: + self.addr = addr + return self.socket.bind(addr) + +- def connect(self, address): +- self.connected = False +- self.connecting = True +- err = self.socket.connect_ex(address) +- if ( +- err in (EINPROGRESS, EALREADY, EWOULDBLOCK) +- or err == EINVAL +- and os.name == "nt" +- ): # pragma: no cover +- self.addr = address +- return +- if err in (0, EISCONN): +- self.addr = address +- self.handle_connect_event() +- else: +- raise OSError(err, errorcode[err]) +- + def accept(self): + # XXX can return either an address pair or None + try: +@@ -470,6 +437,8 @@ class dispatcher: + if why.args[0] not in (ENOTCONN, EBADF): + raise + ++ self.socket = None ++ + # log and log_info may be overridden to provide more sophisticated + # logging and warning methods. In general, log is for 'hit' logging + # and 'log_info' is for informational, warning and error logging. +@@ -520,7 +489,11 @@ class dispatcher: + # handle_expt_event() is called if there might be an error on the + # socket, or if there is OOB data + # check for the error condition first +- err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) ++ err = ( ++ self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) ++ if self.socket is not None ++ else 1 ++ ) + if err != 0: + # we can get here when select.select() says that there is an + # exceptional condition on the socket +@@ -573,34 +546,6 @@ class dispatcher: + self.close() + + +-# --------------------------------------------------------------------------- +-# adds simple buffered output capability, useful for simple clients. +-# [for more sophisticated usage use asynchat.async_chat] +-# --------------------------------------------------------------------------- +- +- +-class dispatcher_with_send(dispatcher): +- def __init__(self, sock=None, map=None): +- dispatcher.__init__(self, sock, map) +- self.out_buffer = b"" +- +- def initiate_send(self): +- num_sent = 0 +- num_sent = dispatcher.send(self, self.out_buffer[:65536]) +- self.out_buffer = self.out_buffer[num_sent:] +- +- handle_write = initiate_send +- +- def writable(self): +- return (not self.connected) or len(self.out_buffer) +- +- def send(self, data): +- if self.debug: # pragma: no cover +- self.log_info("sending %s" % repr(data)) +- self.out_buffer = self.out_buffer + data +- self.initiate_send() +- +- + def close_all(map=None, ignore_all=False): + if map is None: # pragma: no cover + map = socket_map +diff -rupN --no-dereference waitress-2.1.2-nodocs/tests/test_wasyncore.py waitress-2.1.2-nodocs-new/tests/test_wasyncore.py +--- waitress-2.1.2-nodocs/tests/test_wasyncore.py 2022-05-30 21:03:51.000000000 +0200 ++++ waitress-2.1.2-nodocs-new/tests/test_wasyncore.py 2024-11-19 08:08:49.366774976 +0100 +@@ -1,6 +1,7 @@ + import _thread as thread + import contextlib + import errno ++from errno import EALREADY, EINPROGRESS, EINVAL, EISCONN, EWOULDBLOCK, errorcode + import functools + import gc + from io import BytesIO +@@ -641,62 +642,6 @@ class DispatcherTests(unittest.TestCase) + self.assertTrue(err != "") + + +-class dispatcherwithsend_noread(asyncore.dispatcher_with_send): # pragma: no cover +- def readable(self): +- return False +- +- def handle_connect(self): +- pass +- +- +-class DispatcherWithSendTests(unittest.TestCase): +- def setUp(self): +- pass +- +- def tearDown(self): +- asyncore.close_all() +- +- @reap_threads +- def test_send(self): +- evt = threading.Event() +- sock = socket.socket() +- sock.settimeout(3) +- port = bind_port(sock) +- +- cap = BytesIO() +- args = (evt, cap, sock) +- t = threading.Thread(target=capture_server, args=args) +- t.start() +- try: +- # wait a little longer for the server to initialize (it sometimes +- # refuses connections on slow machines without this wait) +- time.sleep(0.2) +- +- data = b"Suppose there isn't a 16-ton weight?" +- d = dispatcherwithsend_noread() +- d.create_socket() +- d.connect((HOST, port)) +- +- # give time for socket to connect +- time.sleep(0.1) +- +- d.send(data) +- d.send(data) +- d.send(b"\n") +- +- n = 1000 +- +- while d.out_buffer and n > 0: # pragma: no cover +- asyncore.poll() +- n -= 1 +- +- evt.wait() +- +- self.assertEqual(cap.getvalue(), data * 2) +- finally: +- join_thread(t, timeout=TIMEOUT) +- +- + @unittest.skipUnless( + hasattr(asyncore, "file_wrapper"), "asyncore.file_wrapper required" + ) +@@ -839,6 +784,23 @@ class BaseClient(BaseTestHandler): + self.create_socket(family) + self.connect(address) + ++ def connect(self, address): ++ self.connected = False ++ self.connecting = True ++ err = self.socket.connect_ex(address) ++ if ( ++ err in (EINPROGRESS, EALREADY, EWOULDBLOCK) ++ or err == EINVAL ++ and os.name == "nt" ++ ): # pragma: no cover ++ self.addr = address ++ return ++ if err in (0, EISCONN): ++ self.addr = address ++ self.handle_connect_event() ++ else: ++ raise OSError(err, errorcode[err]) ++ + def handle_connect(self): + pass + +@@ -1451,17 +1413,6 @@ class Test_dispatcher(unittest.TestCase) + + return dispatcher(sock=sock, map=map) + +- def test_unexpected_getpeername_exc(self): +- sock = dummysocket() +- +- def getpeername(): +- raise OSError(errno.EBADF) +- +- map = {} +- sock.getpeername = getpeername +- self.assertRaises(socket.error, self._makeOne, sock=sock, map=map) +- self.assertEqual(map, {}) +- + def test___repr__accepting(self): + sock = dummysocket() + map = {} +@@ -1497,13 +1448,6 @@ class Test_dispatcher(unittest.TestCase) + inst.set_reuse_addr() + self.assertTrue(sock.errored) + +- def test_connect_raise_socket_error(self): +- sock = dummysocket() +- map = {} +- sock.connect_ex = lambda *arg: 1 +- inst = self._makeOne(sock=sock, map=map) +- self.assertRaises(socket.error, inst.connect, 0) +- + def test_accept_raise_TypeError(self): + sock = dummysocket() + map = {} +@@ -1672,21 +1616,6 @@ class Test_dispatcher(unittest.TestCase) + self.assertTrue(sock.closed) + + +-class Test_dispatcher_with_send(unittest.TestCase): +- def _makeOne(self, sock=None, map=None): +- from waitress.wasyncore import dispatcher_with_send +- +- return dispatcher_with_send(sock=sock, map=map) +- +- def test_writable(self): +- sock = dummysocket() +- map = {} +- inst = self._makeOne(sock=sock, map=map) +- inst.out_buffer = b"123" +- inst.connected = True +- self.assertTrue(inst.writable()) +- +- + class Test_close_all(unittest.TestCase): + def _callFUT(self, map=None, ignore_all=False): + from waitress.wasyncore import close_all diff --git a/6943dcf556610ece2ff3cddb39e59a05ef110661.patch b/6943dcf556610ece2ff3cddb39e59a05ef110661.patch new file mode 100644 index 0000000..332c625 --- /dev/null +++ b/6943dcf556610ece2ff3cddb39e59a05ef110661.patch @@ -0,0 +1,142 @@ +diff -rupN --no-dereference waitress-2.1.2-nodocs/tests/test_channel.py waitress-2.1.2-nodocs-new/tests/test_channel.py +--- waitress-2.1.2-nodocs/tests/test_channel.py 2022-05-30 21:03:51.000000000 +0200 ++++ waitress-2.1.2-nodocs-new/tests/test_channel.py 2024-11-19 08:08:49.329774824 +0100 +@@ -18,7 +18,7 @@ class TestHTTPChannel(unittest.TestCase) + 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 @@ class TestHTTPChannel(unittest.TestCase) + 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 @@ class TestHTTPChannel(unittest.TestCase) + def send(_): + return 0 + +- sock.send = send ++ sock.remote.send = send + + outbufs = inst.outbufs + wrote = inst.write_soon(wrapper) +@@ -270,7 +270,7 @@ class TestHTTPChannel(unittest.TestCase) + 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 @@ class TestHTTPChannel(unittest.TestCase) + 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 @@ class TestHTTPChannel(unittest.TestCase) + 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 @@ class TestHTTPChannel(unittest.TestCase) + 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 @@ class TestHTTPChannel(unittest.TestCase) + 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 @@ class TestHTTPChannel(unittest.TestCase) + + 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 @@ class DummySock: + 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 diff --git a/f4ba1c260cf17156b582c6252496213ddc96b591.patch b/f4ba1c260cf17156b582c6252496213ddc96b591.patch new file mode 100644 index 0000000..df03425 --- /dev/null +++ b/f4ba1c260cf17156b582c6252496213ddc96b591.patch @@ -0,0 +1,28 @@ +diff -rupN --no-dereference waitress-2.1.2-nodocs/src/waitress/channel.py waitress-2.1.2-nodocs-new/src/waitress/channel.py +--- waitress-2.1.2-nodocs/src/waitress/channel.py 2022-05-30 21:03:51.000000000 +0200 ++++ waitress-2.1.2-nodocs-new/src/waitress/channel.py 2024-11-19 08:08:49.347774898 +0100 +@@ -147,7 +147,7 @@ class HTTPChannel(wasyncore.dispatcher): + # 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. + +@@ -203,6 +203,15 @@ class HTTPChannel(wasyncore.dispatcher): + 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) diff --git a/mingw-python-waitress.spec b/mingw-python-waitress.spec index 4fff898..14757e1 100644 --- a/mingw-python-waitress.spec +++ b/mingw-python-waitress.spec @@ -6,7 +6,7 @@ Name: mingw-python-%{mod_name} Summary: MinGW Windows Python %{pypi_name} library Version: 2.1.2 -Release: 6%{?dist} +Release: 7%{?dist} BuildArch: noarch License: ZPL-2.1 @@ -18,6 +18,12 @@ URL: https://github.com/Pylons/waitress Source0: waitress-2.1.2-nodocs.tar.xz Source1: waitress-tarball-nodocs.sh +# Backport fix for CVE-2024-49769 +Patch0: https://github.com/Pylons/waitress/commit/6943dcf556610ece2ff3cddb39e59a05ef110661.patch +# Backport fixes for CVE-2024-49768 +Patch1: https://github.com/Pylons/waitress/commit/f4ba1c260cf17156b582c6252496213ddc96b591.patch +Patch2: https://github.com/Pylons/waitress/commit/1ae4e894c9f76543bee06584001583fc6fa8c95c.patch + BuildRequires: mingw32-filesystem >= 95 BuildRequires: mingw32-python3 BuildRequires: mingw32-python3-build @@ -73,6 +79,9 @@ MinGW Windows Python3 %{pypi_name} library. %changelog +* Tue Nov 19 2024 Sandro Mani - 2.1.2-7 +- Backport fixes for CVE-2024-49768 and CVE-2024-49769 + * Thu Jan 25 2024 Fedora Release Engineering - 2.1.2-6 - Rebuilt for https://fedoraproject.org/wiki/Fedora_40_Mass_Rebuild