From 6b3cc09f45741160787d90e0a1abbb2ed701347e Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sun, 2 Aug 2026 14:19:06 +0100 Subject: [PATCH 1/2] Add mode parameter to create_unix_server() --- Doc/library/asyncio-eventloop.rst | 13 ++++- Doc/library/asyncio-stream.rst | 9 +++- Doc/whatsnew/3.15.rst | 5 ++ Lib/asyncio/events.py | 6 ++- Lib/asyncio/unix_events.py | 20 +++++++- Lib/test/test_asyncio/test_unix_events.py | 51 +++++++++++++++++++ ...6-07-27-12-00-00.gh-issue-94984.Xr3vFq.rst | 4 ++ 7 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-27-12-00-00.gh-issue-94984.Xr3vFq.rst diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index d24c8420ef8920..6516c3fa49f044 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -838,7 +838,7 @@ Creating network servers *, sock=None, backlog=100, ssl=None, \ ssl_handshake_timeout=None, \ ssl_shutdown_timeout=None, \ - start_serving=True, cleanup_socket=True) + start_serving=True, cleanup_socket=True, mode=None) :async: Similar to :meth:`loop.create_server` but works with the @@ -853,6 +853,13 @@ Creating network servers be removed from the filesystem when the server is closed, unless the socket has been replaced after the server has been created. + If *mode* is not ``None``, the permissions of the socket file created + for *path* are changed to *mode* (as accepted by :func:`os.chmod`) + right after binding, before the server starts accepting connections, + so a connection can never be accepted while the default, + umask-derived permissions are still in effect. *mode* cannot be + combined with *sock* and is not supported for abstract Unix sockets. + See the documentation of the :meth:`loop.create_server` method for information about arguments to this method. @@ -871,6 +878,10 @@ Creating network servers Added the *cleanup_socket* parameter. + .. versionchanged:: 3.15 + + Added the *mode* parameter. + .. method:: loop.connect_accepted_socket(protocol_factory, \ sock, *, ssl=None, ssl_handshake_timeout=None, \ diff --git a/Doc/library/asyncio-stream.rst b/Doc/library/asyncio-stream.rst index 05445219510ca5..f29f4eadd326fa 100644 --- a/Doc/library/asyncio-stream.rst +++ b/Doc/library/asyncio-stream.rst @@ -171,7 +171,8 @@ and work with streams: .. function:: start_unix_server(client_connected_cb, path=None, \ *, limit=None, sock=None, backlog=100, ssl=None, \ ssl_handshake_timeout=None, \ - ssl_shutdown_timeout=None, start_serving=True, cleanup_socket=True) + ssl_shutdown_timeout=None, start_serving=True, \ + cleanup_socket=True, mode=None) :async: Start a Unix socket server. @@ -182,6 +183,9 @@ and work with streams: be removed from the filesystem when the server is closed, unless the socket has been replaced after the server has been created. + If *mode* is not ``None``, the permissions of the Unix socket file + are set to *mode* before the server starts accepting connections. + See also the documentation of :meth:`loop.create_unix_server`. .. note:: @@ -205,6 +209,9 @@ and work with streams: .. versionchanged:: 3.13 Added the *cleanup_socket* parameter. + .. versionchanged:: 3.15 + Added the *mode* parameter. + StreamReader ============ diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 5a8ab88a30fcf5..d9b1e1d32446a0 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -973,6 +973,11 @@ asyncio raising a custom exception which is then suppressed as it exits the task group. (Contributed by John Belmonte in :gh:`127214`.) +* Added the *mode* parameter to :meth:`asyncio.loop.create_unix_server` and + :func:`asyncio.start_unix_server` to set the permissions of the Unix + socket file created for *path*. + (Contributed by Sam Bull in :gh:`94984`.) + base64 ------ diff --git a/Lib/asyncio/events.py b/Lib/asyncio/events.py index 807c70bc775aa2..6b2d34e733a6b1 100644 --- a/Lib/asyncio/events.py +++ b/Lib/asyncio/events.py @@ -451,7 +451,7 @@ async def create_unix_server( sock=None, backlog=100, ssl=None, ssl_handshake_timeout=None, ssl_shutdown_timeout=None, - start_serving=True): + start_serving=True, mode=None): """A coroutine which creates a UNIX Domain Socket server. The return value is a Server object, which can be used to stop @@ -480,6 +480,10 @@ async def create_unix_server( the user should await Server.start_serving() or Server.serve_forever() to make the server to start accepting connections. + + mode, if not None, is applied to the socket file created for + path with os.chmod() after binding and before the server + starts accepting connections. """ raise NotImplementedError diff --git a/Lib/asyncio/unix_events.py b/Lib/asyncio/unix_events.py index bb98f0014f8176..a7f622bd26cc3a 100644 --- a/Lib/asyncio/unix_events.py +++ b/Lib/asyncio/unix_events.py @@ -276,7 +276,7 @@ async def create_unix_server( sock=None, backlog=100, ssl=None, ssl_handshake_timeout=None, ssl_shutdown_timeout=None, - start_serving=True, cleanup_socket=True): + start_serving=True, cleanup_socket=True, mode=None): if isinstance(ssl, bool): raise TypeError('ssl argument must be an SSLContext or None') @@ -294,6 +294,9 @@ async def create_unix_server( 'path and sock can not be specified at the same time') path = os.fspath(path) + if mode is not None and path and path[0] in (0, '\x00'): + raise ValueError( + 'mode is not supported for abstract sockets') sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) # Check for abstract socket. `str` and `bytes` paths are supported. @@ -322,11 +325,26 @@ async def create_unix_server( except: sock.close() raise + + if mode is not None: + # The socket cannot accept connections until listen() is + # called, which happens later in Server._start_serving(), + # so no connection can be accepted while the socket still + # has the default permissions. + try: + os.chmod(path, mode) + except: + sock.close() + raise else: if sock is None: raise ValueError( 'path was not specified, and no sock specified') + if mode is not None: + raise ValueError( + 'mode is only meaningful with path') + if (sock.family != socket.AF_UNIX or sock.type != socket.SOCK_STREAM): raise ValueError( diff --git a/Lib/test/test_asyncio/test_unix_events.py b/Lib/test/test_asyncio/test_unix_events.py index e88437eb2337ff..c383a3bff962d7 100644 --- a/Lib/test/test_asyncio/test_unix_events.py +++ b/Lib/test/test_asyncio/test_unix_events.py @@ -411,6 +411,57 @@ def test_create_unix_server_bind_error(self, m_socket): self.loop.run_until_complete(coro) self.assertTrue(sock.close.called) + @socket_helper.skip_unless_bind_unix_socket + def test_create_unix_server_mode(self): + # Two distinct modes: whatever the umask, at most one of them + # can coincide with the default permissions, so a no-op chmod + # cannot pass both subtests. + for mode in (0o600, 0o644): + with self.subTest(mode=mode): + with test_utils.unix_socket_path() as path: + srv = self.loop.run_until_complete( + self.loop.create_unix_server( + lambda: None, path, mode=mode)) + try: + self.assertEqual( + stat.S_IMODE(os.stat(path).st_mode), mode) + finally: + srv.close() + self.loop.run_until_complete(srv.wait_closed()) + + def test_create_unix_server_mode_sock(self): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + with sock: + coro = self.loop.create_unix_server(lambda: None, path=None, + sock=sock, mode=0o600) + with self.assertRaisesRegex(ValueError, + 'mode is only meaningful with path'): + self.loop.run_until_complete(coro) + + def test_create_unix_server_mode_abstract(self): + # The check is a pure string test, so it runs on all platforms. + for path in ('\x00spam', b'\x00spam'): + with self.subTest(path=path): + coro = self.loop.create_unix_server(lambda: None, path, + mode=0o600) + with self.assertRaisesRegex( + ValueError, 'mode is not supported for abstract'): + self.loop.run_until_complete(coro) + + @mock.patch('asyncio.unix_events.socket') + def test_create_unix_server_chmod_error(self, m_socket): + # Ensure that the socket is closed when os.chmod() fails + sock = mock.Mock() + m_socket.socket.return_value = sock + + with mock.patch('asyncio.unix_events.os.chmod', + side_effect=PermissionError): + coro = self.loop.create_unix_server(lambda: None, path='/test', + mode=0o600) + with self.assertRaises(PermissionError): + self.loop.run_until_complete(coro) + self.assertTrue(sock.close.called) + def test_create_unix_connection_path_sock(self): coro = self.loop.create_unix_connection( lambda: None, os.devnull, sock=object()) diff --git a/Misc/NEWS.d/next/Library/2026-07-27-12-00-00.gh-issue-94984.Xr3vFq.rst b/Misc/NEWS.d/next/Library/2026-07-27-12-00-00.gh-issue-94984.Xr3vFq.rst new file mode 100644 index 00000000000000..648287680cf156 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-27-12-00-00.gh-issue-94984.Xr3vFq.rst @@ -0,0 +1,4 @@ +Add the *mode* parameter to :meth:`asyncio.loop.create_unix_server` and +:func:`asyncio.start_unix_server` to set the permissions of the Unix +socket file created for *path*, applied before the server starts +accepting connections. From d67d7f1f336ba0ff0cee701157e7eee7843347e5 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sun, 2 Aug 2026 21:06:21 +0100 Subject: [PATCH 2/2] Target 3.16 --- Doc/library/asyncio-eventloop.rst | 2 +- Doc/library/asyncio-stream.rst | 2 +- Doc/whatsnew/3.15.rst | 5 ----- Doc/whatsnew/3.16.rst | 9 +++++++++ 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index 6516c3fa49f044..41abb2d7d0a53e 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -878,7 +878,7 @@ Creating network servers Added the *cleanup_socket* parameter. - .. versionchanged:: 3.15 + .. versionchanged:: 3.16 Added the *mode* parameter. diff --git a/Doc/library/asyncio-stream.rst b/Doc/library/asyncio-stream.rst index f29f4eadd326fa..4092f440f66ad3 100644 --- a/Doc/library/asyncio-stream.rst +++ b/Doc/library/asyncio-stream.rst @@ -209,7 +209,7 @@ and work with streams: .. versionchanged:: 3.13 Added the *cleanup_socket* parameter. - .. versionchanged:: 3.15 + .. versionchanged:: 3.16 Added the *mode* parameter. diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index d9b1e1d32446a0..5a8ab88a30fcf5 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -973,11 +973,6 @@ asyncio raising a custom exception which is then suppressed as it exits the task group. (Contributed by John Belmonte in :gh:`127214`.) -* Added the *mode* parameter to :meth:`asyncio.loop.create_unix_server` and - :func:`asyncio.start_unix_server` to set the permissions of the Unix - socket file created for *path*. - (Contributed by Sam Bull in :gh:`94984`.) - base64 ------ diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 6e69737768d5e1..c607e3c620572f 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -95,6 +95,15 @@ New modules Improved modules ================ +asyncio +------- + +* Add the *mode* parameter to :meth:`asyncio.loop.create_unix_server` and + :func:`asyncio.start_unix_server` to set the permissions of the Unix + socket file created for *path*. + (Contributed by Sam Bull in :gh:`94984`.) + + codecs ------