]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-94984: Add mode parameter to asyncio create_unix_server() (#155086)
authorSam Bull <git@sambull.org>
Mon, 3 Aug 2026 15:42:16 +0000 (16:42 +0100)
committerGitHub <noreply@github.com>
Mon, 3 Aug 2026 15:42:16 +0000 (21:12 +0530)
Doc/library/asyncio-eventloop.rst
Doc/library/asyncio-stream.rst
Doc/whatsnew/3.16.rst
Lib/asyncio/events.py
Lib/asyncio/unix_events.py
Lib/test/test_asyncio/test_unix_events.py
Misc/NEWS.d/next/Library/2026-07-27-12-00-00.gh-issue-94984.Xr3vFq.rst [new file with mode: 0644]

index d24c8420ef8920c5a64ffede6f9cdc84b61a004c..41abb2d7d0a53eb4b906485c8a94cc803d911f85 100644 (file)
@@ -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, \
index 05445219510ca546e5f591d7d7877c38a3c69de2..4092f440f66ad3b471861f17ce50ea253f03ab92 100644 (file)
@@ -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
 ============
index 6e69737768d5e15f48b174294e1a46de3f93aa49..c607e3c620572fee0121f73526bf7d7587d6bccb 100644 (file)
@@ -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
 ------
 
index 807c70bc775aa2cd3b900ca0983237e15c110d10..6b2d34e733a6b1a8fa7496e2af07346c2e25c1a6 100644 (file)
@@ -451,7 +451,7 @@ class AbstractEventLoop:
             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 @@ class AbstractEventLoop:
         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
 
index 94c5fb726e5933378f11e7771e382ac4ba7ba69e..3a66cee93da4f5097ce17ef54c17bc114eb77ad2 100644 (file)
@@ -276,7 +276,7 @@ class _UnixSelectorEventLoop(selector_events.BaseSelectorEventLoop):
             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 @@ class _UnixSelectorEventLoop(selector_events.BaseSelectorEventLoop):
                     '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 @@ class _UnixSelectorEventLoop(selector_events.BaseSelectorEventLoop):
             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(
index e88437eb2337ff0617337146f91977e1e2e81325..c383a3bff962d7458a6a2b69f814c216fd3eecf6 100644 (file)
@@ -411,6 +411,57 @@ class SelectorEventLoopUnixSocketTests(test_utils.TestCase):
             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 (file)
index 0000000..6482876
--- /dev/null
@@ -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.