From 03077d5fc3103ccc6cb03a02d14b5e63ac6be06c Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 4 Aug 2026 14:23:22 +0200 Subject: [PATCH 1/4] Add runtime guards for dup3 & pipe2 Based on GH-154718 Co-Authored-By: Vamsi-klu --- Doc/library/os.rst | 2 +- Lib/test/test_os/test_posix.py | 16 +++ ...-08-04-14-14-31.gh-issue-153711.PBpc1g.rst | 4 + Modules/posixmodule.c | 110 ++++++++++++++---- 4 files changed, 110 insertions(+), 22 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-04-14-14-31.gh-issue-153711.PBpc1g.rst diff --git a/Doc/library/os.rst b/Doc/library/os.rst index 7bdf415db655f32..0a4a02c45b533bd 100644 --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -1448,7 +1448,7 @@ or `the MSDN `_ on Windo Return a pair of file descriptors ``(r, w)`` usable for reading and writing, respectively. - .. availability:: Unix, not WASI, not macOS, not iOS. + .. availability:: Unix, macOS >= 27.0, not WASI, not iOS. .. versionadded:: 3.3 diff --git a/Lib/test/test_os/test_posix.py b/Lib/test/test_os/test_posix.py index 41a730708974c25..8743b0bf0bc4939 100644 --- a/Lib/test/test_os/test_posix.py +++ b/Lib/test/test_os/test_posix.py @@ -2388,6 +2388,22 @@ def test_pwritev(self): self.assertNotHasAttr(os, "pwritev") self.assertNotHasAttr(os, "preadv") + def test_pipe2(self): + self._verify_available("HAVE_PIPE2") + if self.mac_ver >= (27, 0): + self.assertHasAttr(os, "pipe2") + else: + self.assertNotHasAttr(os, "pipe2") + + def test_dup3(self): + self._verify_available("HAVE_DUP3") + r, w = os.pipe() + self.addCleanup(os.close, r) + self.addCleanup(os.close, w) + # Must not crash even when dup3 unavailable at runtime. + # os.dup2 returns fd2 (here w); do not double-close. + os.dup2(r, w, inheritable=False) + def test_stat(self): self._verify_available("HAVE_FSTATAT") if self.mac_ver >= (10, 10): diff --git a/Misc/NEWS.d/next/Library/2026-08-04-14-14-31.gh-issue-153711.PBpc1g.rst b/Misc/NEWS.d/next/Library/2026-08-04-14-14-31.gh-issue-153711.PBpc1g.rst new file mode 100644 index 000000000000000..9552dde73918c43 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-04-14-14-31.gh-issue-153711.PBpc1g.rst @@ -0,0 +1,4 @@ +On macOS, add run-time checks around the syscalls :manpage:`pipe2 (2)` and +:manpage:`dup3 (2)`, in addition to the existing build-time checks. This +means that Python built on macOS 27 (where these calls are available) can +run on macOS 26 (where they aren't). diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index c34e3fc5eb600df..a58a43cc09c223e 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -504,6 +504,8 @@ static const unsigned int _Py_STATX_KNOWN = (STATX_BASIC_STATS | STATX_BTIME # define HAVE_MKFIFOAT_RUNTIME __builtin_available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) # define HAVE_MKNODAT_RUNTIME __builtin_available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) # define HAVE_PTSNAME_R_RUNTIME __builtin_available(macOS 10.13.4, iOS 11.3, tvOS 11.3, watchOS 4.3, *) +# define HAVE_DUP3_RUNTIME __builtin_available(macOS 27.0, *) +# define HAVE_PIPE2_RUNTIME __builtin_available(macOS 27.0, *) # define HAVE_POSIX_SPAWN_SETSID_RUNTIME __builtin_available(macOS 10.15, *) @@ -589,6 +591,14 @@ static const unsigned int _Py_STATX_KNOWN = (STATX_BASIC_STATS | STATX_BTIME # define HAVE_PTSNAME_R_RUNTIME (ptsname_r != NULL) # endif +# ifdef HAVE_DUP3_RUNTIME +# define HAVE_DUP3_RUNTIME (dup3 != NULL) +# endif + +# ifdef HAVE_PIPE2_RUNTIME +# define HAVE_PIPE2_RUNTIME (pipe2 != NULL) +# endif + #endif #ifdef HAVE_FUTIMESAT @@ -619,6 +629,8 @@ static const unsigned int _Py_STATX_KNOWN = (STATX_BASIC_STATS | STATX_BTIME # define HAVE_MKFIFOAT_RUNTIME 1 # define HAVE_MKNODAT_RUNTIME 1 # define HAVE_PTSNAME_R_RUNTIME 1 +# define HAVE_DUP3_RUNTIME 1 +# define HAVE_PIPE2_RUNTIME 1 #endif @@ -11866,11 +11878,13 @@ os_dup2_impl(PyObject *module, int fd, int fd2, int inheritable) /*[clinic end generated code: output=bc059d34a73404d1 input=c3cddda8922b038d]*/ { int res = 0; -#if defined(HAVE_DUP3) && \ - !(defined(HAVE_FCNTL_H) && defined(F_DUP2FD_CLOEXEC)) - /* dup3() is available on Linux 2.6.27+ and glibc 2.9 */ + + /* dup3() is available on Linux 2.6.27+ and glibc 2.9 and macOS 27.0; + * it needs runtime detection for the case of running on older kernels. + * Values: -1: unknown; 0: doesn't work; 1: works + */ static int dup3_works = -1; -#endif + (void) dup3_works; // unused on some platforms /* dup2() can fail with EINTR if the target FD is already open, because it * then has to be closed. See os_close_impl() for why we don't handle EINTR @@ -11910,17 +11924,23 @@ os_dup2_impl(PyObject *module, int fd, int fd2, int inheritable) #ifdef HAVE_DUP3 if (!inheritable && dup3_works != 0) { - Py_BEGIN_ALLOW_THREADS - res = dup3(fd, fd2, O_CLOEXEC); - Py_END_ALLOW_THREADS - if (res < 0) { - if (dup3_works == -1) - dup3_works = (errno != ENOSYS); - if (dup3_works) { - posix_error(); - return -1; + if (HAVE_DUP3_RUNTIME) { + Py_BEGIN_ALLOW_THREADS + res = dup3(fd, fd2, O_CLOEXEC); + Py_END_ALLOW_THREADS + if (res < 0) { + if (dup3_works == -1) { + dup3_works = (errno != ENOSYS); + } + if (dup3_works) { + posix_error(); + return -1; + } } } + else { + dup3_works = 0; + } } if (inheritable || dup3_works == 0) @@ -12761,7 +12781,14 @@ os_pipe_impl(PyObject *module) SECURITY_ATTRIBUTES attr; BOOL ok; #else - int res; + int res = -1; + + /* pipe2() is available on some newer linux/glibc & macOS; + * it needs runtime detection for the case of running on older kernels. + * Values: -1: unknown; 0: doesn't work; 1: works + */ + static int pipe2_works = -1; + (void) pipe2_works; // unused on some platforms #endif #ifdef MS_WINDOWS @@ -12787,11 +12814,27 @@ os_pipe_impl(PyObject *module) #else #ifdef HAVE_PIPE2 - Py_BEGIN_ALLOW_THREADS - res = pipe2(fds, O_CLOEXEC); - Py_END_ALLOW_THREADS + if (pipe2_works != 0) { + if (HAVE_PIPE2_RUNTIME) { + Py_BEGIN_ALLOW_THREADS + res = pipe2(fds, O_CLOEXEC); + Py_END_ALLOW_THREADS + if (pipe2_works == -1) { + if (res != 0 && errno == ENOSYS) { + pipe2_works = 0; + } + else { + // pipe2 is present but this call failed + pipe2_works = 1; + } + } + } + else { + pipe2_works = 0; + } + } - if (res != 0 && errno == ENOSYS) + if (pipe2_works == 0) { #endif Py_BEGIN_ALLOW_THREADS @@ -12814,8 +12857,9 @@ os_pipe_impl(PyObject *module) } #endif - if (res != 0) + if (res != 0) { return PyErr_SetFromErrno(PyExc_OSError); + } #endif /* !MS_WINDOWS */ return Py_BuildValue("(ii)", fds[0], fds[1]); } @@ -12845,9 +12889,17 @@ os_pipe2_impl(PyObject *module, int flags) int fds[2]; int res; - res = pipe2(fds, flags); - if (res != 0) + if (HAVE_PIPE2_RUNTIME) { + res = pipe2(fds, flags); + } + else { + res = -1; + errno = ENOSYS; + } + if (res != 0) { return posix_error(); + } + return Py_BuildValue("(ii)", fds[0], fds[1]); } #endif /* HAVE_PIPE2 */ @@ -18844,6 +18896,22 @@ posixmodule_exec(PyObject *m) } #endif +#if HAVE_PIPE2 + if (HAVE_PIPE2_RUNTIME) { + // Do nothing. (`__builtin_available` doesn't allow `!`; see + // "using negations" in a comment above.) + } + else { + PyObject* dct = PyModule_GetDict(m); + if (dct == NULL) { + return -1; + } + if (PyDict_PopString(dct, "pipe2", NULL) < 0) { + return -1; + } + } +#endif + /* Initialize environ dictionary */ if (PyModule_Add(m, "environ", convertenviron()) != 0) { return -1; From 5ba1a2093ecce0859b293f1ca637d80bb0999da8 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 4 Aug 2026 15:30:24 +0200 Subject: [PATCH 2/4] Add entry for c-analyzer --- Tools/c-analyzer/cpython/ignored.tsv | 1 + 1 file changed, 1 insertion(+) diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index ef314625d507d61..d47c1c6fa3976a4 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -22,6 +22,7 @@ Python/fileutils.c set_inheritable ioctl_works - # (set lazily, *after* first init) # XXX Is this thread-safe? Modules/posixmodule.c os_dup2_impl dup3_works - +Modules/posixmodule.c os_pipe_impl pipe2_works - ## guards around resource init Python/thread_pthread.h PyThread__init_thread lib_initialized - From 1126ba4ec1d255e60b796e60e216dcee9ae756f0 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 4 Aug 2026 16:22:41 +0200 Subject: [PATCH 3/4] Use relaxed atomics for thread safety --- Modules/posixmodule.c | 20 ++++++++++++++------ Tools/c-analyzer/cpython/ignored.tsv | 3 +-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index a58a43cc09c223e..c6b4d9940d342c4 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -11882,9 +11882,12 @@ os_dup2_impl(PyObject *module, int fd, int fd2, int inheritable) /* dup3() is available on Linux 2.6.27+ and glibc 2.9 and macOS 27.0; * it needs runtime detection for the case of running on older kernels. * Values: -1: unknown; 0: doesn't work; 1: works + * For thread safety, use a process-global with one read & one store, + * both relaxed. (It's fine if two threads race and do the detection + * simultaneously; they should get the same result.) */ - static int dup3_works = -1; - (void) dup3_works; // unused on some platforms + static int dup3_works_atomic = -1; + (void) dup3_works_atomic; // unused on some platforms /* dup2() can fail with EINTR if the target FD is already open, because it * then has to be closed. See os_close_impl() for why we don't handle EINTR @@ -11923,6 +11926,7 @@ os_dup2_impl(PyObject *module, int fd, int fd2, int inheritable) #else #ifdef HAVE_DUP3 + int dup3_works = FT_ATOMIC_LOAD_INT_RELAXED(dup3_works_atomic); if (!inheritable && dup3_works != 0) { if (HAVE_DUP3_RUNTIME) { Py_BEGIN_ALLOW_THREADS @@ -11931,6 +11935,7 @@ os_dup2_impl(PyObject *module, int fd, int fd2, int inheritable) if (res < 0) { if (dup3_works == -1) { dup3_works = (errno != ENOSYS); + FT_ATOMIC_STORE_INT_RELAXED(dup3_works_atomic, dup3_works); } if (dup3_works) { posix_error(); @@ -11940,6 +11945,7 @@ os_dup2_impl(PyObject *module, int fd, int fd2, int inheritable) } else { dup3_works = 0; + FT_ATOMIC_STORE_INT_RELAXED(dup3_works_atomic, dup3_works); } } @@ -12784,11 +12790,10 @@ os_pipe_impl(PyObject *module) int res = -1; /* pipe2() is available on some newer linux/glibc & macOS; - * it needs runtime detection for the case of running on older kernels. - * Values: -1: unknown; 0: doesn't work; 1: works + * use the same runtime detection as for dup3 above. */ - static int pipe2_works = -1; - (void) pipe2_works; // unused on some platforms + static int pipe2_works_atomic = -1; + (void) pipe2_works_atomic; // unused on some platforms #endif #ifdef MS_WINDOWS @@ -12814,6 +12819,7 @@ os_pipe_impl(PyObject *module) #else #ifdef HAVE_PIPE2 + int pipe2_works = FT_ATOMIC_LOAD_INT_RELAXED(pipe2_works_atomic); if (pipe2_works != 0) { if (HAVE_PIPE2_RUNTIME) { Py_BEGIN_ALLOW_THREADS @@ -12827,10 +12833,12 @@ os_pipe_impl(PyObject *module) // pipe2 is present but this call failed pipe2_works = 1; } + FT_ATOMIC_STORE_INT_RELAXED(pipe2_works_atomic, pipe2_works); } } else { pipe2_works = 0; + FT_ATOMIC_STORE_INT_RELAXED(pipe2_works_atomic, pipe2_works); } } diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index d47c1c6fa3976a4..857b1b55994ef4e 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -19,8 +19,7 @@ Python/bootstrap_hash.c py_getrandom getrandom_works - Python/bootstrap_hash.c py_getentropy getentropy_works - Python/fileutils.c - _Py_open_cloexec_works - Python/fileutils.c set_inheritable ioctl_works - -# (set lazily, *after* first init) -# XXX Is this thread-safe? +# (set lazily, atomically, *after* first init) Modules/posixmodule.c os_dup2_impl dup3_works - Modules/posixmodule.c os_pipe_impl pipe2_works - From 9edc83d42765bad4c0deb8c45b7e8fc715f760d6 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 4 Aug 2026 17:18:14 +0200 Subject: [PATCH 4/4] Update variable names --- Tools/c-analyzer/cpython/ignored.tsv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index 857b1b55994ef4e..4c143164650a2ba 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -20,8 +20,8 @@ Python/bootstrap_hash.c py_getentropy getentropy_works - Python/fileutils.c - _Py_open_cloexec_works - Python/fileutils.c set_inheritable ioctl_works - # (set lazily, atomically, *after* first init) -Modules/posixmodule.c os_dup2_impl dup3_works - -Modules/posixmodule.c os_pipe_impl pipe2_works - +Modules/posixmodule.c os_dup2_impl dup3_works_atomic - +Modules/posixmodule.c os_pipe_impl pipe2_works_atomic - ## guards around resource init Python/thread_pthread.h PyThread__init_thread lib_initialized -