From 77ab121a908708f443d30c9e518e2c7a1528767c Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 7 Jun 2026 17:30:07 +0300 Subject: [PATCH 1/4] gh-151039: Fix a crash when `datetime.timedelta` outlives `_datetime` module --- Lib/test/datetimetester.py | 23 +++++++++++++ ...-06-07-17-29-33.gh-issue-151039.AZ0qBn.rst | 1 + Modules/_datetimemodule.c | 34 +++++++++++++++++-- 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-07-17-29-33.gh-issue-151039.AZ0qBn.rst diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 5d5b8e415f3cd2..64189d185b4310 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -7509,6 +7509,29 @@ def func(): self.assertEqual(out, b"a" * 8) self.assertEqual(err, b"") + @support.cpython_only + def test_gh_151039(self): + # gh-151039: This code used to crash + script = """if True: + import sys, gc + import _datetime + + td = _datetime.timedelta # static C type, survives the module + del sys.modules['_datetime'] + del _datetime + sys.modules['_datetime'] = None # block re-import + gc.collect() # module object is collected + + try: + td(seconds=2) # used to be a segmentation fault + except ImportError: + pass + else: + assert False, "ImportError not raised" + """ + rc, out, err = script_helper.assert_python_ok("-c", script) + self.assertEqual(rc, 0) + def load_tests(loader, standard_tests, pattern): standard_tests.addTest(ZoneInfoCompleteTest()) diff --git a/Misc/NEWS.d/next/Library/2026-06-07-17-29-33.gh-issue-151039.AZ0qBn.rst b/Misc/NEWS.d/next/Library/2026-06-07-17-29-33.gh-issue-151039.AZ0qBn.rst new file mode 100644 index 00000000000000..c8facd46174e5e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-07-17-29-33.gh-issue-151039.AZ0qBn.rst @@ -0,0 +1 @@ +Fix a crash when :class:`datetime.timezone` outlives ``_datetime`` module. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 59af7afcfcc644..15e508f2360bcb 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -141,7 +141,10 @@ get_current_module(PyInterpreterState *interp) } if (ref != NULL) { if (ref != Py_None) { - (void)PyWeakref_GetRef(ref, &mod); + if (PyWeakref_GetRef(ref, &mod) < 0) { + Py_DECREF(ref); + goto error; + } if (mod == Py_None) { Py_CLEAR(mod); } @@ -163,7 +166,6 @@ _get_current_state(PyObject **p_mod) PyInterpreterState *interp = PyInterpreterState_Get(); PyObject *mod = get_current_module(interp); if (mod == NULL) { - assert(!PyErr_Occurred()); if (PyErr_Occurred()) { return NULL; } @@ -2130,6 +2132,10 @@ delta_to_microseconds(PyDateTime_Delta *self) PyObject *current_mod = NULL; datetime_state *st = GET_CURRENT_STATE(current_mod); + if (st == NULL) { + assert(current_mod == NULL); + return NULL; + } x1 = PyLong_FromLong(GET_TD_DAYS(self)); if (x1 == NULL) @@ -2209,6 +2215,10 @@ microseconds_to_delta_ex(PyObject *pyus, PyTypeObject *type) PyObject *current_mod = NULL; datetime_state *st = GET_CURRENT_STATE(current_mod); + if (st == NULL) { + assert(current_mod == NULL); + return NULL; + } tuple = checked_divmod(pyus, CONST_US_PER_SECOND(st)); if (tuple == NULL) { @@ -2817,6 +2827,10 @@ delta_new_impl(PyTypeObject *type, PyObject *days, PyObject *seconds, PyObject *current_mod = NULL; datetime_state *st = GET_CURRENT_STATE(current_mod); + if (st == NULL) { + assert(current_mod == NULL); + return NULL; + } PyObject *x = NULL; /* running sum of microseconds */ PyObject *y = NULL; /* temp sum of microseconds */ @@ -3016,6 +3030,10 @@ delta_total_seconds(PyObject *op, PyObject *Py_UNUSED(dummy)) PyObject *current_mod = NULL; datetime_state *st = GET_CURRENT_STATE(current_mod); + if (st == NULL) { + assert(current_mod == NULL); + return NULL; + } total_seconds = PyNumber_TrueDivide(total_microseconds, CONST_US_PER_SECOND(st)); @@ -3869,6 +3887,10 @@ date_isocalendar(PyObject *self, PyObject *Py_UNUSED(dummy)) PyObject *current_mod = NULL; datetime_state *st = GET_CURRENT_STATE(current_mod); + if (st == NULL) { + assert(current_mod == NULL); + return NULL; + } PyObject *v = iso_calendar_date_new_impl(ISOCALENDAR_DATE_TYPE(st), year, week + 1, day + 1); @@ -6802,6 +6824,10 @@ local_timezone(PyDateTime_DateTime *utc_time) PyObject *current_mod = NULL; datetime_state *st = GET_CURRENT_STATE(current_mod); + if (st == NULL) { + assert(current_mod == NULL); + return NULL; + } delta = datetime_subtract((PyObject *)utc_time, CONST_EPOCH(st)); RELEASE_CURRENT_STATE(st, current_mod); @@ -7049,6 +7075,10 @@ datetime_timestamp(PyObject *op, PyObject *Py_UNUSED(dummy)) if (HASTZINFO(self) && self->tzinfo != Py_None) { PyObject *current_mod = NULL; datetime_state *st = GET_CURRENT_STATE(current_mod); + if (st == NULL) { + assert(current_mod == NULL); + return NULL; + } PyObject *delta; delta = datetime_subtract(op, CONST_EPOCH(st)); From c88df5177a9dbae6770802be94c49a4c733b5b58 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 7 Jun 2026 18:35:48 +0300 Subject: [PATCH 2/4] Add one more test case --- Lib/test/datetimetester.py | 25 +++++++++++++++++++++++++ Modules/_datetimemodule.c | 1 + 2 files changed, 26 insertions(+) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 64189d185b4310..300b9fe4e665ce 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -7531,6 +7531,31 @@ def test_gh_151039(self): """ rc, out, err = script_helper.assert_python_ok("-c", script) self.assertEqual(rc, 0) + self.assertEqual(out, b'') + self.assertEqual(err, b'') + + @support.cpython_only + def test_static_type_at_shutdown(self): + # gh-132413 + script = textwrap.dedent(""" + import _datetime + timedelta = _datetime.timedelta + + def gen(): + try: + yield + finally: + # sys.modules is empty + _datetime.timedelta(days=1) + timedelta(days=1) + + it = gen() + next(it) + """) + rc, out, err = script_helper.assert_python_ok("-c", script) + self.assertEqual(rc, 0) + self.assertEqual(out, b'') + self.assertEqual(err, b'') def load_tests(loader, standard_tests, pattern): diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 15e508f2360bcb..7a39439bde1055 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -3032,6 +3032,7 @@ delta_total_seconds(PyObject *op, PyObject *Py_UNUSED(dummy)) datetime_state *st = GET_CURRENT_STATE(current_mod); if (st == NULL) { assert(current_mod == NULL); + Py_DECREF(total_microseconds); return NULL; } From 725f159d311f55d6e7dcaa8914397a4e295d0ca5 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 7 Jun 2026 21:35:30 +0300 Subject: [PATCH 3/4] Update Misc/NEWS.d/next/Library/2026-06-07-17-29-33.gh-issue-151039.AZ0qBn.rst Co-authored-by: Stan Ulbrych --- .../next/Library/2026-06-07-17-29-33.gh-issue-151039.AZ0qBn.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-06-07-17-29-33.gh-issue-151039.AZ0qBn.rst b/Misc/NEWS.d/next/Library/2026-06-07-17-29-33.gh-issue-151039.AZ0qBn.rst index c8facd46174e5e..1e99567f555057 100644 --- a/Misc/NEWS.d/next/Library/2026-06-07-17-29-33.gh-issue-151039.AZ0qBn.rst +++ b/Misc/NEWS.d/next/Library/2026-06-07-17-29-33.gh-issue-151039.AZ0qBn.rst @@ -1 +1 @@ -Fix a crash when :class:`datetime.timezone` outlives ``_datetime`` module. +Fix a crash when static :mod:`datetime` types outlive the ``_datetime`` module. From 0b55c3c56b210048e827c35e088e05665a12746e Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 7 Jun 2026 22:38:01 +0300 Subject: [PATCH 4/4] More tests --- Lib/test/datetimetester.py | 37 +++++++++---------------------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 300b9fe4e665ce..966ef10114faf6 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -7510,53 +7510,34 @@ def func(): self.assertEqual(err, b"") @support.cpython_only - def test_gh_151039(self): + @support.subTests(("setup", "call"), [ + ("obj = _datetime.timedelta", "obj(seconds=2)"), + ("obj = _datetime.date(2026, 6, 7)", "obj.isocalendar()"), + ]) + def test_static_datetime_types_outlive_collected_module(self, setup, call): # gh-151039: This code used to crash - script = """if True: + script = f"""if True: import sys, gc import _datetime - td = _datetime.timedelta # static C type, survives the module + {setup} # static C type, survives the module del sys.modules['_datetime'] del _datetime sys.modules['_datetime'] = None # block re-import gc.collect() # module object is collected try: - td(seconds=2) # used to be a segmentation fault + {call} # used to be a segmentation fault except ImportError: pass else: - assert False, "ImportError not raised" + raise AssertionError("ImportError not raised") """ rc, out, err = script_helper.assert_python_ok("-c", script) self.assertEqual(rc, 0) self.assertEqual(out, b'') self.assertEqual(err, b'') - @support.cpython_only - def test_static_type_at_shutdown(self): - # gh-132413 - script = textwrap.dedent(""" - import _datetime - timedelta = _datetime.timedelta - - def gen(): - try: - yield - finally: - # sys.modules is empty - _datetime.timedelta(days=1) - timedelta(days=1) - - it = gen() - next(it) - """) - rc, out, err = script_helper.assert_python_ok("-c", script) - self.assertEqual(rc, 0) - self.assertEqual(out, b'') - self.assertEqual(err, b'') - def load_tests(loader, standard_tests, pattern): standard_tests.addTest(ZoneInfoCompleteTest())