From d57abd0dea3b92881fb88037160518b7527742ff Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Tue, 8 Oct 2024 14:28:18 -0700 Subject: [PATCH 01/10] gh-120754: _io Ensure stat cache is cleared on fd change Performed an audit of `fileio.c` and `_pyio` and made sure anytime the fd changes the stat result, if set, is also cleared/changed. There's one case where it's not cleared, if code would clear it in __init__, keep the memory allocated and just do another fstat with the existing memory. --- Modules/_io/fileio.c | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/Modules/_io/fileio.c b/Modules/_io/fileio.c index f374592eb95967..50d39df15ce5ab 100644 --- a/Modules/_io/fileio.c +++ b/Modules/_io/fileio.c @@ -115,7 +115,7 @@ fileio_dealloc_warn(PyObject *op, PyObject *source) /* Returns 0 on success, -1 with exception set on failure. */ static int -internal_close(fileio *self) +internal_close(fileio *self, bool clear_stat) { int err = 0; int save_errno = 0; @@ -130,6 +130,11 @@ internal_close(fileio *self) save_errno = errno; _Py_END_SUPPRESS_IPH Py_END_ALLOW_THREADS + + } + if (clear_stat && self->stat_atopen != NULL) { + PyMem_Free(self->stat_atopen); + self->stat_atopen = NULL; } if (err < 0) { errno = save_errno; @@ -178,7 +183,7 @@ _io_FileIO_close_impl(fileio *self, PyTypeObject *cls) PyErr_Clear(); } } - rc = internal_close(self); + rc = internal_close(self, true); if (res == NULL) { _PyErr_ChainExceptions1(exc); } @@ -267,8 +272,10 @@ _io_FileIO___init___impl(fileio *self, PyObject *nameobj, const char *mode, #endif if (self->fd >= 0) { if (self->closefd) { - /* Have to close the existing file first. */ - if (internal_close(self) < 0) + /* Have to close the existing file first. + + This leaves the stat so we can reuse the memory. */ + if (internal_close(self, false) < 0) return -1; } else @@ -455,12 +462,15 @@ _io_FileIO___init___impl(fileio *self, PyObject *nameobj, const char *mode, #endif } - PyMem_Free(self->stat_atopen); - self->stat_atopen = PyMem_New(struct _Py_stat_struct, 1); + /* Reuse stat_atopen memory if possible. */ if (self->stat_atopen == NULL) { - PyErr_NoMemory(); - goto error; + self->stat_atopen = PyMem_New(struct _Py_stat_struct, 1); + if (self->stat_atopen == NULL) { + PyErr_NoMemory(); + goto error; + } } + Py_BEGIN_ALLOW_THREADS fstat_result = _Py_fstat_noraise(self->fd, self->stat_atopen); Py_END_ALLOW_THREADS @@ -520,7 +530,7 @@ _io_FileIO___init___impl(fileio *self, PyObject *nameobj, const char *mode, self->fd = -1; if (self->fd >= 0) { PyObject *exc = PyErr_GetRaisedException(); - internal_close(self); + internal_close(self, true); _PyErr_ChainExceptions1(exc); } if (self->stat_atopen != NULL) { From d2cb36594dfddedcb773f597fbf3a6ec9a32e016 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Tue, 8 Oct 2024 16:40:29 -0700 Subject: [PATCH 02/10] gh-120754: _pyio ensure _stat_atopen is cleared on fd change --- Lib/_pyio.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/_pyio.py b/Lib/_pyio.py index 7b6d10c008d3cb..150496b281131d 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -1480,6 +1480,7 @@ def __init__(self, file, mode='r', closefd=True, opener=None): """ if self._fd >= 0: # Have to close the existing file first. + self._stat_atopen = None try: if self._closefd: os.close(self._fd) @@ -1585,6 +1586,7 @@ def __init__(self, file, mode='r', closefd=True, opener=None): except: if owned_fd is not None: os.close(owned_fd) + self._stat_atopen = None raise self._fd = fd @@ -1756,6 +1758,7 @@ def close(self): called more than once without error. """ if not self.closed: + self._stat_atopen = None try: if self._closefd: os.close(self._fd) From 854ae91f1e1d0f7a0289d3f283480fb71032165b Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Tue, 8 Oct 2024 17:19:27 -0700 Subject: [PATCH 03/10] _io: Add curlies since already modifying if --- Modules/_io/fileio.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/_io/fileio.c b/Modules/_io/fileio.c index 50d39df15ce5ab..707b1158c7a6cf 100644 --- a/Modules/_io/fileio.c +++ b/Modules/_io/fileio.c @@ -275,8 +275,9 @@ _io_FileIO___init___impl(fileio *self, PyObject *nameobj, const char *mode, /* Have to close the existing file first. This leaves the stat so we can reuse the memory. */ - if (internal_close(self, false) < 0) + if (internal_close(self, false) < 0) { return -1; + } } else self->fd = -1; From c6da86e6362cd9aedf01f74d4dd84a37922f0514 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Wed, 9 Oct 2024 11:42:53 -0700 Subject: [PATCH 04/10] _io: Always clear self->stat_atopen --- Modules/_io/fileio.c | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/Modules/_io/fileio.c b/Modules/_io/fileio.c index 707b1158c7a6cf..56f78948c43038 100644 --- a/Modules/_io/fileio.c +++ b/Modules/_io/fileio.c @@ -115,7 +115,7 @@ fileio_dealloc_warn(PyObject *op, PyObject *source) /* Returns 0 on success, -1 with exception set on failure. */ static int -internal_close(fileio *self, bool clear_stat) +internal_close(fileio *self) { int err = 0; int save_errno = 0; @@ -130,12 +130,9 @@ internal_close(fileio *self, bool clear_stat) save_errno = errno; _Py_END_SUPPRESS_IPH Py_END_ALLOW_THREADS - - } - if (clear_stat && self->stat_atopen != NULL) { - PyMem_Free(self->stat_atopen); - self->stat_atopen = NULL; } + PyMem_Free(self->stat_atopen); + self->stat_atopen = NULL; if (err < 0) { errno = save_errno; PyErr_SetFromErrno(PyExc_OSError); @@ -183,7 +180,7 @@ _io_FileIO_close_impl(fileio *self, PyTypeObject *cls) PyErr_Clear(); } } - rc = internal_close(self, true); + rc = internal_close(self); if (res == NULL) { _PyErr_ChainExceptions1(exc); } @@ -272,10 +269,8 @@ _io_FileIO___init___impl(fileio *self, PyObject *nameobj, const char *mode, #endif if (self->fd >= 0) { if (self->closefd) { - /* Have to close the existing file first. - - This leaves the stat so we can reuse the memory. */ - if (internal_close(self, false) < 0) { + /* Have to close the existing file first. */ + if (internal_close(self) < 0) { return -1; } } @@ -463,15 +458,18 @@ _io_FileIO___init___impl(fileio *self, PyObject *nameobj, const char *mode, #endif } - /* Reuse stat_atopen memory if possible. */ - if (self->stat_atopen == NULL) { + /* FileIO.__init__ may be called on an already initialized object. Closing + out the old fd (see: internal_close) should always nullify + self->stat_atopen before this point. Just in case though, to prevent + leaks, only allocate a new one if required. */ + assert(self->stat_atopen == NULL) + if (self->stat_atopen != NULL) { self->stat_atopen = PyMem_New(struct _Py_stat_struct, 1); if (self->stat_atopen == NULL) { PyErr_NoMemory(); goto error; } } - Py_BEGIN_ALLOW_THREADS fstat_result = _Py_fstat_noraise(self->fd, self->stat_atopen); Py_END_ALLOW_THREADS @@ -531,13 +529,11 @@ _io_FileIO___init___impl(fileio *self, PyObject *nameobj, const char *mode, self->fd = -1; if (self->fd >= 0) { PyObject *exc = PyErr_GetRaisedException(); - internal_close(self, true); + internal_close(self); _PyErr_ChainExceptions1(exc); } - if (self->stat_atopen != NULL) { - PyMem_Free(self->stat_atopen); - self->stat_atopen = NULL; - } + PyMem_Free(self->stat_atopen); + self->stat_atopen = NULL; done: #ifdef MS_WINDOWS From 0f6448333cd4bd719d34282dbb44b15afd071d36 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Wed, 9 Oct 2024 11:44:41 -0700 Subject: [PATCH 05/10] pyio: Always clear stat_atopen just before close --- Lib/_pyio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pyio.py b/Lib/_pyio.py index 150496b281131d..42b0aea4e2eb2e 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -1584,9 +1584,9 @@ def __init__(self, file, mode='r', closefd=True, opener=None): if e.errno != errno.ESPIPE: raise except: + self._stat_atopen = None if owned_fd is not None: os.close(owned_fd) - self._stat_atopen = None raise self._fd = fd From e2eebb061cb0ec6c7746dc35625ae69585590b4f Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Wed, 9 Oct 2024 11:47:28 -0700 Subject: [PATCH 06/10] Remove assert accidentally commited --- Modules/_io/fileio.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/_io/fileio.c b/Modules/_io/fileio.c index 56f78948c43038..727099ac77f2f7 100644 --- a/Modules/_io/fileio.c +++ b/Modules/_io/fileio.c @@ -462,7 +462,6 @@ _io_FileIO___init___impl(fileio *self, PyObject *nameobj, const char *mode, out the old fd (see: internal_close) should always nullify self->stat_atopen before this point. Just in case though, to prevent leaks, only allocate a new one if required. */ - assert(self->stat_atopen == NULL) if (self->stat_atopen != NULL) { self->stat_atopen = PyMem_New(struct _Py_stat_struct, 1); if (self->stat_atopen == NULL) { From 5299a04ba34312a7fd2c60277938d55b6663596a Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Wed, 9 Oct 2024 11:48:12 -0700 Subject: [PATCH 07/10] _io: fix conditional --- Modules/_io/fileio.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_io/fileio.c b/Modules/_io/fileio.c index 727099ac77f2f7..d8dea262dd037b 100644 --- a/Modules/_io/fileio.c +++ b/Modules/_io/fileio.c @@ -462,7 +462,7 @@ _io_FileIO___init___impl(fileio *self, PyObject *nameobj, const char *mode, out the old fd (see: internal_close) should always nullify self->stat_atopen before this point. Just in case though, to prevent leaks, only allocate a new one if required. */ - if (self->stat_atopen != NULL) { + if (self->stat_atopen == NULL) { self->stat_atopen = PyMem_New(struct _Py_stat_struct, 1); if (self->stat_atopen == NULL) { PyErr_NoMemory(); From d749709717c04e837b7f33c24f86131c232e9c3b Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Tue, 22 Oct 2024 14:40:27 -0700 Subject: [PATCH 08/10] Clear before internal_close to match _pyio os.close callsites --- Modules/_io/fileio.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Modules/_io/fileio.c b/Modules/_io/fileio.c index d8dea262dd037b..27413b5c12f526 100644 --- a/Modules/_io/fileio.c +++ b/Modules/_io/fileio.c @@ -180,6 +180,8 @@ _io_FileIO_close_impl(fileio *self, PyTypeObject *cls) PyErr_Clear(); } } + PyMem_Free(self->stat_atopen); + self->stat_atopen = NULL; rc = internal_close(self); if (res == NULL) { _PyErr_ChainExceptions1(exc); @@ -268,6 +270,8 @@ _io_FileIO___init___impl(fileio *self, PyObject *nameobj, const char *mode, assert(PyFileIO_Check(state, self)); #endif if (self->fd >= 0) { + PyMem_Free(self->stat_atopen); + self->stat_atopen = NULL; if (self->closefd) { /* Have to close the existing file first. */ if (internal_close(self) < 0) { From b429587d76e48b729ba3718b51dc9a35d812ac46 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Tue, 29 Oct 2024 20:50:41 -0700 Subject: [PATCH 09/10] Move to conditional free, simplify code, drop comment --- Modules/_io/fileio.c | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Modules/_io/fileio.c b/Modules/_io/fileio.c index 27413b5c12f526..1bdb1770fb5ddd 100644 --- a/Modules/_io/fileio.c +++ b/Modules/_io/fileio.c @@ -462,16 +462,13 @@ _io_FileIO___init___impl(fileio *self, PyObject *nameobj, const char *mode, #endif } - /* FileIO.__init__ may be called on an already initialized object. Closing - out the old fd (see: internal_close) should always nullify - self->stat_atopen before this point. Just in case though, to prevent - leaks, only allocate a new one if required. */ + if (self->stat_atopen != NULL) { + PyMem_Free(self->stat_atopen); + } + self->stat_atopen = PyMem_New(struct _Py_stat_struct, 1); if (self->stat_atopen == NULL) { - self->stat_atopen = PyMem_New(struct _Py_stat_struct, 1); - if (self->stat_atopen == NULL) { - PyErr_NoMemory(); - goto error; - } + PyErr_NoMemory(); + goto error; } Py_BEGIN_ALLOW_THREADS fstat_result = _Py_fstat_noraise(self->fd, self->stat_atopen); From e153d97899b9e94110ed5f16c4b02ee3f72a5822 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Wed, 30 Oct 2024 20:11:43 -0700 Subject: [PATCH 10/10] Apply review fixes --- Modules/_io/fileio.c | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Modules/_io/fileio.c b/Modules/_io/fileio.c index 1bdb1770fb5ddd..cf0f1d671b507a 100644 --- a/Modules/_io/fileio.c +++ b/Modules/_io/fileio.c @@ -180,8 +180,6 @@ _io_FileIO_close_impl(fileio *self, PyTypeObject *cls) PyErr_Clear(); } } - PyMem_Free(self->stat_atopen); - self->stat_atopen = NULL; rc = internal_close(self); if (res == NULL) { _PyErr_ChainExceptions1(exc); @@ -270,8 +268,6 @@ _io_FileIO___init___impl(fileio *self, PyObject *nameobj, const char *mode, assert(PyFileIO_Check(state, self)); #endif if (self->fd >= 0) { - PyMem_Free(self->stat_atopen); - self->stat_atopen = NULL; if (self->closefd) { /* Have to close the existing file first. */ if (internal_close(self) < 0) { @@ -462,9 +458,7 @@ _io_FileIO___init___impl(fileio *self, PyObject *nameobj, const char *mode, #endif } - if (self->stat_atopen != NULL) { - PyMem_Free(self->stat_atopen); - } + PyMem_Free(self->stat_atopen); self->stat_atopen = PyMem_New(struct _Py_stat_struct, 1); if (self->stat_atopen == NULL) { PyErr_NoMemory();