Skip to content

Commit 6d00ae3

Browse files
authored
[3.11] gh-104372: Cleanup _posixsubprocess make_inheritable for async signal safety gh-104518 (#104785)
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. This is a backport of c649df6 for #104518 and the tiny adjustment in d1732fe #104697. Backporting this allows backporting of the real bug fix that requires it. Co-authored-by: Gregory P. Smith [Google] <greg@krypto.org>
1 parent 582aadc commit 6d00ae3

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 (fd == -1 && 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,7 @@ 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;
813855

814856
if (!PyArg_ParseTuple(
815857
args, "OOpO!OOiiiiiiiiii" _Py_PARSE_PID "OOOiOp:fork_exec",
@@ -983,6 +1025,16 @@ subprocess_fork_exec(PyObject *module, PyObject *args)
9831025
#endif /* HAVE_SETREUID */
9841026
}
9851027

1028+
Py_ssize_t fds_to_keep_len = PyTuple_GET_SIZE(py_fds_to_keep);
1029+
c_fds_to_keep = PyMem_Malloc(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_Free(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)