From d6f9a30643ae6bfd033328b7a5753f78f9303b62 Mon Sep 17 00:00:00 2001 From: Alon Faraj Date: Wed, 28 Oct 2026 15:52:01 +0200 Subject: [PATCH 1/3] gh-160773: Fix use-after-free in OSError.__str__ --- Lib/test/test_exceptions.py | 14 +++ ...-10-28-13-47-43.gh-issue-160773.7VFxkA.rst | 2 + Objects/exceptions.c | 85 ++++++++++++------- 3 files changed, 71 insertions(+), 30 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-28-13-47-43.gh-issue-160773.7VFxkA.rst diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 963da1b0eae27ce..5753b8b87d4b93a 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -1804,6 +1804,20 @@ def __init__(self, code, message, filename, filename2): exc = LeakingOSError(1, "some message", "filename.py", "filename2.py") exc.__init__(2, "another message", "filename3.py", "filename4.py") + def test_oserror_str_evil_mutation(self): + # gh-160773: str(OSError) must not crash when formatting one + # attribute clears the others. + class Evil: + def __repr__(self): + exc.filename = None + exc.filename2 = None + return 'evil' + + exc = OSError(2, Evil(), Evil()) + self.assertEqual(str(exc), "[Errno 2] evil: evil") + exc = OSError(2, Evil(), Evil(), None, Evil()) + self.assertEqual(str(exc), "[Errno 2] evil: evil -> evil") + def test_errno_ENOTDIR(self): # Issue #12802: "not a directory" errors are ENOTDIR even on Windows with self.assertRaises(OSError) as cm: diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-28-13-47-43.gh-issue-160773.7VFxkA.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-28-13-47-43.gh-issue-160773.7VFxkA.rst new file mode 100644 index 000000000000000..09d282e1dbeee7d --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-28-13-47-43.gh-issue-160773.7VFxkA.rst @@ -0,0 +1,2 @@ +Fix a crash in :func:`str` of an :exc:`OSError` when formatting one of its +attributes modifies the others. diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 86a64783372ceae..3a985b4648066ae 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2325,46 +2325,71 @@ static PyObject * OSError_str(PyObject *op) { PyOSErrorObject *self = PyOSErrorObject_CAST(op); + PyObject *res; + /* Hold strong references: formatting may run arbitrary code + which modifies the attributes. */ + PyObject *myerrno = Py_XNewRef(self->myerrno); + PyObject *strerror = Py_XNewRef(self->strerror); + PyObject *filename = Py_XNewRef(self->filename); + PyObject *filename2 = Py_XNewRef(self->filename2); +#ifdef MS_WINDOWS + PyObject *winerror = Py_XNewRef(self->winerror); +#endif #define OR_NONE(x) ((x)?(x):Py_None) #ifdef MS_WINDOWS /* If available, winerror has the priority over myerrno */ - if (self->winerror && self->filename) { - if (self->filename2) { - return PyUnicode_FromFormat("[WinError %S] %S: %R -> %R", - OR_NONE(self->winerror), - OR_NONE(self->strerror), - self->filename, - self->filename2); + if (winerror && filename) { + if (filename2) { + res = PyUnicode_FromFormat("[WinError %S] %S: %R -> %R", + OR_NONE(winerror), + OR_NONE(strerror), + filename, + filename2); } else { - return PyUnicode_FromFormat("[WinError %S] %S: %R", - OR_NONE(self->winerror), - OR_NONE(self->strerror), - self->filename); + res = PyUnicode_FromFormat("[WinError %S] %S: %R", + OR_NONE(winerror), + OR_NONE(strerror), + filename); } + goto done; + } + if (winerror && strerror) { + res = PyUnicode_FromFormat("[WinError %S] %S", + winerror ? winerror: Py_None, + strerror ? strerror: Py_None); + goto done; } - if (self->winerror && self->strerror) - return PyUnicode_FromFormat("[WinError %S] %S", - self->winerror ? self->winerror: Py_None, - self->strerror ? self->strerror: Py_None); #endif - if (self->filename) { - if (self->filename2) { - return PyUnicode_FromFormat("[Errno %S] %S: %R -> %R", - OR_NONE(self->myerrno), - OR_NONE(self->strerror), - self->filename, - self->filename2); + if (filename) { + if (filename2) { + res = PyUnicode_FromFormat("[Errno %S] %S: %R -> %R", + OR_NONE(myerrno), + OR_NONE(strerror), + filename, + filename2); } else { - return PyUnicode_FromFormat("[Errno %S] %S: %R", - OR_NONE(self->myerrno), - OR_NONE(self->strerror), - self->filename); + res = PyUnicode_FromFormat("[Errno %S] %S: %R", + OR_NONE(myerrno), + OR_NONE(strerror), + filename); } + goto done; } - if (self->myerrno && self->strerror) - return PyUnicode_FromFormat("[Errno %S] %S", - self->myerrno, self->strerror); - return BaseException_str(op); + if (myerrno && strerror) { + res = PyUnicode_FromFormat("[Errno %S] %S", + myerrno, strerror); + goto done; + } + res = BaseException_str(op); +done: + Py_XDECREF(myerrno); + Py_XDECREF(strerror); + Py_XDECREF(filename); + Py_XDECREF(filename2); +#ifdef MS_WINDOWS + Py_XDECREF(winerror); +#endif + return res; } static PyObject * From 8224d63eefd3744257ba5c162614c0812976a93a Mon Sep 17 00:00:00 2001 From: Alon Faraj Date: Wed, 28 Oct 2026 16:49:36 +0200 Subject: [PATCH 2/3] Fix test on Windows --- Lib/test/test_exceptions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 5753b8b87d4b93a..3883c0efb9ad4fc 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -1815,7 +1815,8 @@ def __repr__(self): exc = OSError(2, Evil(), Evil()) self.assertEqual(str(exc), "[Errno 2] evil: evil") - exc = OSError(2, Evil(), Evil(), None, Evil()) + exc = OSError(2, Evil(), Evil()) + exc.filename2 = Evil() self.assertEqual(str(exc), "[Errno 2] evil: evil -> evil") def test_errno_ENOTDIR(self): From 1954aa5e8c310facb3b79e1b56de55af9c9b42b2 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Thu, 29 Oct 2026 10:44:51 +0000 Subject: [PATCH 3/3] Nit: Drop redundant `Py_None` fallbacks in the WinError branch --- Objects/exceptions.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 3a985b4648066ae..e2f757c5ee9c0be 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2355,8 +2355,7 @@ OSError_str(PyObject *op) } if (winerror && strerror) { res = PyUnicode_FromFormat("[WinError %S] %S", - winerror ? winerror: Py_None, - strerror ? strerror: Py_None); + winerror, strerror); goto done; } #endif