diff --git a/Lib/test/test_zoneinfo/test_zoneinfo_property.py b/Lib/test/test_zoneinfo/test_zoneinfo_property.py index c00815e2fd4c36..7c68cf0da67169 100644 --- a/Lib/test/test_zoneinfo/test_zoneinfo_property.py +++ b/Lib/test/test_zoneinfo/test_zoneinfo_property.py @@ -4,9 +4,7 @@ import pickle import unittest import zoneinfo - from test.support.hypothesis_helper import hypothesis - import test.test_zoneinfo._support as test_support ZoneInfoTestBase = test_support.ZoneInfoTestBase @@ -61,6 +59,50 @@ def valid_key(key): return output +def _make_datetime_class(missing): + all_attrs = ( + "year", + "month", + "day", + "hour", + "minute", + "second", + "microsecond", + "tzinfo", + "fold", + ) + included_attrs = set(all_attrs) - set(missing) + + class ClassWithMissing: + def __init__(self, *args, **kwargs): + self._datetime = datetime.datetime(*args, **kwargs) + for attr, arg in zip(all_attrs, args): + if attr in kwargs: + raise ValueError( + f"{attr} specified more than once in argument list" + ) + + kwargs[attr] = arg + + for attr, arg in kwargs.items(): + if attr in included_attrs: + setattr(self, attr, arg) + + def utcoffset(self): + return self.tzinfo.utcoffset(self) + + def dst(self): + return self.tzinfo.dst(self) + + def tzname(self): + return self.tzinfo.tzname(self) + + def toordinal(self): + return self._datetime.toordinal() + + return ClassWithMissing + + VALID_KEYS = _valid_keys() if not VALID_KEYS: raise unittest.SkipTest("No time zone data available") @@ -127,6 +169,90 @@ def test_utc(self, dt): self.assertEqual(dt_zi.dst(), ZERO) self.assertEqual(dt_zi.tzname(), "UTC") + @hypothesis.given( + dt=hypothesis.strategies.datetimes(), + key=valid_keys(), + missing=hypothesis.strategies.lists( + hypothesis.strategies.sampled_from( + ( + "year", + "month", + "day", + "hour", + "minute", + "second", + "microsecond", + "tzinfo", + "fold", + ) + ), + unique=True, + min_size=1, + ), + ) + @hypothesis.example( + dt=datetime.datetime(1995, 9, 2, 14, 34, 56), + key="Europe/Berlin", + missing=["fold"], + ) + @hypothesis.example( + dt=datetime.datetime(1995, 9, 2, 14, 34, 56), + key="Europe/Berlin", + missing=["year"], + ) + @hypothesis.example( + dt=datetime.datetime(1995, 9, 2, 14, 34, 56), + key="Europe/Berlin", + missing=["tzinfo"], + ) + @hypothesis.example( + dt=datetime.datetime(1995, 9, 2, 14, 34, 56), + key="Europe/Berlin", + missing=[ + "year", + "month", + "day", + "hour", + "minute", + "second", + "microsecond", + "tzinfo", + "fold", + ], + ) + def test_bad_duck_typed_class(self, dt, key, missing): + # Passing duck typed `datetime`-lookalikes is not actively supported, + # but it also shouldn't raise segfaults. + + tzi = self.module.ZoneInfo(key) + DateTimeClass = _make_datetime_class(missing=missing) + false_friend = DateTimeClass( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tzinfo=tzi, + fold=dt.fold, + ) + + try: + false_friend.utcoffset() + except Exception: + pass + + try: + false_friend.dst() + except Exception: + pass + + try: + false_friend.tzname() + except Exception: + pass + class CZoneInfoTest(ZoneInfoTest): module = c_zoneinfo diff --git a/Misc/NEWS.d/next/Library/2025-09-18-16-40-18.gh-issue-125318.JzZysW.rst b/Misc/NEWS.d/next/Library/2025-09-18-16-40-18.gh-issue-125318.JzZysW.rst new file mode 100644 index 00000000000000..4ed3cac4b0f41a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-18-16-40-18.gh-issue-125318.JzZysW.rst @@ -0,0 +1,3 @@ +Fixes a segmentation fault that would happen if :class:`zoneinfo.ZoneInfo` +were used with certain non-:class:`datetime.datetime` classes. Patch by Paul +Ganssle diff --git a/Modules/_zoneinfo.c b/Modules/_zoneinfo.c index b99be073db5460..e816a37eea6365 100644 --- a/Modules/_zoneinfo.c +++ b/Modules/_zoneinfo.c @@ -123,6 +123,9 @@ static const int SOURCE_FILE = 2; static const size_t ZONEINFO_STRONG_CACHE_MAX_SIZE = 8; +static const int MINYEAR = 1; +static const int MAXYEAR = 9999; + // Forward declarations static int load_data(zoneinfo_state *state, PyZoneInfo_ZoneInfo *self, @@ -2197,7 +2200,29 @@ find_ttinfo(zoneinfo_state *state, PyZoneInfo_ZoneInfo *self, PyObject *dt) return NULL; } - unsigned char fold = PyDateTime_DATE_GET_FOLD(dt); + unsigned char fold; + if (PyDateTime_Check(dt)) { + fold = PyDateTime_DATE_GET_FOLD(dt); + } + else { + PyObject *fold_obj = PyObject_GetAttrString(dt, "fold"); + if (fold_obj == NULL) { + return NULL; + } + + long fold_int = PyLong_AsLong(fold_obj); + Py_DECREF(fold_obj); + if (PyErr_Occurred()) { + return NULL; + } + if (fold_int < 0 || fold_int > 2) { + PyErr_Format(PyExc_ValueError, + "fold must be 0 or 1, got %d", fold_int); + return NULL; + } + fold = (unsigned char)fold_int; + } + assert(fold < 2); int64_t *local_transitions = self->trans_list_wall[fold]; size_t num_trans = self->num_transitions; @@ -2206,8 +2231,33 @@ find_ttinfo(zoneinfo_state *state, PyZoneInfo_ZoneInfo *self, PyObject *dt) return self->ttinfo_before; } else if (!num_trans || ts > local_transitions[self->num_transitions - 1]) { - return find_tzrule_ttinfo(&(self->tzrule_after), ts, fold, - PyDateTime_GET_YEAR(dt)); + int year; + if (PyDateTime_Check(dt)) { + year = PyDateTime_GET_YEAR(dt); + } + else { + PyObject *year_obj = PyObject_GetAttrString(dt, "year"); + if (year_obj == NULL) { + return NULL; + } + + year = PyLong_AsInt(year_obj); + Py_DECREF(year_obj); + if (PyErr_Occurred()) { + return NULL; + } + + if (year < MINYEAR || year > 9999) { + PyErr_Format(PyExc_ValueError, + "year out of range, should be in (%d, %d) but got %d", + MINYEAR, + MAXYEAR, + year + ); + return NULL; + } + } + return find_tzrule_ttinfo(&(self->tzrule_after), ts, fold, year); } else { size_t idx = _bisect(ts, local_transitions, self->num_transitions) - 1;