Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d4dcb70

Browse files
committed
Don't restrict ourselves to a "max" fd when closing fds before exec()
when we have a way to get an actual list of all open fds from the OS. Fixes issue #21618: The subprocess module would ignore fds that were inherited by the calling process and already higher than POSIX resource limits would otherwise allow. On systems with a functioning /proc/self/fd or /dev/fd interface the max is now ignored and all fds are closed.
1 parent 694c315 commit d4dcb70

3 files changed

Lines changed: 102 additions & 43 deletions

File tree

Lib/test/test_subprocess.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1926,6 +1926,59 @@ def test_close_fds(self):
19261926
"Some fds not in pass_fds were left open")
19271927
self.assertIn(1, remaining_fds, "Subprocess failed")
19281928

1929+
1930+
def test_close_fds_when_max_fd_is_lowered(self):
1931+
"""Confirm that issue21618 is fixed (may fail under valgrind)."""
1932+
fd_status = support.findfile("fd_status.py", subdir="subprocessdata")
1933+
1934+
open_fds = set()
1935+
# Add a bunch more fds to pass down.
1936+
for _ in range(10):
1937+
fd = os.open("/dev/null", os.O_RDONLY)
1938+
open_fds.add(fd)
1939+
1940+
# Leave a two pairs of low ones available for use by the
1941+
# internal child error pipe and the stdout pipe.
1942+
for fd in sorted(open_fds)[:4]:
1943+
os.close(fd)
1944+
open_fds.remove(fd)
1945+
1946+
for fd in open_fds:
1947+
self.addCleanup(os.close, fd)
1948+
os.set_inheritable(fd, True)
1949+
1950+
max_fd_open = max(open_fds)
1951+
1952+
import resource
1953+
rlim_cur, rlim_max = resource.getrlimit(resource.RLIMIT_NOFILE)
1954+
try:
1955+
# 9 is lower than the highest fds we are leaving open.
1956+
resource.setrlimit(resource.RLIMIT_NOFILE, (9, rlim_max))
1957+
# Launch a new Python interpreter with our low fd rlim_cur that
1958+
# inherits open fds above that limit. It then uses subprocess
1959+
# with close_fds=True to get a report of open fds in the child.
1960+
# An explicit list of fds to check is passed to fd_status.py as
1961+
# letting fd_status rely on its default logic would miss the
1962+
# fds above rlim_cur as it normally only checks up to that limit.
1963+
p = subprocess.Popen(
1964+
[sys.executable, '-c',
1965+
textwrap.dedent("""
1966+
import subprocess, sys
1967+
subprocess.Popen([sys.executable, {fd_status!r}] +
1968+
[str(x) for x in range({max_fd})],
1969+
close_fds=True)
1970+
""".format(fd_status=fd_status, max_fd=max_fd_open+1))],
1971+
stdout=subprocess.PIPE, close_fds=False)
1972+
finally:
1973+
resource.setrlimit(resource.RLIMIT_NOFILE, (rlim_cur, rlim_max))
1974+
1975+
output, unused_stderr = p.communicate()
1976+
remaining_fds = set(map(int, output.strip().split(b',')))
1977+
1978+
self.assertFalse(remaining_fds & open_fds,
1979+
msg="Some fds were left open.")
1980+
1981+
19291982
# Mac OS X Tiger (10.4) has a kernel bug: sometimes, the file
19301983
# descriptor of a pipe closed in the parent process is valid in the
19311984
# child process according to fstat(), but the mode of the file

Misc/NEWS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ Core and Builtins
1818
Library
1919
-------
2020

21+
- Issue #21618: The subprocess module could fail to close open fds that were
22+
inherited by the calling process and already higher than POSIX resource
23+
limits would otherwise allow. On systems with a functioning /proc/self/fd
24+
or /dev/fd interface the max is now ignored and all fds are closed.
25+
2126
- Issue #21552: Fixed possible integer overflow of too long string lengths in
2227
the tkinter module on 64-bit platforms.
2328

Modules/_posixsubprocess.c

Lines changed: 44 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,6 @@
4444
#define POSIX_CALL(call) do { if ((call) == -1) goto error; } while (0)
4545

4646

47-
/* Maximum file descriptor, initialized on module load. */
48-
static long max_fd;
49-
50-
5147
/* Given the gc module call gc.enable() and return 0 on success. */
5248
static int
5349
_enable_gc(PyObject *gc_module)
@@ -166,14 +162,39 @@ make_inheritable(PyObject *py_fds_to_keep, int errpipe_write)
166162
}
167163

