Skip to content

Segfault from zoneinfo with custom DateTime class #125318

Description

@robsdedude

Crash report

What happened?

Here's a minimal reproducer

import zoneinfo
from datetime import date


class MyDateTime:
    def __init__(
        self,
        year: int,
        month: int,
        day: int,
        hour: int = 0,
        minute: int = 0,
        second: int = 0,
    ) -> None:
        self.ordinal = date(year, month, day).toordinal()
        self.hour = hour
        self.minute = minute
        self.second = second

    def toordinal(self) -> int:
        return self.ordinal


def whoopsie():
    tz_berlin = zoneinfo.ZoneInfo("Europe/Berlin")
    dt = MyDateTime(1995, 9, 2, 14, 34, 56)
    print(tz_berlin.utcoffset(dt), flush=True)

If I run whoopsie, I get one of the following outcomes:

  • It prints 02:00:00
  • It prints 01:00:00
  • It prints 00:53:28
  • It segfaults
<project dir>/venv313/bin/python -X dev <project dir>/segfault.py 
Fatal Python error: Segmentation fault

Current thread 0x00007fd62be47b80 (most recent call first):
 File "<project dir>/segfault.py", line 27 in whoopsie
 File "<project dir>/segfault.py", line 34 in <module>

I've tried running the test in a loop and it seems to always spit out the same result within a single run. So I assume some global state, memory layout, or or such is involved.

CPython versions tested on:

3.9, 3.13

Operating systems tested on:

Linux

Output from running 'python -VV' on the command line:

Python 3.13.0 (main, Oct 10 2024, 13:32:18) [GCC 11.4.0]

Linked PRs

