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. added
    type-crashA hard crash of the interpreter, possibly with a core dump
    on Oct 11, 2024
  2. added
    3.12only security fixes
    3.13only security fixes
    3.14bugs and security fixes
    and removed
    3.12only security fixes
    3.13only security fixes
    3.14bugs and security fixes
    on Oct 11, 2024
  3. ZeroIntensity commented on Oct 11, 2024

    @ZeroIntensity
    Member

    Hmm, I wasn't able to reproduce this on Linux, and Valgrind doesn't report any memory errors either. (That's what I get for prematurely applying version labels, heh.)

    Could you run a debugger on your end (with the PYTHONMALLOC=malloc environment variable enabled)?

  4. y5c4l3 commented on Oct 11, 2024

    @y5c4l3
    Contributor

    Confirmed on Python 3.13.0

    Program received signal SIGSEGV, Segmentation fault.
    0x00007ffff7b46b51 in find_ttinfo (state=<optimized out>, self=self@entry=0x7ffff778b920, dt=0x7ffff77ccec0) at /usr/local/src/conda/python-3.13.0/Modules/_zoneinfo.c:2202
    warning: 2202   /usr/local/src/conda/python-3.13.0/Modules/_zoneinfo.c: No such file or directory
    (gdb) bt
    #0  0x00007ffff7b46b51 in find_ttinfo (state=<optimized out>, self=self@entry=0x7ffff778b920, dt=0x7ffff77ccec0) at /usr/local/src/conda/python-3.13.0/Modules/_zoneinfo.c:2202
    #1  0x00007ffff7b47009 in zoneinfo_ZoneInfo_utcoffset_impl (dt=<optimized out>, cls=0x555555c10cf0, self=0x7ffff778b920) at /usr/local/src/conda/python-3.13.0/Modules/_zoneinfo.c:554
    #2  zoneinfo_ZoneInfo_utcoffset (self=0x7ffff778b920, cls=0x555555c10cf0, args=<optimized out>, nargs=<optimized out>, kwnames=<optimized out>) at /usr/local/src/conda/python-3.13.0/Modules/clinic/_zoneinfo.c.h:229
    #3  0x00005555557741d2 in method_vectorcall_FASTCALL_KEYWORDS_METHOD (func=0x7ffff760a480, args=0x7ffff7b5d0f8, nargsf=<optimized out>, kwnames=<optimized out>) at /usr/local/src/conda/python-3.13.0/Objects/descrobject.c:380
    #4  0x000055555570a8fe in _PyObject_VectorcallTstate (kwnames=<optimized out>, nargsf=<optimized out>, args=<optimized out>, callable=0x7ffff760a480, tstate=0x555555aaf640 <_PyRuntime+282976>)
        at /usr/local/src/conda/python-3.13.0/Include/internal/pycore_call.h:168
    #5  PyObject_Vectorcall (callable=0x7ffff760a480, args=<optimized out>, nargsf=<optimized out>, kwnames=<optimized out>) at /usr/local/src/conda/python-3.13.0/Objects/call.c:327
    #6  0x00005555555f2967 in _PyEval_EvalFrameDefault (tstate=<optimized out>, frame=<optimized out>, throwflag=<optimized out>) at /usr/local/src/conda/python-3.13.0/Python/generated_cases.c.h:813
    #7  0x00005555557c3e51 in PyEval_EvalCode (co=co@entry=0x7ffff7764580, globals=globals@entry=0x7ffff772ff80, locals=locals@entry=0x7ffff772ff80) at /usr/local/src/conda/python-3.13.0/Python/ceval.c:596
    #8  0x00005555557e8ad0 in run_eval_code_obj (tstate=tstate@entry=0x555555aaf640 <_PyRuntime+282976>, co=co@entry=0x7ffff7764580, globals=globals@entry=0x7ffff772ff80, locals=locals@entry=0x7ffff772ff80)
        at /usr/local/src/conda/python-3.13.0/Python/pythonrun.c:1323
    #9  0x00005555557e3b2c in run_mod (mod=mod@entry=0x555555be7148, filename=filename@entry=0x7ffff7746330, globals=globals@entry=0x7ffff772ff80, locals=locals@entry=0x7ffff772ff80, flags=flags@entry=0x7fffffffdb78,
        arena=arena@entry=0x7ffff7b9fbf0, interactive_src=0x0, generate_new_source=0) at /usr/local/src/conda/python-3.13.0/Python/pythonrun.c:1408
    #10 0x00005555558028c6 in pyrun_file (fp=fp@entry=0x555555b401e0, filename=filename@entry=0x7ffff7746330, start=start@entry=257, globals=globals@entry=0x7ffff772ff80, locals=locals@entry=0x7ffff772ff80, closeit=closeit@entry=1,
        flags=0x7fffffffdb78) at /usr/local/src/conda/python-3.13.0/Python/pythonrun.c:1241
    #11 0x0000555555801358 in _PyRun_SimpleFileObject (fp=fp@entry=0x555555b401e0, filename=filename@entry=0x7ffff7746330, closeit=closeit@entry=1, flags=flags@entry=0x7fffffffdb78) at /usr/local/src/conda/python-3.13.0/Python/pythonrun.c:490
    #12 0x00005555558010ec in _PyRun_AnyFileObject (fp=0x555555b401e0, filename=filename@entry=0x7ffff7746330, closeit=closeit@entry=1, flags=flags@entry=0x7fffffffdb78) at /usr/local/src/conda/python-3.13.0/Python/pythonrun.c:77
    #13 0x00005555557fabfe in pymain_run_file_obj (skip_source_first_line=0, filename=0x7ffff7746330, program_name=0x7ffff776a010) at /usr/local/src/conda/python-3.13.0/Modules/main.c:409
    #14 pymain_run_file (config=0x555555a81d38 <_PyRuntime+96344>) at /usr/local/src/conda/python-3.13.0/Modules/main.c:428
    #15 pymain_run_python (exitcode=0x7fffffffdb6c) at /usr/local/src/conda/python-3.13.0/Modules/main.c:696
    #16 Py_RunMain () at /usr/local/src/conda/python-3.13.0/Modules/main.c:775
    #17 0x00005555557b3d57 in Py_BytesMain (argc=<optimized out>, argv=<optimized out>) at /usr/local/src/conda/python-3.13.0/Modules/main.c:829
    #18 0x00007ffff7cabe08 in ?? () from /usr/lib/libc.so.6
    #19 0x00007ffff7cabecc in __libc_start_main () from /usr/lib/libc.so.6
    #20 0x00005555557b314e in _start ()
  5. ZeroIntensity commented on Oct 11, 2024

    @ZeroIntensity
    Member

    Oh wait, I think I tested it on main, not 3.13. Though, 3.12 didn't reproduce it either. It's rare for bugs to be specific to one version, because things typically get backported, but I guess it's possible.

  6. y5c4l3 commented on Oct 11, 2024

    @y5c4l3
    Contributor

    Oh wait, I think I tested it on main, not 3.13. Though, 3.12 didn't reproduce it either. It's rare for bugs to be specific to one version, because things typically get backported, but I guess it's possible.

    Can even confirm the bug up to Python 3.10 due to out-of-bounds read of PyDateTime_DateTime->fold on non-datetime types. That explains why it behaves randomly.

    I really doubt whether it's a proper use case of passing custom toordinal types. Despite of being undocumented, it's already here and there.

    I could make a draft PR on this.

  7. robsdedude commented on Oct 12, 2024

    @robsdedude
    ContributorAuthor

    My 2 cents: I very much dislike how many of the temporal type related things (e.g. datetime.timezone insist on only working with objects of datetime types. Why isn't duck typing a thing here as in many other places in Python?

    Use case: I'm the maintainer of the neo4j Python driver. Neo4j supports temporal types with nanosecond precision. So until Python ships with this feature (there's an open issue and wip PR somewhere), we ship custom temporal types. Since pytz mostly works with duck typing, but zoneinfo and datetime.timezone don't, we have little choice but to rely on pytz only.

    Why not inherit from datetime types? Because the types work a little different and have for instance different signatures for instantiation but can still provide the required information needed for timezone computation (e.g., comparison, arithmetics, toordinal).

    I'd much prefer to just get an AttributeError telling me that fold is missing over an inheritance check (or a segfault 😉).

    EDIT: looking at the PR I might have misunderstood what you meant by adding a typecheck while mentioning that you couldn't imagine a use-case for calling this with an object not inheriting from datetime.datetime. The PR looks like it would still work with such an object (but ignoring its fold and assuming 0?).

  8. 6 remaining items

  9. 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?

  10. 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.

  11. 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);

  12. 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.

  13. 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.

  14. 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?

  15. 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.

  16. removed
    3.12only security fixes
    3.13only security fixes
    3.14bugs and security fixes
    on Sep 18, 2025
  17. 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.

  18. robsdedude commented on Oct 20, 2026

    @robsdedude
    ContributorAuthor

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

  19. added a commit that references this issue on Oct 20, 2026
  20. 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