diff --git a/Doc/library/os.rst b/Doc/library/os.rst index be7ea26356bebe3..0a8c302c7516294 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, not WASI, macOS >= 27.0, iOS >= 27.0. .. 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/macOS/2026-07-26-07-01-41.gh-issue-153711.ad8g_Q.rst b/Misc/NEWS.d/next/macOS/2026-07-26-07-01-41.gh-issue-153711.ad8g_Q.rst new file mode 100644 index 000000000000000..0456b10b47b2193 --- /dev/null +++ b/Misc/NEWS.d/next/macOS/2026-07-26-07-01-41.gh-issue-153711.ad8g_Q.rst @@ -0,0 +1,3 @@ +Guard ``os.dup2`` and ``os.pipe``/``os.pipe2`` against calling ``dup3``/``pipe2`` +when the symbols are weak-imported but unavailable at runtime (Xcode 27 SDKs on +older macOS). diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 350f56c393d3b97..a46f9750be3d0b9 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, iOS 27.0, tvOS 27.0, watchOS 27.0, *) +# define HAVE_PIPE2_RUNTIME __builtin_available(macOS 27.0, iOS 27.0, tvOS 27.0, watchOS 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 +# define HAVE_DUP3_RUNTIME (dup3 != NULL) +# endif + +# ifdef HAVE_PIPE2 +# 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 @@ -11910,17 +11922,22 @@ 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) @@ -12779,9 +12796,13 @@ os_pipe_impl(PyObject *module) #else #ifdef HAVE_PIPE2 - Py_BEGIN_ALLOW_THREADS - res = pipe2(fds, O_CLOEXEC); - Py_END_ALLOW_THREADS + res = -1; + errno = ENOSYS; + if (HAVE_PIPE2_RUNTIME) { + Py_BEGIN_ALLOW_THREADS + res = pipe2(fds, O_CLOEXEC); + Py_END_ALLOW_THREADS + } if (res != 0 && errno == ENOSYS) { @@ -18811,6 +18832,18 @@ posixmodule_exec(PyObject *m) } #endif +#if defined(HAVE_PIPE2) + if (HAVE_PIPE2_RUNTIME) {} else { + PyObject *dct = PyModule_GetDict(m); + if (dct == NULL) { + return -1; + } + if (PyDict_PopString(dct, "pipe2", NULL) < 0) { + return -1; + } + } +#endif + #ifdef HAVE_STATX if (statx == NULL) { PyObject* dct = PyModule_GetDict(m);