Activity

  1. ZeroIntensity commented on Oct 13, 2024

    @ZeroIntensity
    Member

    Oh wait, I figured out why I couldn't reproduce this before. The given reproducer does not call whoopsie(), so the script compiles and finishes just fine--silly me! Confirmed on all bugfix versions, thank you for the report!

  2. y5c4l3 commented on Oct 21, 2024

    @y5c4l3
    Contributor

    As the contract for tzinfo base class is not very clear and by the docstring in pure Python zoneinfo here, I would assume these methods should only accept datetime types. However, there isn't pretty much typecheck in both C zoneinfo and pure Python one.

    cpython/Lib/_pydatetime.py

    Lines 1290 to 1317 in 0cd2140

    def tzname(self, dt):
    "datetime -> string name of time zone."
    raise NotImplementedError("tzinfo subclass must override tzname()")
    def utcoffset(self, dt):
    "datetime -> timedelta, positive for east of UTC, negative for west of UTC"
    raise NotImplementedError("tzinfo subclass must override utcoffset()")
    def dst(self, dt):
    """datetime -> DST offset as timedelta, positive for east of UTC.
    Return 0 if DST not in effect. utcoffset() must include the DST
    offset.
    """
    raise NotImplementedError("tzinfo subclass must override dst()")
    def fromutc(self, dt):
    "datetime in UTC -> datetime in local time."
    if not isinstance(dt, datetime):
    raise TypeError("fromutc() requires a datetime argument")
    if dt.tzinfo is not self:
    raise ValueError("dt.tzinfo is not self")
    dtoff = dt.utcoffset()
    if dtoff is None:
    raise ValueError("fromutc() requires a non-None utcoffset() "
    "result")

    They only check the types at fromutc(dt).

    cpython/Modules/_zoneinfo.c

    Lines 615 to 618 in 0cd2140

    static PyObject *
    zoneinfo_fromutc(PyObject *obj_self, PyObject *dt)
    {
    if (!PyDateTime_Check(dt)) {

    def fromutc(self, dt):
    """Convert from datetime in UTC to datetime in local time"""
    if not isinstance(dt, datetime):
    raise TypeError("fromutc() requires a datetime argument")

    It turns out that the pure Python one could be leveraged safely by duck types:

    def _find_trans(self, dt):
    if dt is None:
    if self._fixed_offset:
    return self._tz_after
    else:
    return _NO_TTINFO
    ts = self._get_local_timestamp(dt)
    lt = self._trans_local[dt.fold]
    num_trans = len(lt)
    if num_trans and ts < lt[0]:
    return self._tti_before
    elif not num_trans or ts > lt[-1]:
    if isinstance(self._tz_after, _TZStr):
    return self._tz_after.get_trans_info(ts, dt.year, dt.fold)
    else:
    return self._tz_after

  3. y5c4l3 commented on Oct 21, 2024

    @y5c4l3
    Contributor

    @robsdedude The PR is a draft and only means to be some attempt (could be bad in theory and it is). Honestly I didn't find it interesting and correct to set fold = 0 and derive hacky ymd from toordinal(), which is only a part of the datetime "contract".

    If possible, we should:

    • either add type checks in all zoneinfo implementations to restrict the contract
    • or remove all type checks, and align the C module with pure Python zoneinfo behaviors

    Since the missing typecheck causes out-of-bounds read now, I think the best bet here is to add type checks first.

    @ZeroIntensity What would you think? Or shall we cc and discuss with datetime code owners instead?

  4. ZeroIntensity commented on Oct 21, 2024

    @ZeroIntensity
    Member

    @pganssle As a datetime expert, what do you think?

  5. pganssle commented on Oct 21, 2024

    @pganssle
    Member

    I'm not clear on what is causing the segfault here, but I think regardless of the contract of Zoneinfo, this needs to be fixed. It should not be possible to crash the interpreter using pure Python code.

    Or am I missing something, and my response is already the assumption? Is the question about whether this should raise an exception vs. doing something else?

  6. ZeroIntensity commented on Oct 21, 2024

    @ZeroIntensity
    Member

    Is the question about whether this should raise an exception vs. doing something else?

    Yeah, from my understanding it's about whether or not to try to ducktype a datetime class or just thrown a exception if it doesn't inherit.

  7. y5c4l3 commented on Oct 21, 2024

    @y5c4l3
    Contributor

    I'm not clear on what is causing the segfault here

    FYI PyDateTime_DATE_GET_FOLD on a non PyDateTime dt

    cpython/Modules/_zoneinfo.c

    Lines 2180 to 2199 in 5989eb7

    static _ttinfo *
    find_ttinfo(zoneinfo_state *state, PyZoneInfo_ZoneInfo *self, PyObject *dt)
    {
    // datetime.time has a .tzinfo attribute that passes None as the dt
    // argument; it only really has meaning for fixed-offset zones.
    if (dt == Py_None) {
    if (self->fixed_offset) {
    return &(self->tzrule_after.std);
    }
    else {
    return &(state->NO_TTINFO);
    }
    }
    int64_t ts;
    if (get_local_timestamp(dt, &ts)) {
    return NULL;
    }
    unsigned char fold = PyDateTime_DATE_GET_FOLD(dt);

  8. ZeroIntensity commented on Oct 21, 2024

    @ZeroIntensity
    Member

    IMO, duck-typing C code is a PITA, and we shouldn't encourage it. But, I don't know what the convention is for datetime--if we're supporting duck-typing already, then it makes sense to do it here as well.

  9. pganssle commented on Oct 21, 2024

    @pganssle
    Member

    Ah, sorry, I was on the phone and didn't really follow the context here. Thanks for the extra information.

    I think for this particular issue, we need to backport a fix (possibly even to versions receiving security-only-updates), so at least for the backports, we can't do anything even remotely breaking except in cases that were already broken. If enforcing that ZoneInfo accept only datetime subclasses would break use cases that were working before and not causing segfaults, we have to maintain that.

    For 3.13+, we can take a more principled stance. I'm kinda ambivalent about it. I am generally in favor of duck typing, but it is hard to detect that something actually matches the protocol — especially since we can't really declare easily that the semantic meaning of a method on a duck-typed "datetime" matches the semantics we are expecting. For example Pendulum is even a subclass of datetime, but they change the semantics of addition and subtraction, so we may be making invalid assumptions about how those work.

    That said, at the end of the day, bugs introduced by "false friend" classes are basically bugs for those classes, and that's really more an argument for not creating duck-typed datetime-like classes than it is for not supporting them.

    Assuming we do go with a mechanism that supports duck-typing, we will almost certainly want to do something similar to what we're doing with subclass constructors, where we have a "fast path" for datetime subclasses, and then a slow path that goes through the Python interface for anything else.

    For the backport only, I would say to just try to detect the situation where a segfault is going to happen and raise an exception.

  10. y5c4l3 commented on Oct 22, 2024

    @y5c4l3
    Contributor

    #125824 Here is the backport patch. I don't think the segfault could be precisely predicted. In favor of soundness / completeness, a type check is necessary, but would break existing usecases. The patch I made now allows only 1 step of out-of-bounds read (i.e. out-of-bounds value fold must be either 0 or 1).

    I've written a test that makes the out-of-bounds read more reproducible, but address sanitizer catches the UAF in my test:
    https://github.com/python/cpython/actions/runs/11452302563/job/31862970634

    Would you think it's reasonable to skip the test when address sanitizer is enabled?

  11. pganssle commented on Sep 18, 2025

    @pganssle
    Member

    I looked into this and it seems like we can pretty easily do this in a fully backwards-compatible way by dispatching to Python attribute lookups when the dt object is not a datetime.datetime, so I implemented that in #139132. I don't know if we want to consider this a valid thing to do, but at least now it won't segfault.

  12. removed
    3.12only security fixes
    3.13only security fixes
    3.14bugs and security fixes
    on Sep 18, 2025
  13. serhiy-storchaka commented on Sep 19, 2025

    @serhiy-storchaka
    Member

    This is something similar to #137754. There are two implementations of both datetime and zoneinfo in the stdlib: pure Python and C. But they only work with the corresponding implementation -- Python with Python, C with C. If we are going to support independent third-party implementations, we still have a lot of work to do. For example, ZoneInfo.fromutc() raises TypeError if the argument is not the instance of the right datetime.

  14. robsdedude commented on Oct 20, 2026

    @robsdedude
    ContributorAuthor

    This is a duplicate of or at least related to #111306

  15. added a commit that references this issue on Oct 20, 2026
  16. added a commit that references this issue on Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    extension-modulesC modules in the Modules dirtype-crashA hard crash of the interpreter, possibly with a core dump

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions