Skip to content

Commit e93c39b

Browse files
authored
gh-118422: Fix run_fileexflags() test (#118429)
Don't test the undefined behavior of fileno() on a closed file, but use fstat() as a reliable test if the file was closed or not.
1 parent 587388f commit e93c39b

File tree

4 files changed

+62
-58
lines changed

4 files changed

+62
-58
lines changed

Include/internal/pycore_fileutils.h

+3
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,9 @@ extern int _PyFile_Flush(PyObject *);
326326
extern int _Py_GetTicksPerSecond(long *ticks_per_second);
327327
#endif
328328

329+
// Export for '_testcapi' shared extension
330+
PyAPI_FUNC(int) _Py_IsValidFD(int fd);
331+
329332
#ifdef __cplusplus
330333
}
331334
#endif

Modules/_testcapi/run.c

+6-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
#define PYTESTCAPI_NEED_INTERNAL_API
12
#include "parts.h"
23
#include "util.h"
4+
#include "pycore_fileutils.h" // _Py_IsValidFD()
35

46
#include <stdio.h>
57
#include <errno.h>
@@ -71,21 +73,18 @@ run_fileexflags(PyObject *mod, PyObject *pos_args)
7173
PyErr_SetFromErrnoWithFilename(PyExc_OSError, filename);
7274
return NULL;
7375
}
76+
int fd = fileno(fp);
7477

7578
result = PyRun_FileExFlags(fp, filename, start, globals, locals, closeit, pflags);
7679

77-
#if defined(__linux__) || defined(MS_WINDOWS) || defined(__APPLE__)
78-
/* The behavior of fileno() after fclose() is undefined, but it is
79-
* the only practical way to check whether the file was closed.
80-
* Only test this on the known platforms. */
81-
if (closeit && result && fileno(fp) >= 0) {
80+
if (closeit && result && _Py_IsValidFD(fd)) {
8281
PyErr_SetString(PyExc_AssertionError, "File was not closed after excution");
8382
Py_DECREF(result);
8483
fclose(fp);
8584
return NULL;
8685
}
87-
#endif
88-
if (!closeit && fileno(fp) < 0) {
86+
87+
if (!closeit && !_Py_IsValidFD(fd)) {
8988
PyErr_SetString(PyExc_AssertionError, "Bad file descriptor after excution");
9089
Py_XDECREF(result);
9190
return NULL;

Python/fileutils.c

+49
Original file line numberDiff line numberDiff line change
@@ -3050,3 +3050,52 @@ _Py_GetTicksPerSecond(long *ticks_per_second)
30503050
return 0;
30513051
}
30523052
#endif
3053+
3054+
3055+
/* Check if a file descriptor is valid or not.
3056+
Return 0 if the file descriptor is invalid, return non-zero otherwise. */
3057+
int
3058+
_Py_IsValidFD(int fd)
3059+
{
3060+
/* dup() is faster than fstat(): fstat() can require input/output operations,
3061+
whereas dup() doesn't. There is a low risk of EMFILE/ENFILE at Python
3062+
startup. Problem: dup() doesn't check if the file descriptor is valid on
3063+
some platforms.
3064+
3065+
fcntl(fd, F_GETFD) is even faster, because it only checks the process table.
3066+
It is preferred over dup() when available, since it cannot fail with the
3067+
"too many open files" error (EMFILE).
3068+
3069+
bpo-30225: On macOS Tiger, when stdout is redirected to a pipe and the other
3070+
side of the pipe is closed, dup(1) succeed, whereas fstat(1, &st) fails with
3071+
EBADF. FreeBSD has similar issue (bpo-32849).
3072+
3073+
Only use dup() on Linux where dup() is enough to detect invalid FD
3074+
(bpo-32849).
3075+
*/
3076+
if (fd < 0) {
3077+
return 0;
3078+
}
3079+
#if defined(F_GETFD) && ( \
3080+
defined(__linux__) || \
3081+
defined(__APPLE__) || \
3082+
(defined(__wasm__) && !defined(__wasi__)))
3083+
return fcntl(fd, F_GETFD) >= 0;
3084+
#elif defined(__linux__)
3085+
int fd2 = dup(fd);
3086+
if (fd2 >= 0) {
3087+
close(fd2);
3088+
}
3089+
return (fd2 >= 0);
3090+
#elif defined(MS_WINDOWS)
3091+
HANDLE hfile;
3092+
_Py_BEGIN_SUPPRESS_IPH
3093+
hfile = (HANDLE)_get_osfhandle(fd);
3094+
_Py_END_SUPPRESS_IPH
3095+
return (hfile != INVALID_HANDLE_VALUE
3096+
&& GetFileType(hfile) != FILE_TYPE_UNKNOWN);
3097+
#else
3098+
struct stat st;
3099+
return (fstat(fd, &st) == 0);
3100+
#endif
3101+
}

Python/pylifecycle.c

+4-51
Original file line numberDiff line numberDiff line change
@@ -2410,54 +2410,6 @@ init_import_site(void)
24102410
return _PyStatus_OK();
24112411
}
24122412

