Skip to content

Commit 80ee5fc

Browse files
committed
[3.11] pythongh-104372: Cleanup _posixsubprocess make_inheritable for async signal safety and no GIL requirement (pythonGH-104518)
Move all of the Python C API calls into the parent process up front instead of doing PyLong_AsLong and PyErr_Occurred and PyTuple_GET from the post-fork/vfork child process. Much of this was long overdue. We shouldn't have been using PyTuple and PyLong APIs within all of these low level functions anyways.. (cherry picked from commit c649df6) Co-authored-by: Gregory P. Smith <[email protected]>
1 parent be20e9c commit 80ee5fc

File tree

2 files changed

+92
-34
lines changed

2 files changed

+92
-34
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Refactored the ``_posixsubprocess`` internals to avoid Python C API usage between fork and exec when marking ``pass_fds=`` file descriptors inheritable.

Modules/_posixsubprocess.c

Lines changed: 91 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -138,16 +138,17 @@ _sanity_check_python_fd_sequence(PyObject *fd_sequence)
138138

139139
/* Is fd found in the sorted Python Sequence? */
140140
static int
141-
_is_fd_in_sorted_fd_sequence(int fd, PyObject *fd_sequence)
141+
_is_fd_in_sorted_fd_sequence(int fd, int *fd_sequence,
142+
Py_ssize_t fd_sequence_len)
142143
{
143144
/* Binary search. */
144145
Py_ssize_t search_min = 0;
145-
Py_ssize_t search_max = PyTuple_GET_SIZE(fd_sequence) - 1;
146+
Py_ssize_t search_max = fd_sequence_len - 1;
146147
if (search_max < 0)
147148
return 0;
148149
do {
149150
long middle = (search_min + search_max) / 2;
150-
long middle_fd = PyLong_AsLong(PyTuple_GET_ITEM(fd_sequence, middle));
151+
long middle_fd = fd_sequence[middle];
151152
if (fd == middle_fd)
152153
return 1;
153154
if (fd > middle_fd)
@@ -158,24 +159,56 @@ _is_fd_in_sorted_fd_sequence(int fd, PyObject *fd_sequence)
158159
return 0;
159160
}
160161

162+
/*
163+
* Do all the Python C API calls in the parent process to turn the pass_fds
164+
* "py_fds_to_keep" tuple into a C array. The caller owns allocation and
165+
* freeing of the array.
166+
*
167+
* On error an unknown number of array elements may have been filled in.
168+
* A Python exception has been set when an error is returned.
169+
*
170+
* Returns: -1 on error, 0 on success.
171+
*/
161172
static int
162-
make_inheritable(PyObject *py_fds_to_keep, int errpipe_write)
173+
convert_fds_to_keep_to_c(PyObject *py_fds_to_keep, int *c_fds_to_keep)
163174
{
164175
Py_ssize_t i, len;
165176

166177
len = PyTuple_GET_SIZE(py_fds_to_keep);
167178
for (i = 0; i < len; ++i) {
168179
PyObject* fdobj = PyTuple_GET_ITEM(py_fds_to_keep, i);
169180
long fd = PyLong_AsLong(fdobj);
170-
assert(!PyErr_Occurred());
171-
assert(0 <= fd && fd <= INT_MAX);
181+
if (PyErr_Occurred()) {
182+
return -1;
183+
}
184+
if (fd < 0 || fd > INT_MAX) {
185+
PyErr_SetString(PyExc_ValueError,
186+
"fd out of range in fds_to_keep.");
187+
return -1;
188+
}
189+
c_fds_to_keep[i] = (int)fd;
190+
}
191+
return 0;
192+
}
193+
194+
195+
/* This function must be async-signal-safe as it is called from child_exec()
196+
* after fork() or vfork().
197+
*/
198+
static int
199+
make_inheritable(int *c_fds_to_keep, Py_ssize_t len, int errpipe_write)
200+
{
201+
Py_ssize_t i;
202+
203+
for (i = 0; i < len; ++i) {
204+
int fd = c_fds_to_keep[i];
172205
if (fd == errpipe_write) {
173-
/* errpipe_write is part of py_fds_to_keep. It must be closed at
206+
/* errpipe_write is part of fds_to_keep. It must be closed at
174207
exec(), but kept open in the child process until exec() is
175208
called. */
176209
continue;
177210
}
178-
if (_Py_set_inheritable_async_safe((int)fd, 1, NULL) < 0)
211+
if (_Py_set_inheritable_async_safe(fd, 1, NULL) < 0)
179212
return -1;
180213
}
181214
return 0;
@@ -211,7 +244,7 @@ safe_get_max_fd(void)
211244

212245

213246
/* Close all file descriptors in the given range except for those in
214-
* py_fds_to_keep by invoking closer on each subrange.
247+
* fds_to_keep by invoking closer on each subrange.
215248
*
216249
* If end_fd == -1, it's guessed via safe_get_max_fd(), but it isn't
217250
* possible to know for sure what the max fd to go up to is for
@@ -221,19 +254,18 @@ safe_get_max_fd(void)
221254
static int
222255
_close_range_except(int start_fd,
223256
int end_fd,
224-
PyObject *py_fds_to_keep,
257+
int *fds_to_keep,
258+
Py_ssize_t fds_to_keep_len,
225259
int (*closer)(int, int))
226260
{
227261
if (end_fd == -1) {
228262
end_fd = Py_MIN(safe_get_max_fd(), INT_MAX);
229263
}
230-
Py_ssize_t num_fds_to_keep = PyTuple_GET_SIZE(py_fds_to_keep);
231264
Py_ssize_t keep_seq_idx;
232-
/* As py_fds_to_keep is sorted we can loop through the list closing
265+
/* As fds_to_keep is sorted we can loop through the list closing
233266
* fds in between any in the keep list falling within our range. */
234-
for (keep_seq_idx = 0; keep_seq_idx < num_fds_to_keep; ++keep_seq_idx) {
235-
PyObject* py_keep_fd = PyTuple_GET_ITEM(py_fds_to_keep, keep_seq_idx);
236-
int keep_fd = PyLong_AsLong(py_keep_fd);
267+
for (keep_seq_idx = 0; keep_seq_idx < fds_to_keep_len; ++keep_seq_idx) {
268+
int keep_fd = fds_to_keep[keep_seq_idx];
237269
if (keep_fd < start_fd)
238270
continue;
239271
if (closer(start_fd, keep_fd - 1) != 0)
@@ -273,7 +305,7 @@ _brute_force_closer(int first, int last)
273305
}
274306

275307
/* Close all open file descriptors in the range from start_fd and higher
276-
* Do not close any in the sorted py_fds_to_keep list.
308+
* Do not close any in the sorted fds_to_keep list.
277309
*
278310
* This version is async signal safe as it does not make any unsafe C library
279311
* calls, malloc calls or handle any locks. It is _unfortunate_ to be forced
@@ -288,14 +320,16 @@ _brute_force_closer(int first, int last)
288320
* it with some cpp #define magic to work on other OSes as well if you want.
289321
*/
290322
static void
291-
_close_open_fds_safe(int start_fd, PyObject* py_fds_to_keep)
323+
_close_open_fds_safe(int start_fd, int *fds_to_keep, Py_ssize_t fds_to_keep_len)
292324
{
293325
int fd_dir_fd;
294326

295327
fd_dir_fd = _Py_open_noraise(FD_DIR, O_RDONLY);
296328
if (fd_dir_fd == -1) {
297329
/* No way to get a list of open fds. */
298-
_close_range_except(start_fd, -1, py_fds_to_keep, _brute_force_closer);
330+
_close_range_except(start_fd, -1,
331+
fds_to_keep, fds_to_keep_len,
332+
_brute_force_closer);
299333
return;
300334
} else {
301335
char buffer[sizeof(struct linux_dirent64)];
@@ -314,7 +348,8 @@ _close_open_fds_safe(int start_fd, PyObject* py_fds_to_keep)
314348
if ((fd = _pos_int_from_ascii(entry->d_name)) < 0)
315349
continue; /* Not a number. */
316350
if (fd != fd_dir_fd && fd >= start_fd &&
317-
!_is_fd_in_sorted_fd_sequence(fd, py_fds_to_keep)) {
351+
!_is_fd_in_sorted_fd_sequence(fd, fds_to_keep,
352+
fds_to_keep_len)) {
318353
close(fd);
319354
}
320355
}
@@ -335,7 +370,7 @@ _unsafe_closer(int first, int last)
335370
}
336371

337372
/* Close all open file descriptors from start_fd and higher.
338-
* Do not close any in the sorted py_fds_to_keep tuple.
373+
* Do not close any in the sorted fds_to_keep tuple.
339374
*
340375
* This function violates the strict use of async signal safe functions. :(
341376
* It calls opendir(), readdir() and closedir(). Of these, the one most
@@ -348,11 +383,13 @@ _unsafe_closer(int first, int last)
348383
* http://womble.decadent.org.uk/readdir_r-advisory.html
349384
*/
350385
static void
351-
_close_open_fds_maybe_unsafe(int start_fd, PyObject* py_fds_to_keep)
386+
_close_open_fds_maybe_unsafe(int start_fd, int *fds_to_keep,
387+
Py_ssize_t fds_to_keep_len)
352388
{
353389
DIR *proc_fd_dir;
354390
#ifndef HAVE_DIRFD
355-
while (_is_fd_in_sorted_fd_sequence(start_fd, py_fds_to_keep)) {
391+
while (_is_fd_in_sorted_fd_sequence(start_fd, fds_to_keep,
392+
fds_to_keep_len)) {
356393
++start_fd;
357394
}
358395
/* Close our lowest fd before we call opendir so that it is likely to
@@ -371,7 +408,8 @@ _close_open_fds_maybe_unsafe(int start_fd, PyObject* py_fds_to_keep)
371408
proc_fd_dir = opendir(FD_DIR);
372409
if (!proc_fd_dir) {
373410
/* No way to get a list of open fds. */
374-
_close_range_except(start_fd, -1, py_fds_to_keep, _unsafe_closer);
411+
_close_range_except(start_fd, -1, fds_to_keep, fds_to_keep_len,
412+
_unsafe_closer);
375413
} else {
376414
struct dirent *dir_entry;
377415
#ifdef HAVE_DIRFD
@@ -385,14 +423,16 @@ _close_open_fds_maybe_unsafe(int start_fd, PyObject* py_fds_to_keep)
385423
if ((fd = _pos_int_from_ascii(dir_entry->d_name)) < 0)
386424
continue; /* Not a number. */
387425
if (fd != fd_used_by_opendir && fd >= start_fd &&
388-
!_is_fd_in_sorted_fd_sequence(fd, py_fds_to_keep)) {
426+
!_is_fd_in_sorted_fd_sequence(fd, fds_to_keep,
427+
fds_to_keep_len)) {
389428
close(fd);
390429
}
391430
errno = 0;
392431
}
393432
if (errno) {
394433
/* readdir error, revert behavior. Highly Unlikely. */
395-
_close_range_except(start_fd, -1, py_fds_to_keep, _unsafe_closer);
434+
_close_range_except(start_fd, -1, fds_to_keep, fds_to_keep_len,
435+
_unsafe_closer);
396436
}
397437
closedir(proc_fd_dir);
398438
}
@@ -420,16 +460,16 @@ _close_range_closer(int first, int last)
420460
#endif
421461

422462
static void
423-
_close_open_fds(int start_fd, PyObject* py_fds_to_keep)
463+
_close_open_fds(int start_fd, int *fds_to_keep, Py_ssize_t fds_to_keep_len)
424464
{
425465
#ifdef HAVE_ASYNC_SAFE_CLOSE_RANGE
426466
if (_close_range_except(
427-
start_fd, INT_MAX, py_fds_to_keep,
467+
start_fd, INT_MAX, fds_to_keep, fds_to_keep_len,
428468
_close_range_closer) == 0) {
429469
return;
430470
}
431471
#endif
432-
_close_open_fds_fallback(start_fd, py_fds_to_keep);
472+
_close_open_fds_fallback(start_fd, fds_to_keep, fds_to_keep_len);
433473
}
434474

435475
#ifdef VFORK_USABLE
@@ -522,7 +562,7 @@ child_exec(char *const exec_array[],
522562
int call_setgroups, size_t groups_size, const gid_t *groups,
523563
int call_setuid, uid_t uid, int child_umask,
524564
const void *child_sigmask,
525-
PyObject *py_fds_to_keep,
565+
int *fds_to_keep, Py_ssize_t fds_to_keep_len,
526566
PyObject *preexec_fn,
527567
PyObject *preexec_fn_args_tuple)
528568
{
@@ -532,7 +572,7 @@ child_exec(char *const exec_array[],
532572
/* Buffer large enough to hold a hex integer. We can't malloc. */
533573
char hex_errno[sizeof(saved_errno)*2+1];
534574

535-
if (make_inheritable(py_fds_to_keep, errpipe_write) < 0)
575+
if (make_inheritable(fds_to_keep, fds_to_keep_len, errpipe_write) < 0)
536576
goto error;
537577

538578
/* Close parent's pipe ends. */
@@ -652,7 +692,7 @@ child_exec(char *const exec_array[],
652692
/* close FDs after executing preexec_fn, which might open FDs */
653693
if (close_fds) {
654694
/* TODO HP-UX could use pstat_getproc() if anyone cares about it. */
655-
_close_open_fds(3, py_fds_to_keep);
695+
_close_open_fds(3, fds_to_keep, fds_to_keep_len);
656696
}
657697

658698
/* This loop matches the Lib/os.py _execvpe()'s PATH search when */
@@ -726,7 +766,7 @@ do_fork_exec(char *const exec_array[],
726766
int call_setgroups, size_t groups_size, const gid_t *groups,
727767
int call_setuid, uid_t uid, int child_umask,
728768
const void *child_sigmask,
729-
PyObject *py_fds_to_keep,
769+
int *fds_to_keep, Py_ssize_t fds_to_keep_len,
730770
PyObject *preexec_fn,
731771
PyObject *preexec_fn_args_tuple)
732772
{
@@ -777,7 +817,8 @@ do_fork_exec(char *const exec_array[],
777817
close_fds, restore_signals, call_setsid, pgid_to_set,
778818
call_setgid, gid, call_setgroups, groups_size, groups,
779819
call_setuid, uid, child_umask, child_sigmask,
780-
py_fds_to_keep, preexec_fn, preexec_fn_args_tuple);
820+
fds_to_keep, fds_to_keep_len,
821+
preexec_fn, preexec_fn_args_tuple);
781822
_exit(255);
782823
return 0; /* Dead code to avoid a potential compiler warning. */
783824
}
@@ -810,6 +851,8 @@ subprocess_fork_exec(PyObject *module, PyObject *args)
810851
int need_after_fork = 0;
811852
int saved_errno = 0;
812853
int allow_vfork;
854+
int *c_fds_to_keep = NULL;
855+
Py_ssize_t fds_to_keep_len = PyTuple_GET_SIZE(py_fds_to_keep);
813856

814857
if (!PyArg_ParseTuple(
815858
args, "OOpO!OOiiiiiiiiii" _Py_PARSE_PID "OOOiOp:fork_exec",
@@ -983,6 +1026,15 @@ subprocess_fork_exec(PyObject *module, PyObject *args)
9831026
#endif /* HAVE_SETREUID */
9841027
}
9851028

1029+
c_fds_to_keep = PyMem_RawMalloc(fds_to_keep_len * sizeof(int));
1030+
if (c_fds_to_keep == NULL) {
1031+
PyErr_SetString(PyExc_MemoryError, "failed to malloc c_fds_to_keep");
1032+
goto cleanup;
1033+
}
1034+
if (convert_fds_to_keep_to_c(py_fds_to_keep, c_fds_to_keep) < 0) {
1035+
goto cleanup;
1036+
}
1037+
9861038
/* This must be the last thing done before fork() because we do not
9871039
* want to call PyOS_BeforeFork() if there is any chance of another
9881040
* error leading to the cleanup: code without calling fork(). */
@@ -1025,7 +1077,8 @@ subprocess_fork_exec(PyObject *module, PyObject *args)
10251077
close_fds, restore_signals, call_setsid, pgid_to_set,
10261078
call_setgid, gid, call_setgroups, num_groups, groups,
10271079
call_setuid, uid, child_umask, old_sigmask,
1028-
py_fds_to_keep, preexec_fn, preexec_fn_args_tuple);
1080+
c_fds_to_keep, fds_to_keep_len,
1081+
preexec_fn, preexec_fn_args_tuple);
10291082

10301083
/* Parent (original) process */
10311084
if (pid == -1) {
@@ -1055,6 +1108,10 @@ subprocess_fork_exec(PyObject *module, PyObject *args)
10551108
PyOS_AfterFork_Parent();
10561109

10571110
cleanup:
1111+
if (c_fds_to_keep != NULL) {
1112+
PyMem_RawFree(c_fds_to_keep);
1113+
}
1114+
10581115
if (saved_errno != 0) {
10591116
errno = saved_errno;
10601117
/* We can't call this above as PyOS_AfterFork_Parent() calls back

0 commit comments

Comments
 (0)