168164

169-
/* Close all file descriptors in the range start_fd inclusive to
170-
* end_fd exclusive except for those in py_fds_to_keep. If the
171-
* range defined by [start_fd, end_fd) is large this will take a
172-
* long time as it calls close() on EVERY possible fd.
165+
/* Get the maximum file descriptor that could be opened by this process.
166+
* This function is async signal safe for use between fork() and exec().
167+
*/
168+
static long
169+
safe_get_max_fd(void)
170+
{
171+
long local_max_fd;
172+
#if defined(__NetBSD__)
173+
local_max_fd = fcntl(0, F_MAXFD);
174+
if (local_max_fd >= 0)
175+
return local_max_fd;
176+
#endif
177+
#ifdef _SC_OPEN_MAX
178+
local_max_fd = sysconf(_SC_OPEN_MAX);
179+
if (local_max_fd == -1)
180+
#endif
181+
local_max_fd = 256; /* Matches legacy Lib/subprocess.py behavior. */
182+
return local_max_fd;
183+
}
184+
185+
186+
/* Close all file descriptors in the range from start_fd and higher
187+
* except for those in py_fds_to_keep. If the range defined by
188+
* [start_fd, safe_get_max_fd()) is large this will take a long
189+
* time as it calls close() on EVERY possible fd.
190+
*
191+
* It isn't possible to know for sure what the max fd to go up to
192+
* is for processes with the capability of raising their maximum.
173193
*/
174194
static void
175-
_close_fds_by_brute_force(int start_fd, int end_fd, PyObject *py_fds_to_keep)
195+
_close_fds_by_brute_force(long start_fd, PyObject *py_fds_to_keep)
176196
{
197+
long end_fd = safe_get_max_fd();
177198
Py_ssize_t num_fds_to_keep = PySequence_Length(py_fds_to_keep);
178199
Py_ssize_t keep_seq_idx;
179200
int fd_num;
@@ -229,16 +250,14 @@ struct linux_dirent64 {
229250
* it with some cpp #define magic to work on other OSes as well if you want.
230251
*/
231252
static void
232-
_close_open_fd_range_safe(int start_fd, int end_fd, PyObject* py_fds_to_keep)
253+
_close_open_fds_safe(int start_fd, PyObject* py_fds_to_keep)
233254
{
234255
int fd_dir_fd;
235-
if (start_fd >= end_fd)
236-
return;
237256

238257
fd_dir_fd = _Py_open(FD_DIR, O_RDONLY);
239258
if (fd_dir_fd == -1) {
240259
/* No way to get a list of open fds. */
241-
_close_fds_by_brute_force(start_fd, end_fd, py_fds_to_keep);
260+
_close_fds_by_brute_force(start_fd, py_fds_to_keep);
242261
return;
243262
} else {
244263
char buffer[sizeof(struct linux_dirent64)];
@@ -253,23 +272,23 @@ _close_open_fd_range_safe(int start_fd, int end_fd, PyObject* py_fds_to_keep)
253272
entry = (struct linux_dirent64 *)(buffer + offset);
254273
if ((fd = _pos_int_from_ascii(entry->d_name)) < 0)
255274
continue; /* Not a number. */
256-
if (fd != fd_dir_fd && fd >= start_fd && fd < end_fd &&
275+
if (fd != fd_dir_fd && fd >= start_fd &&
257276
!_is_fd_in_sorted_fd_sequence(fd, py_fds_to_keep)) {
258277
while (close(fd) < 0 && errno == EINTR);
259278
}
260279
}
261280
}
262-
close(fd_dir_fd);
281+
while (close(fd_dir_fd) < 0 && errno == EINTR);
263282
}
264283
}
265284

266-
#define _close_open_fd_range _close_open_fd_range_safe
285+
#define _close_open_fds _close_open_fds_safe
267286

268287
#else /* NOT (defined(__linux__) && defined(HAVE_SYS_SYSCALL_H)) */
269288

270289

271-
/* Close all open file descriptors in the range start_fd inclusive to end_fd
272-
* exclusive. Do not close any in the sorted py_fds_to_keep list.
290+
/* Close all open file descriptors from start_fd and higher.
291+
* Do not close any in the sorted py_fds_to_keep list.
273292
*
274293
* This function violates the strict use of async signal safe functions. :(
275294
* It calls opendir(), readdir() and closedir(). Of these, the one most
@@ -282,26 +301,20 @@ _close_open_fd_range_safe(int start_fd, int end_fd, PyObject* py_fds_to_keep)
282301
* http://womble.decadent.org.uk/readdir_r-advisory.html
283302
*/
284303
static void
285-
_close_open_fd_range_maybe_unsafe(int start_fd, int end_fd,
286-
PyObject* py_fds_to_keep)
304+
_close_open_fds_maybe_unsafe(long start_fd, PyObject* py_fds_to_keep)
287305
{
288306
DIR *proc_fd_dir;
289307
#ifndef HAVE_DIRFD
290-
while (_is_fd_in_sorted_fd_sequence(start_fd, py_fds_to_keep) &&
291-
(start_fd < end_fd)) {
308+
while (_is_fd_in_sorted_fd_sequence(start_fd, py_fds_to_keep)) {
292309
++start_fd;
293310
}
294-
if (start_fd >= end_fd)
295-
return;
296311
/* Close our lowest fd before we call opendir so that it is likely to
297312
* reuse that fd otherwise we might close opendir's file descriptor in
298313
* our loop. This trick assumes that fd's are allocated on a lowest
299314
* available basis. */
300315
while (close(start_fd) < 0 && errno == EINTR);
301316
++start_fd;
302317
#endif
303-
if (start_fd >= end_fd)
304-
return;
305318

306319
#if defined(__FreeBSD__)
307320
if (!_is_fdescfs_mounted_on_dev_fd())
@@ -311,7 +324,7 @@ _close_open_fd_range_maybe_unsafe(int start_fd, int end_fd,
311324
proc_fd_dir = opendir(FD_DIR);
312325
if (!proc_fd_dir) {
313326
/* No way to get a list of open fds. */
314-
_close_fds_by_brute_force(start_fd, end_fd, py_fds_to_keep);
327+
_close_fds_by_brute_force(start_fd, py_fds_to_keep);
315328
} else {
316329
struct dirent *dir_entry;
317330
#ifdef HAVE_DIRFD
@@ -324,21 +337,21 @@ _close_open_fd_range_maybe_unsafe(int start_fd, int end_fd,
324337
int fd;
325338
if ((fd = _pos_int_from_ascii(dir_entry->d_name)) < 0)
326339
continue; /* Not a number. */
327-
if (fd != fd_used_by_opendir && fd >= start_fd && fd < end_fd &&
340+
if (fd != fd_used_by_opendir && fd >= start_fd &&
328341
!_is_fd_in_sorted_fd_sequence(fd, py_fds_to_keep)) {
329342
while (close(fd) < 0 && errno == EINTR);
330343
}
331344
errno = 0;
332345
}
333346
if (errno) {
334347
/* readdir error, revert behavior. Highly Unlikely. */
335-
_close_fds_by_brute_force(start_fd, end_fd, py_fds_to_keep);
348+
_close_fds_by_brute_force(start_fd, py_fds_to_keep);
336349
}
337350
closedir(proc_fd_dir);
338351
}
339352
}
340353

341-
#define _close_open_fd_range _close_open_fd_range_maybe_unsafe
354+
#define _close_open_fds _close_open_fds_maybe_unsafe
342355

343356
#endif /* else NOT (defined(__linux__) && defined(HAVE_SYS_SYSCALL_H)) */
344357

@@ -457,14 +470,8 @@ child_exec(char *const exec_array[],
457470

458471
/* close FDs after executing preexec_fn, which might open FDs */
459472
if (close_fds) {
460-
int local_max_fd = max_fd;
461-
#if defined(__NetBSD__)
462-
local_max_fd = fcntl(0, F_MAXFD);
463-
if (local_max_fd < 0)
464-
local_max_fd = max_fd;
465-
#endif
466473
/* TODO HP-UX could use pstat_getproc() if anyone cares about it. */
467-
_close_open_fd_range(3, local_max_fd, py_fds_to_keep);
474+
_close_open_fds(3, py_fds_to_keep);
468475
}
469476

470477
/* This loop matches the Lib/os.py _execvpe()'s PATH search when */
@@ -759,11 +766,5 @@ static struct PyModuleDef _posixsubprocessmodule = {
759766
PyMODINIT_FUNC
760767
PyInit__posixsubprocess(void)
761768
{
762-
#ifdef _SC_OPEN_MAX
763-
max_fd = sysconf(_SC_OPEN_MAX);
764-
if (max_fd == -1)
765-
#endif
766-
max_fd = 256; /* Matches Lib/subprocess.py */
767-
768769
return PyModule_Create(&_posixsubprocessmodule);
769770
}

0 commit comments

Comments
 (0)