Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1448,7 +1448,7 @@ or `the MSDN <https://msdn.microsoft.com/en-us/library/z0kc8e3z.aspx>`_ 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

Expand Down
16 changes: 16 additions & 0 deletions Lib/test/test_os/test_posix.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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).
120 changes: 98 additions & 22 deletions Modules/posixmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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, *)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -11866,11 +11878,16 @@ 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 */
static int dup3_works = -1;
#endif

/* 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_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
Expand Down Expand Up @@ -11909,18 +11926,27 @@ 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) {
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);
FT_ATOMIC_STORE_INT_RELAXED(dup3_works_atomic, dup3_works);
}
if (dup3_works) {
posix_error();
return -1;
}
}
}
else {
dup3_works = 0;
FT_ATOMIC_STORE_INT_RELAXED(dup3_works_atomic, dup3_works);
}
}

if (inheritable || dup3_works == 0)
Expand Down Expand Up @@ -12761,7 +12787,13 @@ 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;
* use the same runtime detection as for dup3 above.
*/
static int pipe2_works_atomic = -1;
(void) pipe2_works_atomic; // unused on some platforms
#endif

#ifdef MS_WINDOWS
Expand All @@ -12787,11 +12819,30 @@ os_pipe_impl(PyObject *module)
#else

#ifdef HAVE_PIPE2
Py_BEGIN_ALLOW_THREADS
res = pipe2(fds, O_CLOEXEC);
Py_END_ALLOW_THREADS
int pipe2_works = FT_ATOMIC_LOAD_INT_RELAXED(pipe2_works_atomic);
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;
}
FT_ATOMIC_STORE_INT_RELAXED(pipe2_works_atomic, pipe2_works);
}
}
else {
pipe2_works = 0;
FT_ATOMIC_STORE_INT_RELAXED(pipe2_works_atomic, pipe2_works);
}
}

if (res != 0 && errno == ENOSYS)
if (pipe2_works == 0)
{
#endif
Py_BEGIN_ALLOW_THREADS
Expand All @@ -12814,8 +12865,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]);
}
Expand Down Expand Up @@ -12845,9 +12897,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 */
Expand Down Expand Up @@ -18844,6 +18904,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;
Expand Down
6 changes: 3 additions & 3 deletions Tools/c-analyzer/cpython/ignored.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ 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?
Modules/posixmodule.c os_dup2_impl dup3_works -
# (set lazily, atomically, *after* first init)
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 -
Expand Down
Loading