From a4095ee272f5e5082281a94a0856dde3c2f323f9 Mon Sep 17 00:00:00 2001 From: Edward Xu Date: Mon, 3 Aug 2026 23:20:39 +0800 Subject: [PATCH 1/4] gh-154937: Fix `_thread._shutdown()` racing with `_thread.start_joinable_thread` on `ThreadHandle.ident` (#155085) --- .../2026-08-02-17-55-29.gh-issue-154937.m9IBba.rst | 3 +++ Modules/_threadmodule.c | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-08-02-17-55-29.gh-issue-154937.m9IBba.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-02-17-55-29.gh-issue-154937.m9IBba.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-02-17-55-29.gh-issue-154937.m9IBba.rst new file mode 100644 index 000000000000000..475dc8358a8d811 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-02-17-55-29.gh-issue-154937.m9IBba.rst @@ -0,0 +1,3 @@ +Fix a data race on thread handle identifiers when ``_thread._shutdown()`` +runs concurrently with the startup of non-daemon threads in the +:term:`free-threaded build`. diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index e999fe20287e2d6..199e4ac3db723bf 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -2413,7 +2413,7 @@ thread_shutdown(PyObject *self, PyObject *args) struct llist_node *node; llist_for_each_safe(node, &state->shutdown_handles) { ThreadHandle *cur = llist_data(node, ThreadHandle, shutdown_node); - if (cur->ident != ident) { + if (ThreadHandle_ident(cur) != ident) { ThreadHandle_incref(cur); handle = cur; break; From c10c7e90befd8ba696c1f96323bf5602d4b6594b Mon Sep 17 00:00:00 2001 From: Wenzel Jakob Date: Mon, 3 Aug 2026 17:21:20 +0200 Subject: [PATCH 2/4] gh-151728: Clear the typing caches at interpreter shutdown (GH-155002) Re-apply GH-154858, which was reverted in GH-154992 because it broke the reference leak buildbots. ``atexit.register(_clear_caches)`` runs on every import of ``typing``, and the registered handler reaches the module dict through ``_clear_caches.__globals__``. Any throwaway copy of ``typing`` therefore stays alive until interpreter shutdown. Two tests create such a copy on each iteration: - ``InternalsTests.test_collect_parameters`` imports a fresh ``typing``. - ``CollectionsAbcTests.test_bytestring`` drops ``typing`` from ``sys.modules`` and re-imports it. Both now unregister the exit handler of the copy they created. --- Lib/test/test_typing.py | 13 +++++++++++++ Lib/typing.py | 6 ++++++ .../2026-07-29-11-05-00.gh-issue-151728.Kq3vTn.rst | 4 ++++ 3 files changed, 23 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-07-29-11-05-00.gh-issue-151728.Kq3vTn.rst diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 106ffdede6fd4ef..53c8c9fac694654 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1,4 +1,5 @@ import annotationlib +import atexit import contextlib import collections import collections.abc @@ -6550,6 +6551,14 @@ class F: class InternalsTests(BaseTestCase): def test_collect_parameters(self): typing = import_helper.import_fresh_module("typing") + # Importing typing registers an internal function named _clear_caches + # with atexit. The throwaway module created here installs its own + # handler, which holds the module alive and keeps references until + # interpreter shutdown even after the test finishes. Each repetition + # of this test under -R would therefore leak another module copy. To + # avoid this, we unregister the handler once the test is done. + self.addCleanup(atexit.unregister, typing._clear_caches) + with self.assertWarnsRegex( DeprecationWarning, "The private _collect_parameters function is deprecated" @@ -7719,6 +7728,10 @@ def test_bytestring(self): with self.assertWarns(DeprecationWarning): from typing import ByteString + # Drop the exit handler of this throwaway copy, see the comment in + # InternalsTests.test_collect_parameters. + self.addCleanup(atexit.unregister, sys.modules["typing"]._clear_caches) + with self.assertWarns(DeprecationWarning): self.assertIsInstance(b'', ByteString) with self.assertWarns(DeprecationWarning): diff --git a/Lib/typing.py b/Lib/typing.py index a05d73c29cf95e5..809c0ff88607a59 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -19,6 +19,7 @@ """ from abc import abstractmethod, ABCMeta +import atexit import collections from collections import defaultdict import collections.abc @@ -397,6 +398,11 @@ def _clear_caches(): cleanup() +# Release the LRU caches at shutdown, they otherwise redistribute reference +# leaks of one extension to types of unrelated ones. See GH-151728. +atexit.register(_clear_caches) + + def _tp_cache(func=None, /, *, typed=False): """Internal wrapper caching __getitem__ of generic types. diff --git a/Misc/NEWS.d/next/Library/2026-07-29-11-05-00.gh-issue-151728.Kq3vTn.rst b/Misc/NEWS.d/next/Library/2026-07-29-11-05-00.gh-issue-151728.Kq3vTn.rst new file mode 100644 index 000000000000000..39c63a1aea3193a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-29-11-05-00.gh-issue-151728.Kq3vTn.rst @@ -0,0 +1,4 @@ +Clear the internal :mod:`typing` caches from an exit handler. Previously, an +extension module that leaked a reference to :mod:`typing` would also keep every +subscripted type alive past interpreter shutdown, including types owned by +unrelated extension modules. From 910e584e1733f1cfff17553b340258c31bc74ccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kul=C3=ADk?= Date: Mon, 3 Aug 2026 17:34:23 +0200 Subject: [PATCH 3/4] gh-145030: Fix asyncio write pipe transport for named FIFOs on Solaris (#155110) --- Lib/asyncio/unix_events.py | 14 +++++++------- Lib/test/test_asyncio/test_events.py | 6 +++--- .../2026-07-20-12-40-00.gh-issue-145030.TFJm6k.rst | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Lib/asyncio/unix_events.py b/Lib/asyncio/unix_events.py index bb98f0014f81760..94c5fb726e59333 100644 --- a/Lib/asyncio/unix_events.py +++ b/Lib/asyncio/unix_events.py @@ -658,19 +658,19 @@ def __init__(self, loop, pipe, protocol, waiter=None, extra=None): # On AIX, the reader trick (to be notified when the read end of the # socket is closed) only works for sockets. On other platforms it # works for pipes and sockets. (Exception: OS X 10.4? Issue #19294.) - # On macOS, the trick misfires for named FIFOs (but not for pipes - # created with os.pipe(), which have st_nlink == 0): the write end - # polls as readable whenever unread data sits in the FIFO, and no + # On macOS and Solaris, the trick misfires for named FIFOs (but not for + # pipes created with os.pipe(), which have st_nlink == 0): the write + # end polls as readable whenever unread data sits in the FIFO, and no # event is delivered when the read end is closed, so it can only - # ever report a false disconnection (gh-145030). The same xnu + # ever report a false disconnection (gh-145030). The same XNU # behaviour applies on iOS/tvOS/watchOS (sys.platform is not # "darwin" there). - is_named_fifo_on_apple = ( - sys.platform in {"darwin", "ios", "tvos", "watchos"} + is_named_fifo_without_close_event = ( + sys.platform in {"darwin", "ios", "tvos", "watchos", "sunos5"} and is_fifo and pipe_stat.st_nlink > 0) if is_socket or (is_fifo and not sys.platform.startswith("aix") - and not is_named_fifo_on_apple): + and not is_named_fifo_without_close_event): # only start reading when connection_made() has been called self._loop.call_soon(self._loop._add_reader, self._fileno, self._read_ready) diff --git a/Lib/test/test_asyncio/test_events.py b/Lib/test/test_asyncio/test_events.py index 3fdfffdb213efc2..f7cd59a54199710 100644 --- a/Lib/test/test_asyncio/test_events.py +++ b/Lib/test/test_asyncio/test_events.py @@ -1729,9 +1729,9 @@ def reader(data): "Don't support pipes for Windows") @unittest.skipUnless(hasattr(os, 'mkfifo'), 'requires os.mkfifo()') def test_write_named_fifo_unread_data(self): - # gh-145030: on macOS, the write end of a named FIFO polls as - # readable while unread data sits in the FIFO, which made the - # transport misinterpret the event as the reader hanging up + # gh-145030: on macOS and Solaris, the write end of a named FIFO + # polls as readable while unread data sits in the FIFO, which made + # the transport misinterpret the event as the reader hanging up # and close itself. path = os_helper.TESTFN os.mkfifo(path) diff --git a/Misc/NEWS.d/next/Library/2026-07-20-12-40-00.gh-issue-145030.TFJm6k.rst b/Misc/NEWS.d/next/Library/2026-07-20-12-40-00.gh-issue-145030.TFJm6k.rst index 2c6b07a4fa8b76f..2f1b717b40ea74a 100644 --- a/Misc/NEWS.d/next/Library/2026-07-20-12-40-00.gh-issue-145030.TFJm6k.rst +++ b/Misc/NEWS.d/next/Library/2026-07-20-12-40-00.gh-issue-145030.TFJm6k.rst @@ -1,3 +1,3 @@ -Fix :mod:`asyncio` write pipe transports for named FIFOs on macOS. Unread -data sitting in the FIFO made the transport misinterpret a poll event as -the reader disconnecting, wrongly closing the transport. +Fix :mod:`asyncio` write pipe transports for named FIFOs on macOS and Solaris. +Unread data sitting in the FIFO made the transport misinterpret a poll event +as the reader disconnecting, wrongly closing the transport. From 1617cbe1dc16db302d708c321b7b0a1579a382c9 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 3 Aug 2026 16:42:16 +0100 Subject: [PATCH 4/4] gh-94984: Add mode parameter to asyncio create_unix_server() (#155086) --- Doc/library/asyncio-eventloop.rst | 13 ++++- Doc/library/asyncio-stream.rst | 9 +++- Doc/whatsnew/3.16.rst | 9 ++++ 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, 108 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 d24c8420ef8920c..41abb2d7d0a53eb 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 05445219510ca54..4092f440f66ad3b 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 6e69737768d5e15..c607e3c620572fe 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 807c70bc775aa2c..6b2d34e733a6b1a 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 94c5fb726e59333..3a66cee93da4f50 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 e88437eb2337ff0..c383a3bff962d74 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 000000000000000..648287680cf1564 --- /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.