python3-waitress: patch CVE-2024-49769

Details: https://nvd.nist.gov/vuln/detail/CVE-2024-49769

Pick the patch that is referenced in the NVD report (which is
a merge commit. The patches here are the individual patches from
that merge).

Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
This commit is contained in:
Gyorgy Sarvari
2026-01-07 10:27:47 +01:00
parent 1ea440cd62
commit 1bd2effd23
7 changed files with 406 additions and 0 deletions
@@ -0,0 +1,27 @@
From fdabcb31093507f50fcaeb46012ec8df8bf76359 Mon Sep 17 00:00:00 2001
From: Delta Regeer <bertjw@regeer.org>
Date: Sun, 3 Mar 2024 16:15:51 -0700
Subject: [PATCH] HTTPChannel is always created from accept, explicitly set
self.connected to True
CVE: CVE-2024-49769
Upstream-Status: Backport [https://github.com/Pylons/waitress/commit/03cc640fe7106902899f82115c26e37002bca7f1]
Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
---
src/waitress/channel.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/waitress/channel.py b/src/waitress/channel.py
index 756adce..cf19ef2 100644
--- a/src/waitress/channel.py
+++ b/src/waitress/channel.py
@@ -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 = []
@@ -0,0 +1,53 @@
From 646d7bfa81185b961b4797965f5c7ff0e380bc5c Mon Sep 17 00:00:00 2001
From: Delta Regeer <bertjw@regeer.org>
Date: Sun, 3 Mar 2024 16:16:48 -0700
Subject: [PATCH] Assume socket is not connected when passed to
wasyncore.dispatcher
No longer call getpeername() on the remote socket either, as it is not
necessary for any of the places where waitress requires that self.addr
in a subclass of the dispatcher needs it.
This removes a race condition when setting up a HTTPChannel where we
accepted the socket, and know the remote address, yet call getpeername()
again which would have the unintended side effect of potentially setting
self.connected to False because the remote has already shut down part of
the socket.
This issue was uncovered in #418, where the server would go into a hard
loop because self.connected was used in various parts of the code base.
CVE: CVE-2024-49769
Upstream-Status: Backport [https://github.com/Pylons/waitress/commit/840aebce1c4c1bfd9036f402c1f5d5a4d2f4a1c2]
Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
---
src/waitress/wasyncore.py | 16 ----------------
1 file changed, 16 deletions(-)
diff --git a/src/waitress/wasyncore.py b/src/waitress/wasyncore.py
index b3459e0..b5ddce2 100644
--- a/src/waitress/wasyncore.py
+++ b/src/waitress/wasyncore.py
@@ -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
@@ -0,0 +1,34 @@
From 28377c0e0fdd8669fb250e69745caf1c27ba541b Mon Sep 17 00:00:00 2001
From: Delta Regeer <bertjw@regeer.org>
Date: Sun, 3 Mar 2024 16:23:33 -0700
Subject: [PATCH] Remove test for getpeername()
CVE: CVE-2024-49769
Upstream-Status: Backport [https://github.com/Pylons/waitress/commit/86c680df4e4bdd40c78dec771cddcee059e802c4]
Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
---
tests/test_wasyncore.py | 11 -----------
1 file changed, 11 deletions(-)
diff --git a/tests/test_wasyncore.py b/tests/test_wasyncore.py
index e833c7e..5f38bd9 100644
--- a/tests/test_wasyncore.py
+++ b/tests/test_wasyncore.py
@@ -1451,17 +1451,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 = {}
@@ -0,0 +1,34 @@
From ee501847c38e21be0683ba81925472f219044a65 Mon Sep 17 00:00:00 2001
From: Delta Regeer <bertjw@regeer.org>
Date: Sun, 3 Mar 2024 16:26:22 -0700
Subject: [PATCH] Don't exit handle_write early -- even if socket is not
connected
Calling handle_close() multiple times does not hurt anything, and is
safe.
CVE: CVE-2024-49769
Upstream-Status: Backport [https://github.com/Pylons/waitress/commit/8cba302b1ac08c2874ae179b2af2445e89311bac]
Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
---
src/waitress/channel.py | 6 ------
1 file changed, 6 deletions(-)
diff --git a/src/waitress/channel.py b/src/waitress/channel.py
index cf19ef2..f4d9677 100644
--- a/src/waitress/channel.py
+++ b/src/waitress/channel.py
@@ -91,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
@@ -0,0 +1,211 @@
From aa161b98cc787f266d8ef358f00fc5b2b3944157 Mon Sep 17 00:00:00 2001
From: Delta Regeer <bertjw@regeer.org>
Date: Sun, 3 Mar 2024 16:35:39 -0700
Subject: [PATCH] Remove code not used by waitress from vendored asyncore
CVE: CVE-2024-49769
Upstream-Status: Backport [https://github.com/Pylons/waitress/commit/63678e652d912e67621580123c603e37c319d8c4]
Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
---
src/waitress/wasyncore.py | 45 ------------------
tests/test_wasyncore.py | 96 ++++++++-------------------------------
2 files changed, 18 insertions(+), 123 deletions(-)
diff --git a/src/waitress/wasyncore.py b/src/waitress/wasyncore.py
index b5ddce2..117f78a 100644
--- a/src/waitress/wasyncore.py
+++ b/src/waitress/wasyncore.py
@@ -379,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:
@@ -557,34 +540,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 --git a/tests/test_wasyncore.py b/tests/test_wasyncore.py
index 5f38bd9..44b8e19 100644
--- a/tests/test_wasyncore.py
+++ b/tests/test_wasyncore.py
@@ -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
@@ -1486,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 = {}
@@ -1661,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
@@ -0,0 +1,41 @@
From 4a5ce98ecaed785a14781700106d60c4072c9b87 Mon Sep 17 00:00:00 2001
From: Delta Regeer <bertjw@regeer.org>
Date: Sun, 3 Mar 2024 16:37:12 -0700
Subject: [PATCH] When closing the socket, set it to None
This avoids calling close() twice on the same socket if self.close() or
self.handle_close() is called multiple times
CVE: CVE-2024-49769
Upstream-Status: Backport [https://github.com/Pylons/waitress/commit/9d99c89ae4aa8449313eea210a5ec9f3994a87b2]
Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
---
src/waitress/wasyncore.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/waitress/wasyncore.py b/src/waitress/wasyncore.py
index 117f78a..f0cd23e 100644
--- a/src/waitress/wasyncore.py
+++ b/src/waitress/wasyncore.py
@@ -437,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.
@@ -487,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