Skip to content

Commit a7f3e56

Browse files
miss-islingtonencukouVamsi-kluned-deily
authored
[3.15] gh-153711: Add runtime guards for dup3 & pipe2 (GH-155174) (#155202)
gh-153711: Add runtime guards for dup3 & pipe2 (GH-155174) (cherry picked from commit f33a5f5) Co-authored-by: Petr Viktorin <encukou@gmail.com> Co-authored-by: Vamsi-klu <nrvamsi13@gmail.com> Co-authored-by: Ned Deily <nad@python.org>
1 parent eea1a49 commit a7f3e56

5 files changed

Lines changed: 122 additions & 26 deletions

File tree

Doc/library/os.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1448,7 +1448,7 @@ or `the MSDN <https://msdn.microsoft.com/en-us/library/z0kc8e3z.aspx>`_ on Windo
14481448
Return a pair of file descriptors ``(r, w)`` usable for reading and writing,
14491449
respectively.
14501450

1451-
.. availability:: Unix, not WASI, not macOS, not iOS.
1451+
.. availability:: Unix, macOS >= 27.0, not WASI, not iOS.
14521452

14531453
.. versionadded:: 3.3
14541454

Lib/test/test_os/test_posix.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2350,6 +2350,22 @@ def test_pwritev(self):
23502350
self.assertNotHasAttr(os, "pwritev")
23512351
self.assertNotHasAttr(os, "preadv")
23522352

2353+
def test_pipe2(self):
2354+
self._verify_available("HAVE_PIPE2")
2355+
if self.mac_ver >= (27, 0):
2356+
self.assertHasAttr(os, "pipe2")
2357+
else:
2358+
self.assertNotHasAttr(os, "pipe2")
2359+
2360+
def test_dup3(self):
2361+
self._verify_available("HAVE_DUP3")
2362+
r, w = os.pipe()
2363+
self.addCleanup(os.close, r)
2364+
self.addCleanup(os.close, w)
2365+
# Must not crash even when dup3 unavailable at runtime.
2366+
# os.dup2 returns fd2 (here w); do not double-close.
2367+
os.dup2(r, w, inheritable=False)
2368+
23532369
def test_stat(self):
23542370
self._verify_available("HAVE_FSTATAT")
23552371
if self.mac_ver >= (10, 10):
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
On macOS, add run-time checks around the syscalls :manpage:`pipe2 (2)` and
2+
:manpage:`dup3 (2)`, in addition to the existing build-time checks. This
3+
means that Python built on macOS 27 (where these calls are available) can
4+
run on macOS 26 (where they aren't).

Modules/posixmodule.c

Lines changed: 98 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,8 @@ static const unsigned int _Py_STATX_KNOWN = (STATX_BASIC_STATS | STATX_BTIME
504504
# define HAVE_MKFIFOAT_RUNTIME __builtin_available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
505505
# define HAVE_MKNODAT_RUNTIME __builtin_available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
506506
# define HAVE_PTSNAME_R_RUNTIME __builtin_available(macOS 10.13.4, iOS 11.3, tvOS 11.3, watchOS 4.3, *)
507+
# define HAVE_DUP3_RUNTIME __builtin_available(macOS 27.0, *)
508+
# define HAVE_PIPE2_RUNTIME __builtin_available(macOS 27.0, *)
507509

508510
# define HAVE_POSIX_SPAWN_SETSID_RUNTIME __builtin_available(macOS 10.15, *)
509511

@@ -589,6 +591,14 @@ static const unsigned int _Py_STATX_KNOWN = (STATX_BASIC_STATS | STATX_BTIME
589591
# define HAVE_PTSNAME_R_RUNTIME (ptsname_r != NULL)
590592
# endif
591593

594+
# ifdef HAVE_DUP3
595+
# define HAVE_DUP3_RUNTIME (dup3 != NULL)
596+
# endif
597+
598+
# ifdef HAVE_PIPE2
599+
# define HAVE_PIPE2_RUNTIME (pipe2 != NULL)
600+
# endif
601+
592602
#endif
593603

594604
#ifdef HAVE_FUTIMESAT
@@ -619,6 +629,8 @@ static const unsigned int _Py_STATX_KNOWN = (STATX_BASIC_STATS | STATX_BTIME
619629
# define HAVE_MKFIFOAT_RUNTIME 1
620630
# define HAVE_MKNODAT_RUNTIME 1
621631
# define HAVE_PTSNAME_R_RUNTIME 1
632+
# define HAVE_DUP3_RUNTIME 1
633+
# define HAVE_PIPE2_RUNTIME 1
622634
#endif
623635

624636

@@ -11833,11 +11845,16 @@ os_dup2_impl(PyObject *module, int fd, int fd2, int inheritable)
1183311845
/*[clinic end generated code: output=bc059d34a73404d1 input=c3cddda8922b038d]*/
1183411846
{
1183511847
int res = 0;
11836-
#if defined(HAVE_DUP3) && \
11837-
!(defined(HAVE_FCNTL_H) && defined(F_DUP2FD_CLOEXEC))
11838-
/* dup3() is available on Linux 2.6.27+ and glibc 2.9 */
11839-
static int dup3_works = -1;
11840-
#endif
11848+
11849+
/* dup3() is available on Linux 2.6.27+ and glibc 2.9 and macOS 27.0;
11850+
* it needs runtime detection for the case of running on older kernels.
11851+
* Values: -1: unknown; 0: doesn't work; 1: works
11852+
* For thread safety, use a process-global with one read & one store,
11853+
* both relaxed. (It's fine if two threads race and do the detection
11854+
* simultaneously; they should get the same result.)
11855+
*/
11856+
static int dup3_works_atomic = -1;
11857+
(void) dup3_works_atomic; // unused on some platforms
1184111858

1184211859
/* dup2() can fail with EINTR if the target FD is already open, because it
1184311860
* then has to be closed. See os_close_impl() for why we don't handle EINTR
@@ -11876,18 +11893,27 @@ os_dup2_impl(PyObject *module, int fd, int fd2, int inheritable)
1187611893
#else
1187711894

1187811895
#ifdef HAVE_DUP3
11896+
int dup3_works = FT_ATOMIC_LOAD_INT_RELAXED(dup3_works_atomic);
1187911897
if (!inheritable && dup3_works != 0) {
11880-
Py_BEGIN_ALLOW_THREADS
11881-
res = dup3(fd, fd2, O_CLOEXEC);
11882-
Py_END_ALLOW_THREADS
11883-
if (res < 0) {
11884-
if (dup3_works == -1)
11885-
dup3_works = (errno != ENOSYS);
11886-
if (dup3_works) {
11887-
posix_error();
11888-
return -1;
11898+
if (HAVE_DUP3_RUNTIME) {
11899+
Py_BEGIN_ALLOW_THREADS
11900+
res = dup3(fd, fd2, O_CLOEXEC);
11901+
Py_END_ALLOW_THREADS
11902+
if (res < 0) {
11903+
if (dup3_works == -1) {
11904+
dup3_works = (errno != ENOSYS);
11905+
FT_ATOMIC_STORE_INT_RELAXED(dup3_works_atomic, dup3_works);
11906+
}
11907+
if (dup3_works) {
11908+
posix_error();
11909+
return -1;
11910+
}
1188911911
}
1189011912
}
11913+
else {
11914+
dup3_works = 0;
11915+
FT_ATOMIC_STORE_INT_RELAXED(dup3_works_atomic, dup3_works);
11916+
}
1189111917
}
1189211918

1189311919
if (inheritable || dup3_works == 0)
@@ -12728,7 +12754,13 @@ os_pipe_impl(PyObject *module)
1272812754
SECURITY_ATTRIBUTES attr;
1272912755
BOOL ok;
1273012756
#else
12731-
int res;
12757+
int res = -1;
12758+
12759+
/* pipe2() is available on some newer linux/glibc & macOS;
12760+
* use the same runtime detection as for dup3 above.
12761+
*/
12762+
static int pipe2_works_atomic = -1;
12763+
(void) pipe2_works_atomic; // unused on some platforms
1273212764
#endif
1273312765

1273412766
#ifdef MS_WINDOWS
@@ -12754,11 +12786,30 @@ os_pipe_impl(PyObject *module)
1275412786
#else
1275512787

1275612788
#ifdef HAVE_PIPE2
12757-
Py_BEGIN_ALLOW_THREADS
12758-
res = pipe2(fds, O_CLOEXEC);
12759-
Py_END_ALLOW_THREADS
12789+
int pipe2_works = FT_ATOMIC_LOAD_INT_RELAXED(pipe2_works_atomic);
12790+
if (pipe2_works != 0) {
12791+
if (HAVE_PIPE2_RUNTIME) {
12792+
Py_BEGIN_ALLOW_THREADS
12793+
res = pipe2(fds, O_CLOEXEC);
12794+
Py_END_ALLOW_THREADS
12795+
if (pipe2_works == -1) {
12796+
if (res != 0 && errno == ENOSYS) {
12797+
pipe2_works = 0;
12798+
}
12799+
else {
12800+
// pipe2 is present but this call failed
12801+
pipe2_works = 1;
12802+
}
12803+
FT_ATOMIC_STORE_INT_RELAXED(pipe2_works_atomic, pipe2_works);
12804+
}
12805+
}
12806+
else {
12807+
pipe2_works = 0;
12808+
FT_ATOMIC_STORE_INT_RELAXED(pipe2_works_atomic, pipe2_works);
12809+
}
12810+
}
1276012811

12761-
if (res != 0 && errno == ENOSYS)
12812+
if (pipe2_works == 0)
1276212813
{
1276312814
#endif
1276412815
Py_BEGIN_ALLOW_THREADS
@@ -12781,8 +12832,9 @@ os_pipe_impl(PyObject *module)
1278112832
}
1278212833
#endif
1278312834

12784-
if (res != 0)
12835+
if (res != 0) {
1278512836
return PyErr_SetFromErrno(PyExc_OSError);
12837+
}
1278612838
#endif /* !MS_WINDOWS */
1278712839
return Py_BuildValue("(ii)", fds[0], fds[1]);
1278812840
}
@@ -12812,9 +12864,17 @@ os_pipe2_impl(PyObject *module, int flags)
1281212864
int fds[2];
1281312865
int res;
1281412866

12815-
res = pipe2(fds, flags);
12816-
if (res != 0)
12867+
if (HAVE_PIPE2_RUNTIME) {
12868+
res = pipe2(fds, flags);
12869+
}
12870+
else {
12871+
res = -1;
12872+
errno = ENOSYS;
12873+
}
12874+
if (res != 0) {
1281712875
return posix_error();
12876+
}
12877+
1281812878
return Py_BuildValue("(ii)", fds[0], fds[1]);
1281912879
}
1282012880
#endif /* HAVE_PIPE2 */
@@ -18803,6 +18863,22 @@ posixmodule_exec(PyObject *m)
1880318863
}
1880418864
#endif
1880518865

18866+
#if HAVE_PIPE2
18867+
if (HAVE_PIPE2_RUNTIME) {
18868+
// Do nothing. (`__builtin_available` doesn't allow `!`; see
18869+
// "using negations" in a comment above.)
18870+
}
18871+
else {
18872+
PyObject* dct = PyModule_GetDict(m);
18873+
if (dct == NULL) {
18874+
return -1;
18875+
}
18876+
if (PyDict_PopString(dct, "pipe2", NULL) < 0) {
18877+
return -1;
18878+
}
18879+
}
18880+
#endif
18881+
1880618882
/* Initialize environ dictionary */
1880718883
if (PyModule_Add(m, "environ", convertenviron()) != 0) {
1880818884
return -1;

Tools/c-analyzer/cpython/ignored.tsv

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ Python/bootstrap_hash.c py_getrandom getrandom_works -
1919
Python/bootstrap_hash.c py_getentropy getentropy_works -
2020
Python/fileutils.c - _Py_open_cloexec_works -
2121
Python/fileutils.c set_inheritable ioctl_works -
22-
# (set lazily, *after* first init)
23-
# XXX Is this thread-safe?
24-
Modules/posixmodule.c os_dup2_impl dup3_works -
22+
# (set lazily, atomically, *after* first init)
23+
Modules/posixmodule.c os_dup2_impl dup3_works_atomic -
24+
Modules/posixmodule.c os_pipe_impl pipe2_works_atomic -
2525

2626
## guards around resource init
2727
Python/thread_pthread.h PyThread__init_thread lib_initialized -

0 commit comments

Comments
 (0)