From 6efe8474683ba9c9dfafe882ff09fc2b0c177435 Mon Sep 17 00:00:00 2001 From: Zakariya Date: Tue, 24 Feb 2026 19:06:04 +0500 Subject: [PATCH] gh-145166: Fix crash in tzinfo.fromutc() when subclass __new__ returns non-datetime --- Lib/_pydatetime.py | 18 +++++- Lib/test/datetimetester.py | 62 +++++++++++++++++++ ...-02-24-19-14-43.gh-issue-145166.bG_Rp2.rst | 4 ++ Modules/_datetimemodule.c | 20 ++++-- 4 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-02-24-19-14-43.gh-issue-145166.bG_Rp2.rst diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index b6d68f2372850a..a60d60aecc9c94 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -1348,11 +1348,22 @@ def fromutc(self, dt): delta = dtoff - dtdst if delta: dt += delta + if not isinstance(dt, datetime): + raise TypeError( + f"datetime arithmetic on a subclass returned non-datetime " + f"(type {type(dt).__name__})" + ) dtdst = dt.dst() if dtdst is None: raise ValueError("fromutc(): dt.dst gave inconsistent " "results; cannot convert") - return dt + dtdst + result = dt + dtdst + if not isinstance(result, datetime): + raise TypeError( + f"datetime arithmetic on a subclass returned non-datetime " + f"(type {type(result).__name__})" + ) + return result # Pickle support. @@ -2063,6 +2074,11 @@ def utctimetuple(self): offset = self.utcoffset() if offset: self -= offset + if not isinstance(self, datetime): + raise TypeError( + f"datetime arithmetic on a subclass returned non-datetime " + f"(type {type(self).__name__})" + ) y, m, d = self.year, self.month, self.day hh, mm, ss = self.hour, self.minute, self.second return _build_struct_time(y, m, d, hh, mm, ss, 0) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 97eec618932aa5..e48687ff4d3385 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -5133,6 +5133,68 @@ def test_pickling(self): self.assertEqual(derived.tzname(), 'cookie') self.assertEqual(orig.__reduce__(), orig.__reduce_ex__(2)) + def test_fromutc_subclass_new_returns_non_datetime(self): + call_count = 0 + + class EvilDatetime(self.theclass): + def __new__(cls, *args, **kwargs): + nonlocal call_count + call_count += 1 + if call_count > 1: + return bytearray(b'\x00' * 200) + return super().__new__(cls, *args, **kwargs) + + class SimpleTZ(tzinfo): + def utcoffset(self, dt): return timedelta(hours=1) + def dst(self, dt): return timedelta(hours=1) + def tzname(self, dt): return "Test" + + tz = SimpleTZ() + dt = EvilDatetime(2000, 1, 1, 12, 0, 0, tzinfo=tz) + with self.assertRaises(TypeError): + tz.fromutc(dt) + + def test_fromutc_subclass_new_returns_non_datetime_with_delta(self): + call_count = 0 + + class EvilDatetime(self.theclass): + def __new__(cls, *args, **kwargs): + nonlocal call_count + call_count += 1 + if call_count > 1: + return bytearray(b'\x00' * 200) + return super().__new__(cls, *args, **kwargs) + class SimpleTZ(tzinfo): + def utcoffset(self, dt): return timedelta(hours=2) + def dst(self, dt): return timedelta(hours=1) + def tzname(self, dt): return "Test" + + tz = SimpleTZ() + dt = EvilDatetime(2000, 1, 1, 12, 0, 0, tzinfo=tz) + with self.assertRaises(TypeError): + tz.fromutc(dt) + + def test_utctimetuple_subclass_new_returns_non_datetime(self): + call_count = 0 + + class EvilDatetime(self.theclass): + def __new__(cls, *args, **kwargs): + nonlocal call_count + call_count += 1 + if call_count > 1: + return bytearray(b'\x00' * 200) + return super().__new__(cls, *args, **kwargs) + + class SimpleTZ(tzinfo): + def utcoffset(self, dt): return timedelta(hours=5) + def dst(self, dt): return timedelta(0) + def tzname(self, dt): return "Test" + + tz = SimpleTZ() + dt = EvilDatetime(2000, 6, 15, 12, 0, 0, tzinfo=tz) + with self.assertRaises(TypeError): + dt.utctimetuple() + def test_compat_unpickle(self): tests = [ b'cdatetime\ndatetime\n' diff --git a/Misc/NEWS.d/next/Library/2026-02-24-19-14-43.gh-issue-145166.bG_Rp2.rst b/Misc/NEWS.d/next/Library/2026-02-24-19-14-43.gh-issue-145166.bG_Rp2.rst new file mode 100644 index 00000000000000..a7c18f4e12b23e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-24-19-14-43.gh-issue-145166.bG_Rp2.rst @@ -0,0 +1,4 @@ +Fix crash in :meth:`datetime.tzinfo.fromutc` and +:meth:`datetime.datetime.utctimetuple` when a +:class:`~datetime.datetime` subclass ``__new__`` returns a non-datetime +object. A :exc:`TypeError` is now raised instead. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 8f64e572bd6086..758c19eae8c51e 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -4168,7 +4168,6 @@ tzinfo_fromutc(PyObject *self, PyObject *dt) result = add_datetime_timedelta((PyDateTime_DateTime *)dt, delta, 1); if (result == NULL) goto Fail; - Py_DECREF(dst); dst = call_dst(GET_DT_TZINFO(dt), result); if (dst == NULL) @@ -6210,11 +6209,20 @@ add_datetime_timedelta(PyDateTime_DateTime *date, PyDateTime_Delta *delta, return NULL; } - return new_datetime_subclass_ex(year, month, day, - hour, minute, second, microsecond, - HASTZINFO(date) ? date->tzinfo : Py_None, - Py_TYPE(date)); -} + PyObject *result = new_datetime_subclass_ex(year, month, day, + hour, minute, second, microsecond, + HASTZINFO(date) ? date->tzinfo : Py_None, + Py_TYPE(date)); + if (result != NULL && !PyDateTime_Check(result)) { + PyErr_Format(PyExc_TypeError, + "datetime arithmetic on a subclass returned " + "non-datetime (type %.200s)", + Py_TYPE(result)->tp_name); + Py_DECREF(result); + return NULL; + } + return result; + } static PyObject * datetime_add(PyObject *left, PyObject *right)