2413-
/* Check if a file descriptor is valid or not.
2414-
Return 0 if the file descriptor is invalid, return non-zero otherwise. */
2415-
static int
2416-
is_valid_fd(int fd)
2417-
{
2418-
/* dup() is faster than fstat(): fstat() can require input/output operations,
2419-
whereas dup() doesn't. There is a low risk of EMFILE/ENFILE at Python
2420-
startup. Problem: dup() doesn't check if the file descriptor is valid on
2421-
some platforms.
2422-
2423-
fcntl(fd, F_GETFD) is even faster, because it only checks the process table.
2424-
It is preferred over dup() when available, since it cannot fail with the
2425-
"too many open files" error (EMFILE).
2426-
2427-
bpo-30225: On macOS Tiger, when stdout is redirected to a pipe and the other
2428-
side of the pipe is closed, dup(1) succeed, whereas fstat(1, &st) fails with
2429-
EBADF. FreeBSD has similar issue (bpo-32849).
2430-
2431-
Only use dup() on Linux where dup() is enough to detect invalid FD
2432-
(bpo-32849).
2433-
*/
2434-
if (fd < 0) {
2435-
return 0;
2436-
}
2437-
#if defined(F_GETFD) && ( \
2438-
defined(__linux__) || \
2439-
defined(__APPLE__) || \
2440-
defined(__wasm__))
2441-
return fcntl(fd, F_GETFD) >= 0;
2442-
#elif defined(__linux__)
2443-
int fd2 = dup(fd);
2444-
if (fd2 >= 0) {
2445-
close(fd2);
2446-
}
2447-
return (fd2 >= 0);
2448-
#elif defined(MS_WINDOWS)
2449-
HANDLE hfile;
2450-
_Py_BEGIN_SUPPRESS_IPH
2451-
hfile = (HANDLE)_get_osfhandle(fd);
2452-
_Py_END_SUPPRESS_IPH
2453-
return (hfile != INVALID_HANDLE_VALUE
2454-
&& GetFileType(hfile) != FILE_TYPE_UNKNOWN);
2455-
#else
2456-
struct stat st;
2457-
return (fstat(fd, &st) == 0);
2458-
#endif
2459-
}
2460-
24612413
/* returns Py_None if the fd is not valid */
24622414
static PyObject*
24632415
create_stdio(const PyConfig *config, PyObject* io,
@@ -2471,8 +2423,9 @@ create_stdio(const PyConfig *config, PyObject* io,
24712423
int buffering, isatty;
24722424
const int buffered_stdio = config->buffered_stdio;
24732425

2474-
if (!is_valid_fd(fd))
2426+
if (!_Py_IsValidFD(fd)) {
24752427
Py_RETURN_NONE;
2428+
}
24762429

24772430
/* stdin is always opened in buffered mode, first because it shouldn't
24782431
make a difference in common use cases, second because TextIOWrapper
@@ -2588,9 +2541,9 @@ create_stdio(const PyConfig *config, PyObject* io,
25882541
Py_XDECREF(text);
25892542
Py_XDECREF(raw);
25902543

2591-
if (PyErr_ExceptionMatches(PyExc_OSError) && !is_valid_fd(fd)) {
2544+
if (PyErr_ExceptionMatches(PyExc_OSError) && !_Py_IsValidFD(fd)) {
25922545
/* Issue #24891: the file descriptor was closed after the first
2593-
is_valid_fd() check was called. Ignore the OSError and set the
2546+
_Py_IsValidFD() check was called. Ignore the OSError and set the
25942547
stream to None. */
25952548
PyErr_Clear();
25962549
Py_RETURN_NONE;

0 commit comments

Comments
 (0)