https://github.com/python/cpython/commit/a10960699a2b3e4e62896331c4f9cfd162ebf440
commit: a10960699a2b3e4e62896331c4f9cfd162ebf440
branch: main
author: Peter Bierma <[email protected]>
committer: ZeroIntensity <[email protected]>
date: 2025-07-21T13:47:26-04:00
summary:
gh-136421: Load `_datetime` static types during interpreter initialization
(GH-136583)
`_datetime` is a special module, because it's the only non-builtin C extension
that contains static types. As such, it would initialize static types in the
module's execution function, which can run concurrently. Since static type
initialization is not thread-safe, this caused crashes. This fixes it by moving
the initialization of `_datetime`'s static types to interpreter startup (where
all other static types are initialized), which is already properly protected
through other locks.
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2025-07-12-09-59-14.gh-issue-136421.ZD1rNj.rst
M Include/internal/pycore_pylifecycle.h
M Lib/test/datetimetester.py
M Modules/Setup.bootstrap.in
M Modules/Setup.stdlib.in
M Modules/_datetimemodule.c
M PCbuild/_freeze_module.vcxproj
M Python/pylifecycle.c
diff --git a/Include/internal/pycore_pylifecycle.h
b/Include/internal/pycore_pylifecycle.h
index 6e89ca33e4208c..8faf7a4d403f84 100644
--- a/Include/internal/pycore_pylifecycle.h
+++ b/Include/internal/pycore_pylifecycle.h
@@ -41,6 +41,7 @@ extern PyStatus _Py_HashRandomization_Init(const PyConfig *);
extern PyStatus _PyGC_Init(PyInterpreterState *interp);
extern PyStatus _PyAtExit_Init(PyInterpreterState *interp);
+extern PyStatus _PyDateTime_InitTypes(PyInterpreterState *interp);
/* Various internal finalizers */
diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py
index 93b3382b9c654e..3bd3a866570042 100644
--- a/Lib/test/datetimetester.py
+++ b/Lib/test/datetimetester.py
@@ -7295,6 +7295,34 @@ def test_update_type_cache(self):
""")
script_helper.assert_python_ok('-c', script)
+ def test_concurrent_initialization_subinterpreter(self):
+ # gh-136421: Concurrent initialization of _datetime across multiple
+ # interpreters wasn't thread-safe due to its static types.
+
+ # Run in a subprocess to ensure we get a clean version of _datetime
+ script = """if True:
+ from concurrent.futures import InterpreterPoolExecutor
+
+ def func():
+ import _datetime
+ print('a', end='')
+
+ with InterpreterPoolExecutor() as executor:
+ for _ in range(8):
+ executor.submit(func)
+ """
+ rc, out, err = script_helper.assert_python_ok("-c", script)
+ self.assertEqual(rc, 0)
+ self.assertEqual(out, b"a" * 8)
+ self.assertEqual(err, b"")
+
+ # Now test against concurrent reinitialization
+ script = "import _datetime\n" + script
+ rc, out, err = script_helper.assert_python_ok("-c", script)
+ self.assertEqual(rc, 0)
+ self.assertEqual(out, b"a" * 8)
+ self.assertEqual(err, b"")
+
def load_tests(loader, standard_tests, pattern):
standard_tests.addTest(ZoneInfoCompleteTest())
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2025-07-12-09-59-14.gh-issue-136421.ZD1rNj.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2025-07-12-09-59-14.gh-issue-136421.ZD1rNj.rst
new file mode 100644
index 00000000000000..dcc73267a78546
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2025-07-12-09-59-14.gh-issue-136421.ZD1rNj.rst
@@ -0,0 +1 @@
+Fix crash when initializing :mod:`datetime` concurrently.
diff --git a/Modules/Setup.bootstrap.in b/Modules/Setup.bootstrap.in
index 2b2e8cb3e3cacd..65a1fefe72e92e 100644
--- a/Modules/Setup.bootstrap.in
+++ b/Modules/Setup.bootstrap.in
@@ -12,6 +12,8 @@ posix posixmodule.c
_signal signalmodule.c
_tracemalloc _tracemalloc.c
_suggestions _suggestions.c
+# needs libm and on some platforms librt
+_datetime _datetimemodule.c
# modules used by importlib, deepfreeze, freeze, runpy, and sysconfig
_codecs _codecsmodule.c
diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in
index 86c8eb27c0a6c7..7f4c4a806737ac 100644
--- a/Modules/Setup.stdlib.in
+++ b/Modules/Setup.stdlib.in
@@ -55,9 +55,6 @@
@MODULE_CMATH_TRUE@cmath cmathmodule.c
@MODULE__STATISTICS_TRUE@_statistics _statisticsmodule.c
-# needs libm and on some platforms librt
-@MODULE__DATETIME_TRUE@_datetime _datetimemodule.c
-
# _decimal uses libmpdec
# either static libmpdec.a from Modules/_decimal/libmpdec or libmpdec.so
# with ./configure --with-system-libmpdec
diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c
index 7a6426593d021f..01039dfeec0719 100644
--- a/Modules/_datetimemodule.c
+++ b/Modules/_datetimemodule.c
@@ -14,6 +14,7 @@
#include "pycore_object.h" // _PyObject_Init()
#include "pycore_time.h" // _PyTime_ObjectToTime_t()
#include "pycore_unicodeobject.h" // _PyUnicode_Copy()
+#include "pycore_initconfig.h" // _PyStatus_OK()
#include "datetime.h"
@@ -124,10 +125,9 @@ get_module_state(PyObject *module)
#define INTERP_KEY ((PyObject *)&_Py_ID(cached_datetime_module))
static PyObject *
-get_current_module(PyInterpreterState *interp, int *p_reloading)
+get_current_module(PyInterpreterState *interp)
{
PyObject *mod = NULL;
- int reloading = 0;
PyObject *dict = PyInterpreterState_GetDict(interp);
if (dict == NULL) {
@@ -138,7 +138,6 @@ get_current_module(PyInterpreterState *interp, int
*p_reloading)
goto error;
}
if (ref != NULL) {
- reloading = 1;
if (ref != Py_None) {
(void)PyWeakref_GetRef(ref, &mod);
if (mod == Py_None) {
@@ -147,9 +146,6 @@ get_current_module(PyInterpreterState *interp, int
*p_reloading)
Py_DECREF(ref);
}
}
- if (p_reloading != NULL) {
- *p_reloading = reloading;
- }
return mod;
error:
@@ -163,7 +159,7 @@ static datetime_state *
_get_current_state(PyObject **p_mod)
{
PyInterpreterState *interp = PyInterpreterState_Get();
- PyObject *mod = get_current_module(interp, NULL);
+ PyObject *mod = get_current_module(interp);
if (mod == NULL) {
assert(!PyErr_Occurred());
if (PyErr_Occurred()) {
@@ -4482,7 +4478,7 @@ static PyTypeObject PyDateTime_TimeZoneType = {
timezone_methods, /* tp_methods */
0, /* tp_members */
0, /* tp_getset */
- 0, /* tp_base; filled in PyInit__datetime */
+ &PyDateTime_TZInfoType, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
@@ -7147,8 +7143,7 @@ static PyTypeObject PyDateTime_DateTimeType = {
datetime_methods, /* tp_methods */
0, /* tp_members */
datetime_getset, /* tp_getset */
- 0, /* tp_base; filled in
- PyInit__datetime */
+ &PyDateTime_DateType, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
@@ -7329,29 +7324,82 @@ clear_state(datetime_state *st)
}
-static int
-init_static_types(PyInterpreterState *interp, int reloading)
+PyStatus
+_PyDateTime_InitTypes(PyInterpreterState *interp)
{
- if (reloading) {
- return 0;
- }
-
- // `&...` is not a constant expression according to a strict reading
- // of C standards. Fill tp_base at run-time rather than statically.
- // See https://bugs.python.org/issue40777
- PyDateTime_TimeZoneType.tp_base = &PyDateTime_TZInfoType;
- PyDateTime_DateTimeType.tp_base = &PyDateTime_DateType;
-
/* Bases classes must be initialized before subclasses,
* so capi_types must have the types in the appropriate order. */
for (size_t i = 0; i < Py_ARRAY_LENGTH(capi_types); i++) {
PyTypeObject *type = capi_types[i];
if (_PyStaticType_InitForExtension(interp, type) < 0) {
- return -1;
+ return _PyStatus_ERR("could not initialize static types");
}
}
- return 0;
+#define DATETIME_ADD_MACRO(dict, c, value_expr) \
+ do { \
+ assert(!PyErr_Occurred()); \
+ PyObject *value = (value_expr); \
+ if (value == NULL) { \
+ goto error; \
+ } \
+ if (PyDict_SetItemString(dict, c, value) < 0) { \
+ Py_DECREF(value); \
+ goto error; \
+ } \
+ Py_DECREF(value); \
+ } while(0)
+
+ /* timedelta values */
+ PyObject *d = _PyType_GetDict(&PyDateTime_DeltaType);
+ DATETIME_ADD_MACRO(d, "resolution", new_delta(0, 0, 1, 0));
+ DATETIME_ADD_MACRO(d, "min", new_delta(-MAX_DELTA_DAYS, 0, 0, 0));
+ DATETIME_ADD_MACRO(d, "max",
+ new_delta(MAX_DELTA_DAYS, 24*3600-1, 1000000-1, 0));
+
+ /* date values */
+ d = _PyType_GetDict(&PyDateTime_DateType);
+ DATETIME_ADD_MACRO(d, "min", new_date(1, 1, 1));
+ DATETIME_ADD_MACRO(d, "max", new_date(MAXYEAR, 12, 31));
+ DATETIME_ADD_MACRO(d, "resolution", new_delta(1, 0, 0, 0));
+
+ /* time values */
+ d = _PyType_GetDict(&PyDateTime_TimeType);
+ DATETIME_ADD_MACRO(d, "min", new_time(0, 0, 0, 0, Py_None, 0));
+ DATETIME_ADD_MACRO(d, "max", new_time(23, 59, 59, 999999, Py_None, 0));
+ DATETIME_ADD_MACRO(d, "resolution", new_delta(0, 0, 1, 0));
+
+ /* datetime values */
+ d = _PyType_GetDict(&PyDateTime_DateTimeType);
+ DATETIME_ADD_MACRO(d, "min",
+ new_datetime(1, 1, 1, 0, 0, 0, 0, Py_None, 0));
+ DATETIME_ADD_MACRO(d, "max", new_datetime(MAXYEAR, 12, 31, 23, 59, 59,
+ 999999, Py_None, 0));
+ DATETIME_ADD_MACRO(d, "resolution", new_delta(0, 0, 1, 0));
+
+ /* timezone values */
+ d = _PyType_GetDict(&PyDateTime_TimeZoneType);
+ if (PyDict_SetItemString(d, "utc", (PyObject *)&utc_timezone) < 0) {
+ goto error;
+ }
+
+ /* bpo-37642: These attributes are rounded to the nearest minute for
backwards
+ * compatibility, even though the constructor will accept a wider range of
+ * values. This may change in the future.*/
+
+ /* -23:59 */
+ DATETIME_ADD_MACRO(d, "min", create_timezone_from_delta(-1, 60, 0, 1));
+
+ /* +23:59 */
+ DATETIME_ADD_MACRO(
+ d, "max", create_timezone_from_delta(0, (23 * 60 + 59) * 60, 0,
0));
+
+#undef DATETIME_ADD_MACRO
+
+ return _PyStatus_OK();
+
+error:
+ return _PyStatus_NO_MEMORY();
}
@@ -7369,20 +7417,15 @@ _datetime_exec(PyObject *module)
{
int rc = -1;
datetime_state *st = get_module_state(module);
- int reloading = 0;
PyInterpreterState *interp = PyInterpreterState_Get();
- PyObject *old_module = get_current_module(interp, &reloading);
+ PyObject *old_module = get_current_module(interp);
if (PyErr_Occurred()) {
assert(old_module == NULL);
goto error;
}
/* We actually set the "current" module right before a successful return.
*/
- if (init_static_types(interp, reloading) < 0) {
- goto error;
- }
-
for (size_t i = 0; i < Py_ARRAY_LENGTH(capi_types); i++) {
PyTypeObject *type = capi_types[i];
const char *name = _PyType_Name(type);
@@ -7396,68 +7439,6 @@ _datetime_exec(PyObject *module)
goto error;
}
-#define DATETIME_ADD_MACRO(dict, c, value_expr) \
- do { \
- assert(!PyErr_Occurred()); \
- PyObject *value = (value_expr); \
- if (value == NULL) { \
- goto error; \
- } \
- if (PyDict_SetItemString(dict, c, value) < 0) { \
- Py_DECREF(value); \
- goto error; \
- } \
- Py_DECREF(value); \
- } while(0)
-
- if (!reloading) {
- /* timedelta values */
- PyObject *d = _PyType_GetDict(&PyDateTime_DeltaType);
- DATETIME_ADD_MACRO(d, "resolution", new_delta(0, 0, 1, 0));
- DATETIME_ADD_MACRO(d, "min", new_delta(-MAX_DELTA_DAYS, 0, 0, 0));
- DATETIME_ADD_MACRO(d, "max",
- new_delta(MAX_DELTA_DAYS, 24*3600-1, 1000000-1, 0));
-
- /* date values */
- d = _PyType_GetDict(&PyDateTime_DateType);
- DATETIME_ADD_MACRO(d, "min", new_date(1, 1, 1));
- DATETIME_ADD_MACRO(d, "max", new_date(MAXYEAR, 12, 31));
- DATETIME_ADD_MACRO(d, "resolution", new_delta(1, 0, 0, 0));
-
- /* time values */
- d = _PyType_GetDict(&PyDateTime_TimeType);
- DATETIME_ADD_MACRO(d, "min", new_time(0, 0, 0, 0, Py_None, 0));
- DATETIME_ADD_MACRO(d, "max", new_time(23, 59, 59, 999999, Py_None, 0));
- DATETIME_ADD_MACRO(d, "resolution", new_delta(0, 0, 1, 0));
-
- /* datetime values */
- d = _PyType_GetDict(&PyDateTime_DateTimeType);
- DATETIME_ADD_MACRO(d, "min",
- new_datetime(1, 1, 1, 0, 0, 0, 0, Py_None, 0));
- DATETIME_ADD_MACRO(d, "max", new_datetime(MAXYEAR, 12, 31, 23, 59, 59,
- 999999, Py_None, 0));
- DATETIME_ADD_MACRO(d, "resolution", new_delta(0, 0, 1, 0));
-
- /* timezone values */
- d = _PyType_GetDict(&PyDateTime_TimeZoneType);
- if (PyDict_SetItemString(d, "utc", (PyObject *)&utc_timezone) < 0) {
- goto error;
- }
-
- /* bpo-37642: These attributes are rounded to the nearest minute for
backwards
- * compatibility, even though the constructor will accept a wider range
of
- * values. This may change in the future.*/
-
- /* -23:59 */
- DATETIME_ADD_MACRO(d, "min", create_timezone_from_delta(-1, 60, 0, 1));
-
- /* +23:59 */
- DATETIME_ADD_MACRO(
- d, "max", create_timezone_from_delta(0, (23 * 60 + 59) * 60,
0, 0));
- }
-
-#undef DATETIME_ADD_MACRO
-
/* Add module level attributes */
if (PyModule_AddIntMacro(module, MINYEAR) < 0) {
goto error;
diff --git a/PCbuild/_freeze_module.vcxproj b/PCbuild/_freeze_module.vcxproj
index efff6a58d895cb..5ceddf759b8f3b 100644
--- a/PCbuild/_freeze_module.vcxproj
+++ b/PCbuild/_freeze_module.vcxproj
@@ -106,6 +106,7 @@
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\Modules\atexitmodule.c" />
+ <ClCompile Include="..\Modules\_datetimemodule.c" />
<ClCompile Include="..\Modules\faulthandler.c" />
<ClCompile Include="..\Modules\gcmodule.c" />
<ClCompile Include="..\Modules\getbuildinfo.c" />
diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c
index 00e8d030765560..e22a9cc1c75050 100644
--- a/Python/pylifecycle.c
+++ b/Python/pylifecycle.c
@@ -760,6 +760,11 @@ pycore_init_types(PyInterpreterState *interp)
return status;
}
+ status = _PyDateTime_InitTypes(interp);
+ if (_PyStatus_EXCEPTION(status)) {
+ return status;
+ }
+
return _PyStatus_OK();
}
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]