From 2a0a5e981535d71f2eb76eb5ea2daa759e92b089 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 4 Aug 2025 23:20:48 +0300 Subject: [PATCH 1/4] gh-137335: Fix unlikely nmae conflicts for named pipes in multiprocessing and asyncio on Windows Since os.stat() raises an OSError for existing named pipe "\\.\pipe\...", os.path.exists() always returns False for it, and tempfile.mktemp() can return a name that matches an existing named pipe. So, tempfile.mktemp() cannot be used to generate unique names for named pipes. Instead, CreateNamedPipe() should be called in a loop with different names until it completes successfully. --- Lib/asyncio/windows_utils.py | 22 +++++--- Lib/multiprocessing/connection.py | 55 ++++++++++++------- ...-08-04-23-20-43.gh-issue-137335.IIjDJN.rst | 2 + 3 files changed, 49 insertions(+), 30 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-08-04-23-20-43.gh-issue-137335.IIjDJN.rst diff --git a/Lib/asyncio/windows_utils.py b/Lib/asyncio/windows_utils.py index ef277fac3e291c..4f6604f7f86df5 100644 --- a/Lib/asyncio/windows_utils.py +++ b/Lib/asyncio/windows_utils.py @@ -6,7 +6,6 @@ raise ImportError('win32 only') import _winapi -import itertools import msvcrt import os import subprocess @@ -23,7 +22,6 @@ BUFSIZE = 8192 PIPE = subprocess.PIPE STDOUT = subprocess.STDOUT -_mmap_counter = itertools.count() # Replacement for os.pipe() using handles instead of fds @@ -31,10 +29,6 @@ def pipe(*, duplex=False, overlapped=(True, True), bufsize=BUFSIZE): """Like os.pipe() but with overlapped support and using handles not fds.""" - address = tempfile.mktemp( - prefix=r'\\.\pipe\python-pipe-{:d}-{:d}-'.format( - os.getpid(), next(_mmap_counter))) - if duplex: openmode = _winapi.PIPE_ACCESS_DUPLEX access = _winapi.GENERIC_READ | _winapi.GENERIC_WRITE @@ -54,11 +48,21 @@ def pipe(*, duplex=False, overlapped=(True, True), bufsize=BUFSIZE): else: flags_and_attribs = 0 + prefix = fr'\\.\pipe\python-pipe-{os.getpid()}-' + names = tempfile._get_candidate_names() h1 = h2 = None try: - h1 = _winapi.CreateNamedPipe( - address, openmode, _winapi.PIPE_WAIT, - 1, obsize, ibsize, _winapi.NMPWAIT_WAIT_FOREVER, _winapi.NULL) + while True: + address = prefix + next(names) + try: + h1 = _winapi.CreateNamedPipe( + address, openmode, _winapi.PIPE_WAIT, + 1, obsize, ibsize, _winapi.NMPWAIT_WAIT_FOREVER, _winapi.NULL) + break + except OSError as e: + if e.winerror not in (_winapi.ERROR_PIPE_BUSY, + _winapi.ERROR_ACCESS_DENIED): + raise h2 = _winapi.CreateFile( address, access, 0, _winapi.NULL, _winapi.OPEN_EXISTING, diff --git a/Lib/multiprocessing/connection.py b/Lib/multiprocessing/connection.py index fc00d2861260a8..46a1af6e83af03 100644 --- a/Lib/multiprocessing/connection.py +++ b/Lib/multiprocessing/connection.py @@ -11,7 +11,6 @@ import errno import io -import itertools import os import sys import socket @@ -45,8 +44,6 @@ # A very generous timeout when it comes to local connections... CONNECTION_TIMEOUT = 20. -_mmap_counter = itertools.count() - default_family = 'AF_INET' families = ['AF_INET'] @@ -78,8 +75,7 @@ def arbitrary_address(family): elif family == 'AF_UNIX': return tempfile.mktemp(prefix='sock-', dir=util.get_temp_dir()) elif family == 'AF_PIPE': - return tempfile.mktemp(prefix=r'\\.\pipe\pyc-%d-%d-' % - (os.getpid(), next(_mmap_counter)), dir="") + return fr'\\.\pipe\pyc-{os.getpid()}-{next(tempfile._get_candidate_names())}' else: raise ValueError('unrecognized family') @@ -472,17 +468,27 @@ class Listener(object): def __init__(self, address=None, family=None, backlog=1, authkey=None): family = family or (address and address_type(address)) \ or default_family - address = address or arbitrary_address(family) - _validate_family(family) + if authkey is not None and not isinstance(authkey, bytes): + raise TypeError('authkey should be a byte string') + if family == 'AF_PIPE': - self._listener = PipeListener(address, backlog) + if address: + self._listener = PipeListener(address, backlog) + else: + while True: + address = arbitrary_address(family) + try: + self._listener = PipeListener(address, backlog) + break + except OSError as e: + if e.winerror not in (_winapi.ERROR_PIPE_BUSY, + _winapi.ERROR_ACCESS_DENIED): + raise else: + address = address or arbitrary_address(family) self._listener = SocketListener(address, family, backlog) - if authkey is not None and not isinstance(authkey, bytes): - raise TypeError('authkey should be a byte string') - self._authkey = authkey def accept(self): @@ -570,7 +576,6 @@ def Pipe(duplex=True): ''' Returns pair of connection objects at either end of a pipe ''' - address = arbitrary_address('AF_PIPE') if duplex: openmode = _winapi.PIPE_ACCESS_DUPLEX access = _winapi.GENERIC_READ | _winapi.GENERIC_WRITE @@ -580,15 +585,23 @@ def Pipe(duplex=True): access = _winapi.GENERIC_WRITE obsize, ibsize = 0, BUFSIZE - h1 = _winapi.CreateNamedPipe( - address, openmode | _winapi.FILE_FLAG_OVERLAPPED | - _winapi.FILE_FLAG_FIRST_PIPE_INSTANCE, - _winapi.PIPE_TYPE_MESSAGE | _winapi.PIPE_READMODE_MESSAGE | - _winapi.PIPE_WAIT, - 1, obsize, ibsize, _winapi.NMPWAIT_WAIT_FOREVER, - # default security descriptor: the handle cannot be inherited - _winapi.NULL - ) + while True: + address = arbitrary_address('AF_PIPE') + try: + h1 = _winapi.CreateNamedPipe( + address, openmode | _winapi.FILE_FLAG_OVERLAPPED | + _winapi.FILE_FLAG_FIRST_PIPE_INSTANCE, + _winapi.PIPE_TYPE_MESSAGE | _winapi.PIPE_READMODE_MESSAGE | + _winapi.PIPE_WAIT, + 1, obsize, ibsize, _winapi.NMPWAIT_WAIT_FOREVER, + # default security descriptor: the handle cannot be inherited + _winapi.NULL + ) + break + except OSError as e: + if e.winerror not in (_winapi.ERROR_PIPE_BUSY, + _winapi.ERROR_ACCESS_DENIED): + raise h2 = _winapi.CreateFile( address, access, 0, _winapi.NULL, _winapi.OPEN_EXISTING, _winapi.FILE_FLAG_OVERLAPPED, _winapi.NULL diff --git a/Misc/NEWS.d/next/Library/2025-08-04-23-20-43.gh-issue-137335.IIjDJN.rst b/Misc/NEWS.d/next/Library/2025-08-04-23-20-43.gh-issue-137335.IIjDJN.rst new file mode 100644 index 00000000000000..bca2344f375fe5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-08-04-23-20-43.gh-issue-137335.IIjDJN.rst @@ -0,0 +1,2 @@ +Get rid of any possibility of a name conflict for named pipes in +:mod:`miltiprocessing` and :mod:`asyncio` on Windows, no matter how small. From 5b91a56e07f247052fbec3c006931693d410d76e Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 5 Aug 2025 10:53:17 +0300 Subject: [PATCH 2/4] Fix typo --- .../next/Library/2025-08-04-23-20-43.gh-issue-137335.IIjDJN.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-08-04-23-20-43.gh-issue-137335.IIjDJN.rst b/Misc/NEWS.d/next/Library/2025-08-04-23-20-43.gh-issue-137335.IIjDJN.rst index bca2344f375fe5..2311ace10e411d 100644 --- a/Misc/NEWS.d/next/Library/2025-08-04-23-20-43.gh-issue-137335.IIjDJN.rst +++ b/Misc/NEWS.d/next/Library/2025-08-04-23-20-43.gh-issue-137335.IIjDJN.rst @@ -1,2 +1,2 @@ Get rid of any possibility of a name conflict for named pipes in -:mod:`miltiprocessing` and :mod:`asyncio` on Windows, no matter how small. +:mod:`multiprocessing` and :mod:`asyncio` on Windows, no matter how small. From ed050a03a3870f7477fd5e3ff7eeba61a05b4d92 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 5 Aug 2025 12:01:39 +0300 Subject: [PATCH 3/4] Do not use os.getpid() and tempfile._get_candidate_names(). --- Lib/asyncio/windows_utils.py | 6 ++---- Lib/multiprocessing/connection.py | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Lib/asyncio/windows_utils.py b/Lib/asyncio/windows_utils.py index 4f6604f7f86df5..aac01acba5f093 100644 --- a/Lib/asyncio/windows_utils.py +++ b/Lib/asyncio/windows_utils.py @@ -8,8 +8,8 @@ import _winapi import msvcrt import os +import random import subprocess -import tempfile import warnings @@ -48,12 +48,10 @@ def pipe(*, duplex=False, overlapped=(True, True), bufsize=BUFSIZE): else: flags_and_attribs = 0 - prefix = fr'\\.\pipe\python-pipe-{os.getpid()}-' - names = tempfile._get_candidate_names() h1 = h2 = None try: while True: - address = prefix + next(names) + address = r'\\.\pipe\python-pipe-' + random.randbytes(8).hex() try: h1 = _winapi.CreateNamedPipe( address, openmode, _winapi.PIPE_WAIT, diff --git a/Lib/multiprocessing/connection.py b/Lib/multiprocessing/connection.py index 46a1af6e83af03..a773df1161d656 100644 --- a/Lib/multiprocessing/connection.py +++ b/Lib/multiprocessing/connection.py @@ -12,6 +12,7 @@ import errno import io import os +import random import sys import socket import struct @@ -75,7 +76,7 @@ def arbitrary_address(family): elif family == 'AF_UNIX': return tempfile.mktemp(prefix='sock-', dir=util.get_temp_dir()) elif family == 'AF_PIPE': - return fr'\\.\pipe\pyc-{os.getpid()}-{next(tempfile._get_candidate_names())}' + return r'\\.\pipe\pyc-' + random.randbytes(8).hex() else: raise ValueError('unrecognized family') From 5243b9ea7790fec7b184056b04fcea0f51a87ed9 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 5 Feb 2026 20:44:05 +0200 Subject: [PATCH 4/4] Use os.urandom(). --- Lib/asyncio/windows_utils.py | 3 +-- Lib/multiprocessing/connection.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Lib/asyncio/windows_utils.py b/Lib/asyncio/windows_utils.py index aac01acba5f093..216e79eff39dc3 100644 --- a/Lib/asyncio/windows_utils.py +++ b/Lib/asyncio/windows_utils.py @@ -8,7 +8,6 @@ import _winapi import msvcrt import os -import random import subprocess import warnings @@ -51,7 +50,7 @@ def pipe(*, duplex=False, overlapped=(True, True), bufsize=BUFSIZE): h1 = h2 = None try: while True: - address = r'\\.\pipe\python-pipe-' + random.randbytes(8).hex() + address = r'\\.\pipe\python-pipe-' + os.urandom(8).hex() try: h1 = _winapi.CreateNamedPipe( address, openmode, _winapi.PIPE_WAIT, diff --git a/Lib/multiprocessing/connection.py b/Lib/multiprocessing/connection.py index d5915b6339e760..5a84210fa96ec4 100644 --- a/Lib/multiprocessing/connection.py +++ b/Lib/multiprocessing/connection.py @@ -12,7 +12,6 @@ import errno import io import os -import random import sys import socket import struct @@ -76,7 +75,7 @@ def arbitrary_address(family): elif family == 'AF_UNIX': return tempfile.mktemp(prefix='sock-', dir=util.get_temp_dir()) elif family == 'AF_PIPE': - return r'\\.\pipe\pyc-' + random.randbytes(8).hex() + return r'\\.\pipe\pyc-' + os.urandom(8).hex() else: raise ValueError('unrecognized family')