python / python/cpython

`datetime` : Type Confusion in tzinfo_fromutc()

Open
#145,166 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

extension-modules type-crash
Dominant language
Python
Stars
77.2k
Forks
35.9k
PR merge metrics
PR metrics pending

Description

Crash report

What happened?

In tzinfo_fromutc(), the first call to add_datetime_timedelta() at line 4168 internally calls new_datetime_subclass_ex(), which invokes the subclass __new__() method. If the subclass __new__() returns an object that is not a datetime instance, the code proceeds to use it as a PyDateTime_DateTime * without any type check.

At line 4179, this non-datetime object is passed to add_datetime_timedelta() again with a direct (PyDateTime_DateTime *) cast:

// Line 4168 — first call, result can be any Python object
result = add_datetime_timedelta((PyDateTime_DateTime *)dt, delta, 1);
if (result == NULL)
    goto Fail;

// Line 4179 — second call, result cast to PyDateTime_DateTime* without type check
Py_SETREF(result, add_datetime_timedelta((PyDateTime_DateTime *)result,
                                         (PyDateTime_Delta *)dst, 1));

Inside add_datetime_timedelta() (line 6194–6217), the accessor macros GET_YEAR(), GET_MONTH(), GET_DAY() etc. read from fixed offsets relative to the PyDateTime_DateTime struct layout. When the object is actually a bytearray, these offsets correspond to entirely different fields (ob_alloc, ob_bytes, ob_start), resulting in garbage values.

import datetime

call_count = 0

class EvilDatetime(datetime.datetime):
    def __new__(cls, *args, **kwargs):
        global call_count
        call_count += 1
        if call_count > 1:
            return bytearray(b'\x00' * 200)
        return super().__new__(cls, *args, **kwargs)


class SimpleTZ(datetime.tzinfo):
    def utcoffset(self, dt):
        return datetime.timedelta(hours=2)
    def dst(self, dt):
        return datetime.timedelta(hours=1)
    def tzname(self, dt):
        return "Test"


tz = SimpleTZ()
call_count = 0
dt = EvilDatetime(2000, 1, 1, 12, 0, 0, tzinfo=tz)
tz.fromutc(dt)  # Crash
Direct execution (ASAN build)
$ ./build-asan/python fromutc_type_confusion.py
python: ../Modules/_datetimemodule.c:770: normalize_y_m_d: Assertion `1 <= *m && *m <= 12' failed.
Exit code: 134

The interpreter aborts due to the assertion 1 <= *m && *m <= 12 in normalize_y_m_d() (line 770), because GET_MONTH() reads a zero value from the bytearray's ob_alloc field instead of the datetime's data[2] field.

Crash backtrace
(gdb) continue

Program received signal SIGABRT, Aborted.

(gdb) backtrace
#0  __pthread_kill_implementation (signo=6) at ./nptl/pthread_kill.c:44
#1  __pthread_kill_internal (signo=6) at ./nptl/pthread_kill.c:78
#2  __GI___pthread_kill (signo=6) at ./nptl/pthread_kill.c:89
#3  __GI_raise (sig=6) at ../sysdeps/posix/raise.c:26
#4  __GI_abort () at ./stdlib/abort.c:79
#5  __assert_fail_base (assertion=0x55555611ad20 "1 <= *m && *m <= 12",
    file=0x555556119940 "../Modules/_datetimemodule.c", line=770,
    function=0x55555611fb40 "normalize_y_m_d") at ./assert/assert.c:96
#6  __assert_fail (assertion=0x55555611ad20 "1 <= *m && *m <= 12",
    file=0x555556119940 "../Modules/_datetimemodule.c", line=770,
    function=0x55555611fb40 "normalize_y_m_d") at ./assert/assert.c:105
#7  normalize_y_m_d (m=0x7ffff551ec30) at ../Modules/_datetimemodule.c:770
#8  normalize_date at ../Modules/_datetimemodule.c:831
#9  normalize_datetime at ../Modules/_datetimemodule.c:846
#10 add_datetime_timedelta (date=0x5080000d4b30, delta=0x50600005dd50, factor=1)
    at ../Modules/_datetimemodule.c:6208
#11 tzinfo_fromutc at ../Modules/_datetimemodule.c:4179
CPython versions tested on:

CPython main branch

Operating systems tested on:

macOS, Linux

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

Python 3.15.0a6+

Linked PRs
  • gh-145181

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Read Modules/_datetimemodule.c around tzinfo_fromutc() and add_datetime_timedelta(), then run the provided reproducer with an ASAN build. Done means the non-datetime subclass result no longer reaches datetime field access and a regression test covers the crash case. PR gh-145181 is already linked.

Written by the indexing model from the issue text.

Assessment

Tech stack
c, python
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.