From 53a5e0df4acea36a3ed06b143a3464d017a6ee32 Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Fri, 23 Jan 2026 20:51:45 +0100 Subject: [PATCH 01/10] Use plain socket objects instead of wrapper classes Refactor socket creation to remove the socket wrapper classes so that these objects have less surprising behavior when used in worker hooks, worker classes, and custom applications. Rebased from https://github.com/benoitc/gunicorn/pull/3127 Co-authored-by: Paul J. Dorn --- gunicorn/arbiter.py | 8 +- gunicorn/config.py | 4 +- gunicorn/sock.py | 278 +++++++++++++++--------------------- gunicorn/workers/ggevent.py | 4 +- tests/test_sock.py | 49 ++++--- 5 files changed, 150 insertions(+), 193 deletions(-) diff --git a/gunicorn/arbiter.py b/gunicorn/arbiter.py index 222b3ca30..75e99c74e 100644 --- a/gunicorn/arbiter.py +++ b/gunicorn/arbiter.py @@ -158,7 +158,7 @@ def start(self): if not (self.cfg.reuse_port and hasattr(socket, 'SO_REUSEPORT')): self.LISTENERS = sock.create_sockets(self.cfg, self.log, fds) - listeners_str = ",".join([str(lnr) for lnr in self.LISTENERS]) + listeners_str = ",".join([sock.get_uri(lnr, self.cfg.is_ssl) for lnr in self.LISTENERS]) self.log.debug("Arbiter booted") self.log.info("Listening at: %s (%s)", listeners_str, self.pid) self.log.info("Using worker: %s", self.cfg.worker_class_str) @@ -459,7 +459,7 @@ def reload(self): lnr.close() # init new listeners self.LISTENERS = sock.create_sockets(self.cfg, self.log) - listeners_str = ",".join([str(lnr) for lnr in self.LISTENERS]) + listeners_str = ",".join([sock.get_uri(lnr, self.cfg.is_ssl) for lnr in self.LISTENERS]) self.log.info("Listening at: %s", listeners_str) # do some actions on reload @@ -601,8 +601,8 @@ def manage_workers(self): "mtype": "gauge"}) if self.cfg.enable_backlog_metric: - backlog = sum(sock.get_backlog() or 0 - for sock in self.LISTENERS) + backlog = sum(sock.get_backlog(lnr) or 0 + for lnr in self.LISTENERS) if backlog >= 0: self.log.debug("socket backlog: {0}".format(backlog), diff --git a/gunicorn/config.py b/gunicorn/config.py index 58141e55b..a27a74ccc 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -2225,7 +2225,7 @@ class KeyFile(Setting): section = "SSL" cli = ["--keyfile"] meta = "FILE" - validator = validate_string + validator = validate_file_exists default = None desc = """\ SSL key file @@ -2237,7 +2237,7 @@ class CertFile(Setting): section = "SSL" cli = ["--certfile"] meta = "FILE" - validator = validate_string + validator = validate_file_exists default = None desc = """\ SSL certificate file diff --git a/gunicorn/sock.py b/gunicorn/sock.py index d89d752cf..e74b3c8ee 100644 --- a/gunicorn/sock.py +++ b/gunicorn/sock.py @@ -7,7 +7,6 @@ import socket import ssl import stat -import struct import sys import time @@ -16,150 +15,76 @@ PLATFORM = sys.platform -class BaseSocket: +if PLATFORM == "linux": + def get_backlog(sock): + return -1 +else: + import struct + # tcp_info struct from include/uapi/linux/tcp.h + _TCPI_FMT = 'B' * 8 + 'I' * 24 + _TCPI_INDEX_UNACKED = 12 + def get_backlog(sock): + if sock.family not in (socket.AF_INET, socket.AF_INET6): + return -1 + try: + tcp_info_struct = self.sock.getsockopt(socket.IPPROTO_TCP, + socket.TCP_INFO, 104) + return struct.unpack(_TCPI_FMT, tcp_info_struct)[_TCPI_INDEX_UNACKED] + except (AttributeError, OSError): + pass + return 0 - def __init__(self, address, conf, log, fd=None): - self.log = log - self.conf = conf - self.cfg_addr = address - if fd is None: - sock = socket.socket(self.FAMILY, socket.SOCK_STREAM) - bound = False +def _get_socket_family(addr): + if isinstance(addr, tuple): + if util.is_ipv6(addr[0]): + return socket.AF_INET6 else: - sock = socket.fromfd(fd, self.FAMILY, socket.SOCK_STREAM) - os.close(fd) - bound = True + return socket.AF_INET - self.sock = self.set_options(sock, bound=bound) + if isinstance(addr, (str, bytes)): + return socket.AF_UNIX - def __str__(self): - return "" % self.sock.fileno() + raise TypeError("Unable to determine socket family for: %r" % addr) - def __getattr__(self, name): - return getattr(self.sock, name) - def set_options(self, sock, bound=False): - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - if (self.conf.reuse_port - and hasattr(socket, 'SO_REUSEPORT')): # pragma: no cover - try: - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) - except OSError as err: - if err.errno not in (errno.ENOPROTOOPT, errno.EINVAL): - raise - if not bound: - self.bind(sock) - sock.setblocking(0) - - # make sure that the socket can be inherited - if hasattr(sock, "set_inheritable"): - sock.set_inheritable(True) - - sock.listen(self.conf.backlog) - return sock - - def bind(self, sock): - sock.bind(self.cfg_addr) - - def close(self): - if self.sock is None: - return +def create_socket(conf, log, addr): + family = _get_socket_family(addr) + if family is socket.AF_UNIX: + # remove any existing socket at the given path try: - self.sock.close() - except OSError as e: - self.log.info("Error while closing socket %s", str(e)) - - self.sock = None - - def get_backlog(self): - return -1 - - -class TCPSocket(BaseSocket): - - FAMILY = socket.AF_INET - - def __str__(self): - if self.conf.is_ssl: - scheme = "https" + st = os.stat(addr) + except OSError as err: + if err.args[0] != errno.ENOENT: + raise else: - scheme = "http" - - addr = self.sock.getsockname() - return "%s://%s:%d" % (scheme, addr[0], addr[1]) - - def set_options(self, sock, bound=False): - sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - return super().set_options(sock, bound=bound) - - if PLATFORM == "linux": - def get_backlog(self): - if self.sock: - # tcp_info struct from include/uapi/linux/tcp.h - fmt = 'B' * 8 + 'I' * 24 - try: - tcp_info_struct = self.sock.getsockopt(socket.IPPROTO_TCP, - socket.TCP_INFO, 104) - # 12 is tcpi_unacked - return struct.unpack(fmt, tcp_info_struct)[12] - except (AttributeError, OSError): - pass - return 0 - else: - def get_backlog(self): - return -1 - - -class TCP6Socket(TCPSocket): - - FAMILY = socket.AF_INET6 - - def __str__(self): - (host, port, _, _) = self.sock.getsockname() - return "http://[%s]:%d" % (host, port) - - -class UnixSocket(BaseSocket): - - FAMILY = socket.AF_UNIX - - def __init__(self, addr, conf, log, fd=None): - if fd is None: - try: - st = os.stat(addr) - except OSError as e: - if e.args[0] != errno.ENOENT: - raise + if stat.S_ISSOCK(st.st_mode): + os.remove(addr) else: - if stat.S_ISSOCK(st.st_mode): - os.remove(addr) - else: - raise ValueError("%r is not a socket" % addr) - super().__init__(addr, conf, log, fd=fd) - - def __str__(self): - return "unix:%s" % self.cfg_addr - - def bind(self, sock): - old_umask = os.umask(self.conf.umask) - sock.bind(self.cfg_addr) - util.chown(self.cfg_addr, self.conf.uid, self.conf.gid) - os.umask(old_umask) + raise ValueError("%r already exists but is not a UNIX socket" % addr) + for i in range(5): + try: + sock = socket.socket(family) + sock.bind(addr) + sock.listen(conf.backlog) + if family is socket.AF_UNIX: + util.chown(addr, conf.uid, conf.gid) + return sock + except OSError as e: + if e.errno == errno.EADDRINUSE: + log.error("Connection in use: %s", str(addr)) + if e.errno == errno.EADDRNOTAVAIL: + log.error("Invalid address: %s", str(addr)) + msg = "connection to {addr} failed: {error}" + log.error(msg.format(addr=str(addr), error=str(e))) + if i < 5: + log.debug("Retrying in 1 second.") + time.sleep(1) -def _sock_type(addr): - if isinstance(addr, tuple): - if util.is_ipv6(addr[0]): - sock_type = TCP6Socket - else: - sock_type = TCPSocket - elif isinstance(addr, (str, bytes)): - sock_type = UnixSocket - else: - raise TypeError("Unable to create socket from: %r" % addr) - return sock_type + log.error("Can't connect to %s", str(addr)) + sys.exit(1) def create_sockets(conf, log, fds=None): @@ -190,49 +115,70 @@ def create_sockets(conf, log, fds=None): # sockets are already bound if fdaddr: for fd in fdaddr: - sock = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM) - sock_name = sock.getsockname() - sock_type = _sock_type(sock_name) - listener = sock_type(sock_name, conf, log, fd=fd) - listeners.append(listener) - + # no file descriptor duplication + sock = socket.socket(fileno=fd) + set_socket_options(conf, sock) + listeners.append(sock) return listeners # no sockets is bound, first initialization of gunicorn in this env. - for addr in laddr: - sock_type = _sock_type(addr) - sock = None - for i in range(5): - try: - sock = sock_type(addr, conf, log) - except OSError as e: - if e.args[0] == errno.EADDRINUSE: - log.error("Connection in use: %s", str(addr)) - if e.args[0] == errno.EADDRNOTAVAIL: - log.error("Invalid address: %s", str(addr)) - msg = "connection to {addr} failed: {error}" - log.error(msg.format(addr=str(addr), error=str(e))) - if i < 5: - log.debug("Retrying in 1 second.") - time.sleep(1) - else: - break - - if sock is None: - log.error("Can't connect to %s", str(addr)) - sys.exit(1) - - listeners.append(sock) + old_umask = os.umask(conf.umask) + try: + bind_list = [bind for bind in conf.address if not isinstance(bind, int)] + for addr in laddr: + sock = create_socket(conf, log, addr) + set_socket_options(conf, sock) + listeners.append(sock) + finally: + os.umask(old_umask) return listeners def close_sockets(listeners, unlink=True): for sock in listeners: - sock_name = sock.getsockname() - sock.close() - if unlink and _sock_type(sock_name) is UnixSocket: - os.unlink(sock_name) + try: + if unlink and sock.family is socket.AF_UNIX: + sock_name = sock.getsockname() + os.unlink(sock_name) + finally: + sock.close() + + +def get_uri(listener, is_ssl): + addr = listener.getsockname() + family = _get_socket_family(addr) + scheme = "https" if is_ssl else "http" + + if family is socket.AF_INET: + (host, port) = listener.getsockname() + return f"{scheme}://{host}:{port}" + + if family is socket.AF_INET6: + (host, port, _, _) = listener.getsockname() + return f"{scheme}://[{host}]:{port}" + + if family is socket.AF_UNIX: + path = listener.getsockname() + return f"unix://{path}" + + +def set_socket_options(conf, sock): + sock.setblocking(False) + + # make sure that the socket can be inherited + if hasattr(sock, "set_inheritable"): + sock.set_inheritable(True) + + if sock.family in (socket.AF_INET, socket.AF_INET6): + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if (conf.reuse_port and hasattr(socket, 'SO_REUSEPORT')): # pragma: no cover + try: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + except OSError as err: + if err.errno not in (errno.ENOPROTOOPT, errno.EINVAL): + raise def ssl_context(conf): diff --git a/gunicorn/workers/ggevent.py b/gunicorn/workers/ggevent.py index 2e6238743..2226cfcce 100644 --- a/gunicorn/workers/ggevent.py +++ b/gunicorn/workers/ggevent.py @@ -40,8 +40,8 @@ def patch(self): # patch sockets sockets = [] for s in self.sockets: - sockets.append(socket.socket(s.FAMILY, socket.SOCK_STREAM, - fileno=s.sock.detach())) + sockets.append(socket.socket(s.family, socket.SOCK_STREAM, + fileno=s.detach())) self.sockets = sockets def notify(self): diff --git a/tests/test_sock.py b/tests/test_sock.py index c3251af8c..a79a470aa 100644 --- a/tests/test_sock.py +++ b/tests/test_sock.py @@ -2,30 +2,41 @@ # This file is part of gunicorn released under the MIT license. # See the NOTICE for more information. +import socket from unittest import mock -from gunicorn import sock +import pytest +from gunicorn import sock -@mock.patch('os.stat') -def test_create_sockets_unix_bytes(stat): - conf = mock.Mock(address=[b'127.0.0.1:8000']) - log = mock.Mock() - with mock.patch.object(sock.UnixSocket, '__init__', lambda *args: None): - listeners = sock.create_sockets(conf, log) - assert len(listeners) == 1 - print(type(listeners[0])) - assert isinstance(listeners[0], sock.UnixSocket) +@pytest.fixture(scope='function') +def addr(request, tmp_path): + if isinstance(request.param, str): + return str(tmp_path / request.param) + return request.param -@mock.patch('os.stat') -def test_create_sockets_unix_strings(stat): - conf = mock.Mock(address=['127.0.0.1:8000']) +@pytest.mark.parametrize( + 'addr, family', + [ + ('gunicorn.sock', socket.AF_UNIX), + (('0.0.0.0', 0), socket.AF_INET), + (('::', 0), socket.AF_INET6), + ], + indirect=['addr'], +) +@mock.patch('socket.socket') +@mock.patch('gunicorn.util.chown') +def test_create_socket(chown, socket, addr, family): + conf = mock.Mock(address=[addr], umask=0o22) log = mock.Mock() - with mock.patch.object(sock.UnixSocket, '__init__', lambda *args: None): - listeners = sock.create_sockets(conf, log) - assert len(listeners) == 1 - assert isinstance(listeners[0], sock.UnixSocket) + listener = sock.create_socket(conf, log, addr) + assert listener == socket.return_value + socket.assert_called_with(family) + listener.bind.assert_called_with(addr) + listener.listen.assert_called_with(conf.backlog) + if family is socket.AF_UNIX: + chown.assert_called_with(addr, conf.uid, conf.gid) def test_socket_close(): @@ -40,7 +51,7 @@ def test_socket_close(): @mock.patch('os.unlink') def test_unix_socket_close_unlink(unlink): - listener = mock.Mock() + listener = mock.Mock(family=socket.AF_UNIX) listener.getsockname.return_value = '/var/run/test.sock' sock.close_sockets([listener]) listener.close.assert_called_with() @@ -49,7 +60,7 @@ def test_unix_socket_close_unlink(unlink): @mock.patch('os.unlink') def test_unix_socket_close_without_unlink(unlink): - listener = mock.Mock() + listener = mock.Mock(family=socket.AF_UNIX) listener.getsockname.return_value = '/var/run/test.sock' sock.close_sockets([listener], False) listener.close.assert_called_with() From 33ac1d2565d4f6d20b3f4e4229ac257ed9c6532f Mon Sep 17 00:00:00 2001 From: "Paul J. Dorn" Date: Fri, 23 Jan 2026 21:11:01 +0100 Subject: [PATCH 02/10] fix: mistakes during rebase --- gunicorn/sock.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gunicorn/sock.py b/gunicorn/sock.py index e74b3c8ee..d1d39331b 100644 --- a/gunicorn/sock.py +++ b/gunicorn/sock.py @@ -27,8 +27,8 @@ def get_backlog(sock): if sock.family not in (socket.AF_INET, socket.AF_INET6): return -1 try: - tcp_info_struct = self.sock.getsockopt(socket.IPPROTO_TCP, - socket.TCP_INFO, 104) + tcp_info_struct = sock.getsockopt(socket.IPPROTO_TCP, + socket.TCP_INFO, 104) return struct.unpack(_TCPI_FMT, tcp_info_struct)[_TCPI_INDEX_UNACKED] except (AttributeError, OSError): pass @@ -124,7 +124,6 @@ def create_sockets(conf, log, fds=None): # no sockets is bound, first initialization of gunicorn in this env. old_umask = os.umask(conf.umask) try: - bind_list = [bind for bind in conf.address if not isinstance(bind, int)] for addr in laddr: sock = create_socket(conf, log, addr) set_socket_options(conf, sock) From 4c6b1a5529209044d19c0b538a992dfaa6aadef4 Mon Sep 17 00:00:00 2001 From: "Paul J. Dorn" Date: Fri, 23 Jan 2026 21:11:54 +0100 Subject: [PATCH 03/10] style --- gunicorn/sock.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gunicorn/sock.py b/gunicorn/sock.py index d1d39331b..0b8dd23a8 100644 --- a/gunicorn/sock.py +++ b/gunicorn/sock.py @@ -23,6 +23,7 @@ def get_backlog(sock): # tcp_info struct from include/uapi/linux/tcp.h _TCPI_FMT = 'B' * 8 + 'I' * 24 _TCPI_INDEX_UNACKED = 12 + def get_backlog(sock): if sock.family not in (socket.AF_INET, socket.AF_INET6): return -1 From 495c09ce319aa1a128552cf37ef6c44cdc2941fd Mon Sep 17 00:00:00 2001 From: "Paul J. Dorn" Date: Fri, 23 Jan 2026 23:42:10 +0100 Subject: [PATCH 04/10] fix backlog metric --- gunicorn/sock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gunicorn/sock.py b/gunicorn/sock.py index 0b8dd23a8..a9ff13f59 100644 --- a/gunicorn/sock.py +++ b/gunicorn/sock.py @@ -15,7 +15,7 @@ PLATFORM = sys.platform -if PLATFORM == "linux": +if PLATFORM != "linux": def get_backlog(sock): return -1 else: From 97a4fa18726255dd0d14a9dc0c976096a02be625 Mon Sep 17 00:00:00 2001 From: "Paul J. Dorn" Date: Sat, 24 Jan 2026 00:32:47 +0100 Subject: [PATCH 05/10] linux: tcp_info is u8/u32 --- gunicorn/sock.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gunicorn/sock.py b/gunicorn/sock.py index a9ff13f59..247f3436d 100644 --- a/gunicorn/sock.py +++ b/gunicorn/sock.py @@ -21,7 +21,9 @@ def get_backlog(sock): else: import struct # tcp_info struct from include/uapi/linux/tcp.h - _TCPI_FMT = 'B' * 8 + 'I' * 24 + _TCPI_FMT = '=' + 'B' * 8 + 'I' * 24 + # getsockopt silently truncates to requested length + _TCPI_LEN = struct.calcsize(_TCPI_FMT) # 104 _TCPI_INDEX_UNACKED = 12 def get_backlog(sock): @@ -29,7 +31,7 @@ def get_backlog(sock): return -1 try: tcp_info_struct = sock.getsockopt(socket.IPPROTO_TCP, - socket.TCP_INFO, 104) + socket.TCP_INFO, _TCPI_LEN) return struct.unpack(_TCPI_FMT, tcp_info_struct)[_TCPI_INDEX_UNACKED] except (AttributeError, OSError): pass From 2c960ba2b51247996e0414e82dcc1a3fc6fc786f Mon Sep 17 00:00:00 2001 From: "Paul J. Dorn" Date: Sat, 24 Jan 2026 00:40:59 +0100 Subject: [PATCH 06/10] asgi: duplicate socket unwrap --- gunicorn/workers/gasgi.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gunicorn/workers/gasgi.py b/gunicorn/workers/gasgi.py index 118d11de2..5535859be 100644 --- a/gunicorn/workers/gasgi.py +++ b/gunicorn/workers/gasgi.py @@ -14,6 +14,7 @@ import signal import sys +from gunicorn.sock import get_uri from gunicorn.workers import base from gunicorn.asgi.protocol import ASGIProtocol @@ -178,15 +179,15 @@ async def _serve(self): try: server = await self.loop.create_server( lambda: ASGIProtocol(self), - sock=sock.sock, + sock=sock, ssl=ssl_context, reuse_address=True, start_serving=True, ) self.servers.append(server) - self.log.info("ASGI server listening on %s", sock) + self.log.info("ASGI server listening on %s", get_uri(sock, self.cfg.is_ssl)) except Exception as e: - self.log.error("Failed to create server on %s: %s", sock, e) + self.log.error("Failed to create server on %s: %s", get_uri(sock, self.cfg.is_ssl), e) if not self.servers: self.log.error("No servers could be started") From e2a856a4ce9457fc4cf6b33be3eccea86be6f27f Mon Sep 17 00:00:00 2001 From: "Paul J. Dorn" Date: Sat, 24 Jan 2026 00:58:08 +0100 Subject: [PATCH 07/10] test sock repeated for string formatting checks --- tests/test_sock.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_sock.py b/tests/test_sock.py index a79a470aa..aa2383c48 100644 --- a/tests/test_sock.py +++ b/tests/test_sock.py @@ -39,6 +39,30 @@ def test_create_socket(chown, socket, addr, family): chown.assert_called_with(addr, conf.uid, conf.gid) +@pytest.mark.parametrize( + 'addr, is_ssl, addr_as_uri', + [ + ('gunicorn.sock', False, "unix://%s"), + (('192.0.2.1', 80), False, "http://192.0.2.1:80"), + (('192.0.2.1', 443), True, "https://192.0.2.1:443"), + (('[fe80::1]', 443), True, "https://[fe80::1]:443"), + ], + indirect=['addr'], +) +@mock.patch('socket.socket') +@mock.patch('gunicorn.util.chown') +def test_get_socket_uri(chown, socket, addr, is_ssl, addr_as_uri): + conf = mock.Mock(address=[addr], umask=0o22) + log = mock.Mock() + listener = sock.create_socket(conf, log, addr) + assert listener == socket.return_value + # mock + listener.getsockname = lambda: addr + if isinstance(addr, str): + addr_as_uri = addr_as_uri.replace("%s", addr) + assert sock.get_uri(listener, is_ssl=is_ssl) == addr_as_uri + + def test_socket_close(): listener1 = mock.Mock() listener1.getsockname.return_value = ('127.0.0.1', '80') From e1b54ff69a57be3dd15d7222c4957e33e72e904c Mon Sep 17 00:00:00 2001 From: "Paul J. Dorn" Date: Sat, 24 Jan 2026 00:58:37 +0100 Subject: [PATCH 08/10] revert: early SSL cert/key file check --- gunicorn/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gunicorn/config.py b/gunicorn/config.py index a27a74ccc..58141e55b 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -2225,7 +2225,7 @@ class KeyFile(Setting): section = "SSL" cli = ["--keyfile"] meta = "FILE" - validator = validate_file_exists + validator = validate_string default = None desc = """\ SSL key file @@ -2237,7 +2237,7 @@ class CertFile(Setting): section = "SSL" cli = ["--certfile"] meta = "FILE" - validator = validate_file_exists + validator = validate_string default = None desc = """\ SSL certificate file From b4e9fcdfb3a2bf139776265c7ee1d6b1256d34e0 Mon Sep 17 00:00:00 2001 From: "Paul J. Dorn" Date: Sat, 24 Jan 2026 01:30:04 +0100 Subject: [PATCH 09/10] test fd bind and (on linux) socket backlog --- tests/test_sock.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_sock.py b/tests/test_sock.py index aa2383c48..4b851e1ae 100644 --- a/tests/test_sock.py +++ b/tests/test_sock.py @@ -2,6 +2,7 @@ # This file is part of gunicorn released under the MIT license. # See the NOTICE for more information. +import sys import socket from unittest import mock @@ -9,6 +10,7 @@ from gunicorn import sock + @pytest.fixture(scope='function') def addr(request, tmp_path): if isinstance(request.param, str): @@ -16,6 +18,36 @@ def addr(request, tmp_path): return request.param +def test_socket_backlog(): + listener = mock.Mock(family=socket.AF_INET6) + + def fake_getsockopt(prot, opt, length): + assert prot == socket.IPPROTO_TCP + assert opt == socket.TCP_INFO + assert length == 104 + return b"\x01\x01\0\0" * (length // 4) + listener.getsockopt = fake_getsockopt + bl = sock.get_backlog(listener) + if sys.platform == "linux": + assert bl == (1 << 8) + 1 + else: + assert bl == -1 + + +@mock.patch('socket.socket') +@mock.patch('gunicorn.util.chown') +def test_inherit_socket(chown, socket): + conf = mock.Mock(address=[], certfile=None, keyfile=None) + log = mock.Mock() + listeners = sock.create_sockets(conf, log, fds=[3]) + assert len(listeners) == 1 + listener = listeners[0] + assert listener == socket.return_value + socket.assert_called_with(fileno=3) + listener.listen.assert_not_called() + chown.assert_not_called() + + @pytest.mark.parametrize( 'addr, family', [ From a5863145611279243e775facf76e70ad579adad4 Mon Sep 17 00:00:00 2001 From: "Paul J. Dorn" Date: Sat, 24 Jan 2026 01:42:17 +0100 Subject: [PATCH 10/10] revert: skip sock.listen --- gunicorn/sock.py | 1 + tests/test_sock.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/gunicorn/sock.py b/gunicorn/sock.py index 247f3436d..625706ca9 100644 --- a/gunicorn/sock.py +++ b/gunicorn/sock.py @@ -120,6 +120,7 @@ def create_sockets(conf, log, fds=None): for fd in fdaddr: # no file descriptor duplication sock = socket.socket(fileno=fd) + sock.listen(conf.backlog) set_socket_options(conf, sock) listeners.append(sock) return listeners diff --git a/tests/test_sock.py b/tests/test_sock.py index 4b851e1ae..5ac9bab6a 100644 --- a/tests/test_sock.py +++ b/tests/test_sock.py @@ -44,7 +44,7 @@ def test_inherit_socket(chown, socket): listener = listeners[0] assert listener == socket.return_value socket.assert_called_with(fileno=3) - listener.listen.assert_not_called() + listener.listen.assert_called_with(conf.backlog) chown.assert_not_called()