Skip to content

Commit c649df6

Browse files
authored
gh-104372: Cleanup _posixsubprocess make_inheritable for async signal safety and no GIL requirement (#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.
1 parent f7df173 commit c649df6

File tree

2 files changed

+92
-34
lines changed

2 files changed

+92
-34
lines changed
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

+91-34
Original file line numberDiff line numberDiff line change
@@ -160,16 +160,17 @@ _sanity_check_python_fd_sequence(PyObject *fd_sequence)
160160

161161
/* Is fd found in the sorted Python Sequence? */
162162
static int
163-
_is_fd_in_sorted_fd_sequence(int fd, PyObject *fd_sequence)
163+
_is_fd_in_sorted_fd_sequence(int fd, int *fd_sequence,
164+
Py_ssize_t fd_sequence_len)
164165
{
165166
/* Binary search. */
166167
Py_ssize_t search_min = 0;
167-
Py_ssize_t search_max = PyTuple_GET_SIZE(fd_sequence) - 1;
168+
Py_ssize_t search_max = fd_sequence_len - 1;
168169
if (search_max < 0)
169170
return 0;
170171
do {
171172
long middle = (search_min + search_max) / 2;
172-
long middle_fd = PyLong_AsLong(PyTuple_GET_ITEM(fd_sequence, middle));
173+
long middle_fd = fd_sequence[middle];
173174
if (fd == middle_fd)
174175
return 1;
175176
if (fd > middle_fd)
@@ -180,24 +181,56 @@ _is_fd_in_sorted_fd_sequence(int fd, PyObject *fd_sequence)
180181
return 0;
181182
}
182183

184+
/*
185+
* Do all the Python C API calls in the parent process to turn the pass_fds
186+
* "py_fds_to_keep" tuple into a C array. The caller owns allocation and
187+
* freeing of the array.
188+
*
189+
* On error an unknown number of array elements may have been filled in.
190+
* A Python exception has been set when an error is returned.
191+
*
192+
* Returns: -1 on error, 0 on success.
193+
*/
183194
static int
184-
make_inheritable(PyObject *py_fds_to_keep, int errpipe_write)
195+
convert_fds_to_keep_to_c(PyObject *py_fds_to_keep, int *c_fds_to_keep)
185196
{
186197
Py_ssize_t i, len;
187198

188199
len = PyTuple_GET_SIZE(py_fds_to_keep);
189200
for (i = 0; i < len; ++i) {
190201
PyObject* fdobj = PyTuple_GET_ITEM(py_fds_to_keep, i);
191202
long fd = PyLong_AsLong(fdobj);
192-
assert(!PyErr_Occurred());
193-
assert(0 <= fd && fd <= INT_MAX);
203+
if (PyErr_Occurred()) {
204+
return -1;
205+
}
206+
if (fd < 0 || fd > INT_MAX) {
207+
PyErr_SetString(PyExc_ValueError,
208+
"fd out of range in fds_to_keep.");
209+
return -1;
210+
}
211+
c_fds_to_keep[i] = (int)fd;
212+
}
213+
return 0;
214+
}
215+
216+
217+
/* This function must be async-signal-safe as it is called from child_exec()
218+
* after fork() or vfork().
219+
*/
220+
static int
221+
make_inheritable(int *c_fds_to_keep, Py_ssize_t len, int errpipe_write)
222+
{
223+
Py_ssize_t i;
224+
225+
for (i = 0; i < len; ++i) {
226+
int fd = c_fds_to_keep[i];
194227
if (fd == errpipe_write) {
195-
/* errpipe_write is part of py_fds_to_keep. It must be closed at
228+
/* errpipe_write is part of fds_to_keep. It must be closed at
196229
exec(), but kept open in the child process until exec() is
197230
called. */
198231
continue;
199232
}
200-
if (_Py_set_inheritable_async_safe((int)fd, 1, NULL) < 0)
233+
if (_Py_set_inheritable_async_safe(fd, 1, NULL) < 0)
201234
return -1;
202235
}
203236
return 0;
@@ -233,7 +266,7 @@ safe_get_max_fd(void)
233266

234267

235268
/* Close all file descriptors in the given range except for those in
236-
* py_fds_to_keep by invoking closer on each subrange.
269+
* fds_to_keep by invoking closer on each subrange.
237270
*
238271
* If end_fd == -1, it's guessed via safe_get_max_fd(), but it isn't
239272
* possible to know for sure what the max fd to go up to is for
@@ -243,19 +276,18 @@ safe_get_max_fd(void)
243276
static int
244277
_close_range_except(int start_fd,
245278
int end_fd,
246-
PyObject *py_fds_to_keep,
279+
int *fds_to_keep,
280+
Py_ssize_t fds_to_keep_len,
247281
int (*closer)(int, int))
248282
{
249283
if (end_fd == -1) {
250284
end_fd = Py_MIN(safe_get_max_fd(), INT_MAX);
251285
}
252-
Py_ssize_t num_fds_to_keep = PyTuple_GET_SIZE(py_fds_to_keep);
253286
Py_ssize_t keep_seq_idx;
254-
/* As py_fds_to_keep is sorted we can loop through the list closing
287+
/* As fds_to_keep is sorted we can loop through the list closing
255288
* fds in between any in the keep list falling within our range. */
256-
for (keep_seq_idx = 0; keep_seq_idx < num_fds_to_keep; ++keep_seq_idx) {
257-
PyObject* py_keep_fd = PyTuple_GET_ITEM(py_fds_to_keep, keep_seq_idx);
258-
int keep_fd = PyLong_AsLong(py_keep_fd);
289+
for (keep_seq_idx = 0; keep_seq_idx < fds_to_keep_len; ++keep_seq_idx) {
290+
int keep_fd = fds_to_keep[keep_seq_idx];
259291
if (keep_fd < start_fd)
260292
continue;
261293
if (closer(start_fd, keep_fd - 1) != 0)
@@ -295,7 +327,7 @@ _brute_force_closer(int first, int last)
295327
}
296328

297329
/* Close all open file descriptors in the range from start_fd and higher
298-
* Do not close any in the sorted py_fds_to_keep list.
330+
* Do not close any in the sorted fds_to_keep list.
299331
*
300332
* This version is async signal safe as it does not make any unsafe C library
301333
* calls, malloc calls or handle any locks. It is _unfortunate_ to be forced
@@ -310,14 +342,16 @@ _brute_force_closer(int first, int last)
310342
* it with some cpp #define magic to work on other OSes as well if you want.
311343
*/
312344
static void
313-
_close_open_fds_safe(int start_fd, PyObject* py_fds_to_keep)
345+
_close_open_fds_safe(int start_fd, int *fds_to_keep, Py_ssize_t fds_to_keep_len)
314346
{
315347
int fd_dir_fd;
316348

317349
fd_dir_fd = _Py_open_noraise(FD_DIR, O_RDONLY);
318350
if (fd_dir_fd == -1) {
319351
/* No way to get a list of open fds. */
320-
_close_range_except(start_fd, -1, py_fds_to_keep, _brute_force_closer);
352+
_close_range_except(start_fd, -1,
353+
fds_to_keep, fds_to_keep_len,
354+
_brute_force_closer);
321355
return;
322356
} else {
323357
char buffer[sizeof(struct linux_dirent64)];
@@ -336,7 +370,8 @@ _close_open_fds_safe(int start_fd, PyObject* py_fds_to_keep)
336370
if ((fd = _pos_int_from_ascii(entry->d_name)) < 0)
337371
continue; /* Not a number. */
338372
if (fd != fd_dir_fd && fd >= start_fd &&
339-
!_is_fd_in_sorted_fd_sequence(fd, py_fds_to_keep)) {
373+
!_is_fd_in_sorted_fd_sequence(fd, fds_to_keep,
374+
fds_to_keep_len)) {
340375
close(fd);
341376
}
342377
}
@@ -357,7 +392,7 @@ _unsafe_closer(int first, int last)
357392
}
358393

359394
/* Close all open file descriptors from start_fd and higher.
360-
* Do not close any in the sorted py_fds_to_keep tuple.
395+
* Do not close any in the sorted fds_to_keep tuple.
361396
*
362397
* This function violates the strict use of async signal safe functions. :(
363398
* It calls opendir(), readdir() and closedir(). Of these, the one most
@@ -370,11 +405,13 @@ _unsafe_closer(int first, int last)
370405
* http://womble.decadent.org.uk/readdir_r-advisory.html
371406
*/
372407
static void
373-
_close_open_fds_maybe_unsafe(int start_fd, PyObject* py_fds_to_keep)
408+
_close_open_fds_maybe_unsafe(int start_fd, int *fds_to_keep,
409+
Py_ssize_t fds_to_keep_len)
374410
{
375411
DIR *proc_fd_dir;
376412
#ifndef HAVE_DIRFD
377-
while (_is_fd_in_sorted_fd_sequence(start_fd, py_fds_to_keep)) {
413+
while (_is_fd_in_sorted_fd_sequence(start_fd, fds_to_keep,
414+
fds_to_keep_len)) {
378415
++start_fd;
379416
}
380417
/* Close our lowest fd before we call opendir so that it is likely to
@@ -393,7 +430,8 @@ _close_open_fds_maybe_unsafe(int start_fd, PyObject* py_fds_to_keep)
393430
proc_fd_dir = opendir(FD_DIR);
394431
if (!proc_fd_dir) {
395432
/* No way to get a list of open fds. */
396-
_close_range_except(start_fd, -1, py_fds_to_keep, _unsafe_closer);
433+
_close_range_except(start_fd, -1, fds_to_keep, fds_to_keep_len,
434+
_unsafe_closer);
397435
} else {
398436
struct dirent *dir_entry;
399437
#ifdef HAVE_DIRFD
@@ -407,14 +445,16 @@ _close_open_fds_maybe_unsafe(int start_fd, PyObject* py_fds_to_keep)
407445
if ((fd = _pos_int_from_ascii(dir_entry->d_name)) < 0)
408446
continue; /* Not a number. */
409447
if (fd != fd_used_by_opendir && fd >= start_fd &&
410-
!_is_fd_in_sorted_fd_sequence(fd, py_fds_to_keep)) {
448+
!_is_fd_in_sorted_fd_sequence(fd, fds_to_keep,
449+
fds_to_keep_len)) {
411450
close(fd);
412451
}
413452
errno = 0;
414453
}
415454
if (errno) {
416455
/* readdir error, revert behavior. Highly Unlikely. */
417-
_close_range_except(start_fd, -1, py_fds_to_keep, _unsafe_closer);
456+
_close_range_except(start_fd, -1, fds_to_keep, fds_to_keep_len,
457+
_unsafe_closer);
418458
}
419459
closedir(proc_fd_dir);
420460
}
@@ -442,16 +482,16 @@ _close_range_closer(int first, int last)
442482
#endif
443483

444484
static void
445-
_close_open_fds(int start_fd, PyObject* py_fds_to_keep)
485+
_close_open_fds(int start_fd, int *fds_to_keep, Py_ssize_t fds_to_keep_len)
446486
{
447487
#ifdef HAVE_ASYNC_SAFE_CLOSE_RANGE
448488
if (_close_range_except(
449-
start_fd, INT_MAX, py_fds_to_keep,
489+
start_fd, INT_MAX, fds_to_keep, fds_to_keep_len,
450490
_close_range_closer) == 0) {
451491
return;
452492
}
453493
#endif
454-
_close_open_fds_fallback(start_fd, py_fds_to_keep);
494+
_close_open_fds_fallback(start_fd, fds_to_keep, fds_to_keep_len);
455495
}
456496

457497
#ifdef VFORK_USABLE
@@ -544,7 +584,7 @@ child_exec(char *const exec_array[],
544584
Py_ssize_t extra_group_size, const gid_t *extra_groups,
545585
uid_t uid, int child_umask,
546586
const void *child_sigmask,
547-
PyObject *py_fds_to_keep,
587+
int *fds_to_keep, Py_ssize_t fds_to_keep_len,
548588
PyObject *preexec_fn,
549589
PyObject *preexec_fn_args_tuple)
550590
{
@@ -554,7 +594,7 @@ child_exec(char *const exec_array[],
554594
/* Buffer large enough to hold a hex integer. We can't malloc. */
555595
char hex_errno[sizeof(saved_errno)*2+1];
556596

557-
if (make_inheritable(py_fds_to_keep, errpipe_write) < 0)
597+
if (make_inheritable(fds_to_keep, fds_to_keep_len, errpipe_write) < 0)
558598
goto error;
559599

560600
/* Close parent's pipe ends. */
@@ -676,7 +716,7 @@ child_exec(char *const exec_array[],
676716
/* close FDs after executing preexec_fn, which might open FDs */
677717
if (close_fds) {
678718
/* TODO HP-UX could use pstat_getproc() if anyone cares about it. */
679-
_close_open_fds(3, py_fds_to_keep);
719+
_close_open_fds(3, fds_to_keep, fds_to_keep_len);
680720
}
681721

682722
/* This loop matches the Lib/os.py _execvpe()'s PATH search when */
@@ -750,7 +790,7 @@ do_fork_exec(char *const exec_array[],
750790
Py_ssize_t extra_group_size, const gid_t *extra_groups,
751791
uid_t uid, int child_umask,
752792
const void *child_sigmask,
753-
PyObject *py_fds_to_keep,
793+
int *fds_to_keep, Py_ssize_t fds_to_keep_len,
754794
PyObject *preexec_fn,
755795
PyObject *preexec_fn_args_tuple)
756796
{
@@ -801,7 +841,8 @@ do_fork_exec(char *const exec_array[],
801841
close_fds, restore_signals, call_setsid, pgid_to_set,
802842
gid, extra_group_size, extra_groups,
803843
uid, child_umask, child_sigmask,
804-
py_fds_to_keep, preexec_fn, preexec_fn_args_tuple);
844+
fds_to_keep, fds_to_keep_len,
845+
preexec_fn, preexec_fn_args_tuple);
805846
_exit(255);
806847
return 0; /* Dead code to avoid a potential compiler warning. */
807848
}
@@ -881,6 +922,8 @@ subprocess_fork_exec_impl(PyObject *module, PyObject *process_args,
881922
Py_ssize_t extra_group_size = 0;
882923
int need_after_fork = 0;
883924
int saved_errno = 0;
925+
int *c_fds_to_keep = NULL;
926+
Py_ssize_t fds_to_keep_len = PyTuple_GET_SIZE(py_fds_to_keep);
884927

885928
PyInterpreterState *interp = PyInterpreterState_Get();
886929
if ((preexec_fn != Py_None) && (interp != PyInterpreterState_Main())) {
@@ -1031,6 +1074,15 @@ subprocess_fork_exec_impl(PyObject *module, PyObject *process_args,
10311074
#endif /* HAVE_SETREUID */
10321075
}
10331076

1077+
c_fds_to_keep = PyMem_RawMalloc(fds_to_keep_len * sizeof(int));
1078+
if (c_fds_to_keep == NULL) {
1079+
PyErr_SetString(PyExc_MemoryError, "failed to malloc c_fds_to_keep");
1080+
goto cleanup;
1081+
}
1082+
if (convert_fds_to_keep_to_c(py_fds_to_keep, c_fds_to_keep) < 0) {
1083+
goto cleanup;
1084+
}
1085+
10341086
/* This must be the last thing done before fork() because we do not
10351087
* want to call PyOS_BeforeFork() if there is any chance of another
10361088
* error leading to the cleanup: code without calling fork(). */
@@ -1073,7 +1125,8 @@ subprocess_fork_exec_impl(PyObject *module, PyObject *process_args,
10731125
close_fds, restore_signals, call_setsid, pgid_to_set,
10741126
gid, extra_group_size, extra_groups,
10751127
uid, child_umask, old_sigmask,
1076-
py_fds_to_keep, preexec_fn, preexec_fn_args_tuple);
1128+
c_fds_to_keep, fds_to_keep_len,
1129+
preexec_fn, preexec_fn_args_tuple);
10771130

10781131
/* Parent (original) process */
10791132
if (pid == (pid_t)-1) {
@@ -1103,6 +1156,10 @@ subprocess_fork_exec_impl(PyObject *module, PyObject *process_args,
11031156
PyOS_AfterFork_Parent();
11041157

11051158
cleanup:
1159+
if (c_fds_to_keep != NULL) {
1160+
PyMem_RawFree(c_fds_to_keep);
1161+
}
1162+
11061163
if (saved_errno != 0) {
11071164
errno = saved_errno;
11081165
/* We can't call this above as PyOS_AfterFork_Parent() calls back

0 commit comments

Comments
 (0)