Skip to content

Commit d0212f6

Browse files
authored
gh-149527: fix creation of temporary socket files post GH-148578 (#149597)
1 parent 58bde1a commit d0212f6

4 files changed

Lines changed: 89 additions & 18 deletions

File tree

‎Lib/multiprocessing/connection.py‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,12 @@ def arbitrary_address(family):
7676
if family == 'AF_INET':
7777
return ('localhost', 0)
7878
elif family == 'AF_UNIX':
79-
# NOTE: util.get_temp_dir() is a 0o700 per-process directory. A
80-
# mktemp-style ToC vs ToU concern is not important; bind() surfaces
81-
# the extremely unlikely collision as EADDRINUSE.
79+
# NOTE: util.get_temp_dir() is a 0o700 per-process directory.
80+
# A mktemp-style ToC vs ToU concern is not important as bind()
81+
# surfaces the extremely unlikely collision as EADDRINUSE.
82+
suffix = os.urandom(util._TMPSOCK_SUFFIXLEN // 2).hex()
8283
return os.path.join(util.get_temp_dir(),
83-
f'sock-{os.urandom(6).hex()}')
84+
f"{util._TMPSOCK_PREFIX}{suffix}")
8485
elif family == 'AF_PIPE':
8586
return (r'\\.\pipe\pyc-%d-%d-%s' %
8687
(os.getpid(), next(_mmap_counter), os.urandom(8).hex()))

‎Lib/multiprocessing/util.py‎

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,41 @@ def is_abstract_socket_namespace(address):
143143
# On Windows platforms, we do not create AF_UNIX sockets.
144144
_SUN_PATH_MAX = None if os.name == 'nt' else 92
145145

146+
# The temporary socket file path (without NULL terminator) is
147+
#
148+
# TEMPDIR/{_TMPPYMP_PREFIX}{S1}/{_TMPSOCK_PREFIX}{S2}
149+
#
150+
# where
151+
#
152+
# - S1 is a random suffix of length 8 generated by get_temp_dir().
153+
# This is used to generate the temporary sub-directory that will
154+
# contains the socket file. Update _TMPPYMP_SUFFIXLEN accordingly
155+
# if the random suffix length chosen by tempfile.mkdtemp() changes.
156+
#
157+
# - S2 is a random suffix of length 2N generated by os.urandom(N).hex()
158+
# in multiprocessing.connection.arbitrary_address(). The value of N
159+
# is not fixed and may vary across Python versions as to minimize
160+
# collisions and path lengths.
161+
#
162+
_TMPPYMP_PREFIX = "pymp-"
163+
_TMPPYMP_PREFIXLEN = len(_TMPPYMP_PREFIX)
164+
_TMPPYMP_SUFFIXLEN = 8
165+
_TMPSOCK_PREFIX = "sock-"
166+
_TMPSOCK_PREFIXLEN = len(_TMPSOCK_PREFIX)
167+
_TMPSOCK_SUFFIXLEN = 12
168+
169+
# Length of the socket filepath from the chosen temporary directory,
170+
# including a leading path separator and the path separator between
171+
# the temporary subdirectory and the socket filename.
172+
_SUN_PATH_LEN_RESERVED = (
173+
len(os.path.sep) + _TMPPYMP_PREFIXLEN + _TMPPYMP_SUFFIXLEN +
174+
len(os.path.sep) + _TMPSOCK_PREFIXLEN + _TMPSOCK_SUFFIXLEN
175+
)
176+
assert (
177+
_SUN_PATH_MAX is None
178+
or _SUN_PATH_LEN_RESERVED < _SUN_PATH_MAX
179+
)
180+
146181

147182
def _remove_temp_dir(rmtree, tempdir):
148183
rmtree(tempdir)
@@ -161,24 +196,16 @@ def _get_base_temp_dir(tempfile):
161196
"""
162197
if os.name == 'nt':
163198
return None
164-
# Most of the time, the default temporary directory is /tmp. Thus,
165-
# listener sockets files "$TMPDIR/pymp-XXXXXXXX/sock-XXXXXXXX" do
166-
# not have a path length exceeding SUN_PATH_MAX.
199+
# Most of the time, the default temporary directory is /tmp. As such,
200+
# the path length of listener sockets files does not exceed SUN_PATH_MAX.
167201
#
168202
# If users specify their own temporary directory, we may be unable
169203
# to create those files. Therefore, we fall back to the system-wide
170204
# temporary directory /tmp, assumed to exist on POSIX systems.
171205
#
172206
# See https://github.com/python/cpython/issues/132124.
173207
base_tempdir = tempfile.gettempdir()
174-
# Files created in a temporary directory are suffixed by a string
175-
# generated by tempfile._RandomNameSequence, which, by design,
176-
# is 8 characters long.
177-
#
178-
# Thus, the socket file path length (without NULL terminator) will be:
179-
#
180-
# len(base_tempdir + '/pymp-XXXXXXXX' + '/sock-XXXXXXXX')
181-
sun_path_len = len(base_tempdir) + 14 + 14
208+
sun_path_len = len(base_tempdir) + _SUN_PATH_LEN_RESERVED
182209
# Strict inequality to account for the NULL terminator.
183210
# See https://github.com/python/cpython/issues/140734.
184211
if sun_path_len < _SUN_PATH_MAX:
@@ -206,8 +233,8 @@ def _get_base_temp_dir(tempfile):
206233
# not be able to write socket files out there.
207234
return base_tempdir
208235
warn("Ignoring user-defined temporary directory: %s", base_tempdir)
209-
# at most max(map(len, dirlist)) + 14 + 14 = 36 characters
210-
assert len(base_system_tempdir) + 14 + 14 < _SUN_PATH_MAX
236+
# The following assertion must be satisfied with the chosen constants.
237+
assert len(base_system_tempdir) + _SUN_PATH_LEN_RESERVED < _SUN_PATH_MAX
211238
return base_system_tempdir
212239

213240

@@ -218,7 +245,7 @@ def get_temp_dir():
218245
# Deferred until first use to keep importing this module cheap.
219246
import shutil, tempfile
220247
base_tempdir = _get_base_temp_dir(tempfile)
221-
tempdir = tempfile.mkdtemp(prefix='pymp-', dir=base_tempdir)
248+
tempdir = tempfile.mkdtemp(prefix=_TMPPYMP_PREFIX, dir=base_tempdir)
222249
info('created temp directory %s', tempdir)
223250
# keep a strong reference to shutil.rmtree(), since the finalizer
224251
# can be called late during Python shutdown
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,47 @@
1+
import os
2+
import re
13
import unittest
4+
from multiprocessing.util import _SUN_PATH_MAX
25
from test._test_multiprocessing import install_tests_in_module_dict
6+
from test.support import os_helper, script_helper
37

48
install_tests_in_module_dict(globals(), 'forkserver', exclude_types=True)
59

10+
11+
class TestForkServerConfiguration(unittest.TestCase):
12+
def test_respect_sun_path_max(self):
13+
# Ensure that the calculation for temporary filepath lengths is correct.
14+
# See https://github.com/python/cpython/issues/149527.
15+
16+
cmd = '''if 1:
17+
from multiprocessing.connection import arbitrary_address
18+
from multiprocessing.util import get_temp_dir
19+
if __name__ == "__main__":
20+
print(get_temp_dir())
21+
print(arbitrary_address("AF_UNIX"))
22+
'''
23+
with os_helper.temp_dir() as root:
24+
self.assertLess(len(root), _SUN_PATH_MAX)
25+
_, out, _ = script_helper.assert_python_ok('-c', cmd, TMPDIR=root)
26+
res = out.decode().strip().splitlines()
27+
self.assertEqual(len(res), 2)
28+
29+
# get_temp_dir() with a TMPDIR that has a too large length ends up
30+
# NOT being used as it is not possible to use it. In particular,
31+
# the corresponding path will fallback to one of the "built-in"
32+
# ones such as '/tmp', '/var/tmp', or '/usr/tmp'.
33+
root_regex = "|".join(map(re.escape, [root, "/tmp", "/var/tmp", "/usr/tmp"]))
34+
35+
temp_pymp = res[0]
36+
self.assertLess(len(temp_pymp), _SUN_PATH_MAX)
37+
temp_pymp_regex = os.path.join(root_regex, r"pymp-\w{8}")
38+
self.assertRegex(temp_pymp, temp_pymp_regex)
39+
40+
temp_sock = res[1]
41+
self.assertLess(len(temp_sock), _SUN_PATH_MAX)
42+
temp_sock_regex = os.path.join(temp_pymp_regex, r"sock-[0-9a-fA-F]{12}")
43+
self.assertRegex(temp_sock, temp_sock_regex)
44+
45+
646
if __name__ == '__main__':
747
unittest.main()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:mod:`multiprocessing`: fix creation of temporary socket files following
2+
:gh:`137335` (`PR#148578 <https://github.com/python/cpython/pull/148578>`__
3+
and corresponding backports). Patch by Bénédikt Tran.

0 commit comments

Comments
 (0)