Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 128 additions & 2 deletions Lib/test/test_zoneinfo/test_zoneinfo_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
38 changes: 33 additions & 5 deletions Modules/_zoneinfo.c
Original file line number Diff line number Diff line change
Expand Up @@ -2197,7 +2197,22 @@ 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 {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} else {
}
else {

PyObject *fold_obj = PyObject_GetAttrString(dt, "fold");
if (fold_obj == NULL) {
return NULL;
}

fold = (unsigned char)PyLong_AsLong(fold_obj);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strictly speaking, this is an undefined behavior. You can use (unsigned char)(unsigned long) if you are fine with silent ignoring the higher bits.

But assert(fold < 2) below can fail, because fold can be in range 0-255 here. We need more strict runtime check to ensure that it is only 0 or 1.

long fold_long = PyLong_AsLong(fold_obj);
Py_DECREF(fold_obj);
if (...) {...}
fold = `(unsigned char)fold_long;

Py_DECREF(fold_obj);
if (PyErr_Occurred()) {
return NULL;
}
}

assert(fold < 2);
int64_t *local_transitions = self->trans_list_wall[fold];
size_t num_trans = self->num_transitions;
Expand All @@ -2206,10 +2221,23 @@ 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));
}
else {
int year;
if (PyDateTime_Check(dt)) {
year = PyDateTime_GET_YEAR(dt);
} else {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick.

Suggested change
} else {
}
else {

PyObject *year_obj = PyObject_GetAttrString(dt, "year");
if (year_obj == NULL) {
return NULL;
}

year = PyLong_AsLong(year_obj);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible integer overflow here. Use PyLong_AsInt().

And do we need an additional range check before passing it to find_tzrule_ttinfo() or it works with the full range of int?

Py_DECREF(year_obj);
if (PyErr_Occurred()) {
return NULL;
}
}
return find_tzrule_ttinfo(&(self->tzrule_after), ts, fold, year);
} else {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} else {
}
else {

size_t idx = _bisect(ts, local_transitions, self->num_transitions) - 1;
assert(idx < self->num_transitions);
return self->trans_ttinfos[idx];
Expand Down
Loading