gh-94984: Add mode parameter to asyncio create_unix_server() (#155086)
kumaraditya303 <[email protected]> Mon, 03 Aug 2026 11:42:27 -0400 (EDT)
| Newsgroups | gmane.comp.python.cvs |
|---|---|
| Message-ID | <[email protected]> |
https://github.com/python/cpython/commit/1617cbe1dc16db302d708c321b7b0a1579a382c9 commit: 1617cbe1dc16db302d708c321b7b0a1579a382c9 branch: main author: Sam Bull <[email protected]> committer: kumaraditya303 <[email protected]> date: 2026-08-03T21:12:16+05:30 summary: gh-94984: Add mode parameter to asyncio create_unix_server() (#155086) files: A Misc/NEWS.d/next/Library/2026-07-27-12-00-00.gh-issue-94984.Xr3vFq.rst M Doc/library/asyncio-eventloop.rst M Doc/library/asyncio-stream.rst M Doc/whatsnew/3.16.rst M Lib/asyncio/events.py M Lib/asyncio/unix_events.py M Lib/test/test_asyncio/test_unix_events.py diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index d24c8420ef8920..41abb2d7d0a53e 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.16 + + 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..4092f440f66ad3 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.16 + Added the *mode* parameter. + StreamReader ============ 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 ------ 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 94c5fb726e5933..3a66cee93da4f5 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. _______________________________________________ Python-checkins mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3//lists/python-checkins.python.org Member address: [email protected]