Skip to content

Commit a109606

Browse files
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.
1 parent 80b2d60 commit a109606

File tree

8 files changed

+111
-95
lines changed

8 files changed

+111
-95
lines changed

Include/internal/pycore_pylifecycle.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ extern PyStatus _Py_HashRandomization_Init(const PyConfig *);
4141

4242
extern PyStatus _PyGC_Init(PyInterpreterState *interp);
4343
extern PyStatus _PyAtExit_Init(PyInterpreterState *interp);
44+
extern PyStatus _PyDateTime_InitTypes(PyInterpreterState *interp);
4445

4546
/* Various internal finalizers */
4647

Lib/test/datetimetester.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7295,6 +7295,34 @@ def test_update_type_cache(self):
72957295
""")
72967296
script_helper.assert_python_ok('-c', script)
72977297

7298+
def test_concurrent_initialization_subinterpreter(self):
7299+
# gh-136421: Concurrent initialization of _datetime across multiple
7300+
# interpreters wasn't thread-safe due to its static types.
7301+
7302+
# Run in a subprocess to ensure we get a clean version of _datetime
7303+
script = """if True:
7304+
from concurrent.futures import InterpreterPoolExecutor
7305+
7306+
def func():
7307+
import _datetime
7308+
print('a', end='')
7309+
7310+
with InterpreterPoolExecutor() as executor:
7311+
for _ in range(8):
7312+
executor.submit(func)
7313+
"""
7314+
rc, out, err = script_helper.assert_python_ok("-c", script)
7315+
self.assertEqual(rc, 0)
7316+
self.assertEqual(out, b"a" * 8)
7317+
self.assertEqual(err, b"")
7318+
7319+
# Now test against concurrent reinitialization
7320+
script = "import _datetime\n" + script
7321+
rc, out, err = script_helper.assert_python_ok("-c", script)
7322+
self.assertEqual(rc, 0)
7323+
self.assertEqual(out, b"a" * 8)
7324+
self.assertEqual(err, b"")
7325+
72987326

72997327
def load_tests(loader, standard_tests, pattern):
73007328
standard_tests.addTest(ZoneInfoCompleteTest())
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix crash when initializing :mod:`datetime` concurrently.

Modules/Setup.bootstrap.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ posix posixmodule.c
1212
_signal signalmodule.c
1313
_tracemalloc _tracemalloc.c
1414
_suggestions _suggestions.c
15+
# needs libm and on some platforms librt
16+
_datetime _datetimemodule.c
1517

1618
# modules used by importlib, deepfreeze, freeze, runpy, and sysconfig
1719
_codecs _codecsmodule.c

Modules/Setup.stdlib.in

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,6 @@
5555
@MODULE_CMATH_TRUE@cmath cmathmodule.c
5656
@MODULE__STATISTICS_TRUE@_statistics _statisticsmodule.c
5757

58-
# needs libm and on some platforms librt
59-
@MODULE__DATETIME_TRUE@_datetime _datetimemodule.c
60-
6158
# _decimal uses libmpdec
6259
# either static libmpdec.a from Modules/_decimal/libmpdec or libmpdec.so
6360
# with ./configure --with-system-libmpdec

Modules/_datetimemodule.c

Lines changed: 73 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include "pycore_object.h" // _PyObject_Init()
1515
#include "pycore_time.h" // _PyTime_ObjectToTime_t()
1616
#include "pycore_unicodeobject.h" // _PyUnicode_Copy()
17+
#include "pycore_initconfig.h" // _PyStatus_OK()
1718

1819
#include "datetime.h"
1920

@@ -124,10 +125,9 @@ get_module_state(PyObject *module)
124125
#define INTERP_KEY ((PyObject *)&_Py_ID(cached_datetime_module))
125126

126127
static PyObject *
127-
get_current_module(PyInterpreterState *interp, int *p_reloading)
128+
get_current_module(PyInterpreterState *interp)
128129
{
129130
PyObject *mod = NULL;
130-
int reloading = 0;
131131

132132
PyObject *dict = PyInterpreterState_GetDict(interp);
133133
if (dict == NULL) {
@@ -138,7 +138,6 @@ get_current_module(PyInterpreterState *interp, int *p_reloading)
138138
goto error;
139139
}
140140
if (ref != NULL) {
141-
reloading = 1;
142141
if (ref != Py_None) {
143142
(void)PyWeakref_GetRef(ref, &mod);
144143
if (mod == Py_None) {
@@ -147,9 +146,6 @@ get_current_module(PyInterpreterState *interp, int *p_reloading)
147146
Py_DECREF(ref);
148147
}
149148
}
150-
if (p_reloading != NULL) {
151-
*p_reloading = reloading;
152-
}
153149
return mod;
154150

155151
error:
@@ -163,7 +159,7 @@ static datetime_state *
163159
_get_current_state(PyObject **p_mod)
164160
{
165161
PyInterpreterState *interp = PyInterpreterState_Get();
166-
PyObject *mod = get_current_module(interp, NULL);
162+
PyObject *mod = get_current_module(interp);
167163
if (mod == NULL) {
168164
assert(!PyErr_Occurred());
169165
if (PyErr_Occurred()) {
@@ -4482,7 +4478,7 @@ static PyTypeObject PyDateTime_TimeZoneType = {
44824478
timezone_methods, /* tp_methods */
44834479
0, /* tp_members */
44844480
0, /* tp_getset */
4485-
0, /* tp_base; filled in PyInit__datetime */
4481+
&PyDateTime_TZInfoType, /* tp_base */
44864482
0, /* tp_dict */
44874483
0, /* tp_descr_get */
44884484
0, /* tp_descr_set */
@@ -7147,8 +7143,7 @@ static PyTypeObject PyDateTime_DateTimeType = {
71477143
datetime_methods, /* tp_methods */
71487144
0, /* tp_members */
71497145
datetime_getset, /* tp_getset */
7150-
0, /* tp_base; filled in
7151-
PyInit__datetime */
7146+
&PyDateTime_DateType, /* tp_base */
71527147
0, /* tp_dict */
71537148
0, /* tp_descr_get */
71547149
0, /* tp_descr_set */
@@ -7329,29 +7324,82 @@ clear_state(datetime_state *st)
73297324
}
73307325

73317326

7332-
static int
7333-
init_static_types(PyInterpreterState *interp, int reloading)
7327+
PyStatus
7328+
_PyDateTime_InitTypes(PyInterpreterState *interp)
73347329
{
7335-
if (reloading) {
7336-
return 0;
7337-
}
7338-
7339-
// `&...` is not a constant expression according to a strict reading
7340-
// of C standards. Fill tp_base at run-time rather than statically.
7341-
// See https://bugs.python.org/issue40777
7342-
PyDateTime_TimeZoneType.tp_base = &PyDateTime_TZInfoType;
7343-
PyDateTime_DateTimeType.tp_base = &PyDateTime_DateType;
7344-
73457330
/* Bases classes must be initialized before subclasses,
73467331
* so capi_types must have the types in the appropriate order. */
73477332
for (size_t i = 0; i < Py_ARRAY_LENGTH(capi_types); i++) {
73487333
PyTypeObject *type = capi_types[i];
73497334
if (_PyStaticType_InitForExtension(interp, type) < 0) {
7350-
return -1;
7335+
return _PyStatus_ERR("could not initialize static types");
73517336
}
73527337
}
73537338

7354-
return 0;
7339+
#define DATETIME_ADD_MACRO(dict, c, value_expr) \
7340+
do { \
7341+
assert(!PyErr_Occurred()); \
7342+
PyObject *value = (value_expr); \
7343+
if (value == NULL) { \
7344+
goto error; \
7345+
} \
7346+
if (PyDict_SetItemString(dict, c, value) < 0) { \
7347+
Py_DECREF(value); \
7348+
goto error; \
7349+
} \
7350+
Py_DECREF(value); \
7351+
} while(0)
7352+
7353+
/* timedelta values */
7354+
PyObject *d = _PyType_GetDict(&PyDateTime_DeltaType);
7355+
DATETIME_ADD_MACRO(d, "resolution", new_delta(0, 0, 1, 0));
7356+
DATETIME_ADD_MACRO(d, "min", new_delta(-MAX_DELTA_DAYS, 0, 0, 0));
7357+
DATETIME_ADD_MACRO(d, "max",
7358+
new_delta(MAX_DELTA_DAYS, 24*3600-1, 1000000-1, 0));
7359+
7360+
/* date values */
7361+
d = _PyType_GetDict(&PyDateTime_DateType);
7362+
DATETIME_ADD_MACRO(d, "min", new_date(1, 1, 1));
7363+
DATETIME_ADD_MACRO(d, "max", new_date(MAXYEAR, 12, 31));
7364+
DATETIME_ADD_MACRO(d, "resolution", new_delta(1, 0, 0, 0));
7365+
7366+
/* time values */
7367+
d = _PyType_GetDict(&PyDateTime_TimeType);
7368+
DATETIME_ADD_MACRO(d, "min", new_time(0, 0, 0, 0, Py_None, 0));
7369+
DATETIME_ADD_MACRO(d, "max", new_time(23, 59, 59, 999999, Py_None, 0));
7370+
DATETIME_ADD_MACRO(d, "resolution", new_delta(0, 0, 1, 0));
7371+
7372+
/* datetime values */
7373+
d = _PyType_GetDict(&PyDateTime_DateTimeType);
7374+
DATETIME_ADD_MACRO(d, "min",
7375+
new_datetime(1, 1, 1, 0, 0, 0, 0, Py_None, 0));
7376+
DATETIME_ADD_MACRO(d, "max", new_datetime(MAXYEAR, 12, 31, 23, 59, 59,
7377+
999999, Py_None, 0));
7378+
DATETIME_ADD_MACRO(d, "resolution", new_delta(0, 0, 1, 0));
7379+
7380+
/* timezone values */
7381+
d = _PyType_GetDict(&PyDateTime_TimeZoneType);
7382+
if (PyDict_SetItemString(d, "utc", (PyObject *)&utc_timezone) < 0) {
7383+
goto error;
7384+
}
7385+
7386+
/* bpo-37642: These attributes are rounded to the nearest minute for backwards
7387+
* compatibility, even though the constructor will accept a wider range of
7388+
* values. This may change in the future.*/
7389+
7390+
/* -23:59 */
7391+
DATETIME_ADD_MACRO(d, "min", create_timezone_from_delta(-1, 60, 0, 1));
7392+
7393+
/* +23:59 */
7394+
DATETIME_ADD_MACRO(
7395+
d, "max", create_timezone_from_delta(0, (23 * 60 + 59) * 60, 0, 0));
7396+
7397+
#undef DATETIME_ADD_MACRO
7398+
7399+
return _PyStatus_OK();
7400+
7401+
error:
7402+
return _PyStatus_NO_MEMORY();
73557403
}
73567404

73577405

@@ -7369,20 +7417,15 @@ _datetime_exec(PyObject *module)
73697417
{
73707418
int rc = -1;
73717419
datetime_state *st = get_module_state(module);
7372-
int reloading = 0;
73737420

73747421
PyInterpreterState *interp = PyInterpreterState_Get();
7375-
PyObject *old_module = get_current_module(interp, &reloading);
7422+
PyObject *old_module = get_current_module(interp);
73767423
if (PyErr_Occurred()) {
73777424
assert(old_module == NULL);
73787425
goto error;
73797426
}
73807427
/* We actually set the "current" module right before a successful return. */
73817428

7382-
if (init_static_types(interp, reloading) < 0) {
7383-
goto error;
7384-
}
7385-
73867429
for (size_t i = 0; i < Py_ARRAY_LENGTH(capi_types); i++) {
73877430
PyTypeObject *type = capi_types[i];
73887431
const char *name = _PyType_Name(type);
@@ -7396,68 +7439,6 @@ _datetime_exec(PyObject *module)
73967439
goto error;
73977440
}
73987441

7399-
#define DATETIME_ADD_MACRO(dict, c, value_expr) \
7400-
do { \
7401-
assert(!PyErr_Occurred()); \
7402-
PyObject *value = (value_expr); \
7403-
if (value == NULL) { \
7404-
goto error; \
7405-
} \
7406-
if (PyDict_SetItemString(dict, c, value) < 0) { \
7407-
Py_DECREF(value); \
7408-
goto error; \
7409-
} \
7410-
Py_DECREF(value); \
7411-
} while(0)
7412-
7413-
if (!reloading) {
7414-
/* timedelta values */
7415-
PyObject *d = _PyType_GetDict(&PyDateTime_DeltaType);
7416-
DATETIME_ADD_MACRO(d, "resolution", new_delta(0, 0, 1, 0));
7417-
DATETIME_ADD_MACRO(d, "min", new_delta(-MAX_DELTA_DAYS, 0, 0, 0));
7418-
DATETIME_ADD_MACRO(d, "max",
7419-
new_delta(MAX_DELTA_DAYS, 24*3600-1, 1000000-1, 0));
7420-
7421-
/* date values */
7422-
d = _PyType_GetDict(&PyDateTime_DateType);
7423-
DATETIME_ADD_MACRO(d, "min", new_date(1, 1, 1));
7424-
DATETIME_ADD_MACRO(d, "max", new_date(MAXYEAR, 12, 31));
7425-
DATETIME_ADD_MACRO(d, "resolution", new_delta(1, 0, 0, 0));
7426-
7427-
/* time values */
7428-
d = _PyType_GetDict(&PyDateTime_TimeType);
7429-
DATETIME_ADD_MACRO(d, "min", new_time(0, 0, 0, 0, Py_None, 0));
7430-
DATETIME_ADD_MACRO(d, "max", new_time(23, 59, 59, 999999, Py_None, 0));
7431-
DATETIME_ADD_MACRO(d, "resolution", new_delta(0, 0, 1, 0));
7432-
7433-
/* datetime values */
7434-
d = _PyType_GetDict(&PyDateTime_DateTimeType);
7435-
DATETIME_ADD_MACRO(d, "min",
7436-
new_datetime(1, 1, 1, 0, 0, 0, 0, Py_None, 0));
7437-
DATETIME_ADD_MACRO(d, "max", new_datetime(MAXYEAR, 12, 31, 23, 59, 59,
7438-
999999, Py_None, 0));
7439-
DATETIME_ADD_MACRO(d, "resolution", new_delta(0, 0, 1, 0));
7440-
7441-
/* timezone values */
7442-
d = _PyType_GetDict(&PyDateTime_TimeZoneType);
7443-
if (PyDict_SetItemString(d, "utc", (PyObject *)&utc_timezone) < 0) {
7444-
goto error;
7445-
}
7446-
7447-
/* bpo-37642: These attributes are rounded to the nearest minute for backwards
7448-
* compatibility, even though the constructor will accept a wider range of
7449-
* values. This may change in the future.*/
7450-
7451-
/* -23:59 */
7452-
DATETIME_ADD_MACRO(d, "min", create_timezone_from_delta(-1, 60, 0, 1));
7453-
7454-
/* +23:59 */
7455-
DATETIME_ADD_MACRO(
7456-
d, "max", create_timezone_from_delta(0, (23 * 60 + 59) * 60, 0, 0));
7457-
}
7458-
7459-
#undef DATETIME_ADD_MACRO
7460-
74617442
/* Add module level attributes */
74627443
if (PyModule_AddIntMacro(module, MINYEAR) < 0) {
74637444
goto error;

PCbuild/_freeze_module.vcxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
</ItemGroup>
107107
<ItemGroup>
108108
<ClCompile Include="..\Modules\atexitmodule.c" />
109+
<ClCompile Include="..\Modules\_datetimemodule.c" />
109110
<ClCompile Include="..\Modules\faulthandler.c" />
110111
<ClCompile Include="..\Modules\gcmodule.c" />
111112
<ClCompile Include="..\Modules\getbuildinfo.c" />

Python/pylifecycle.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,11 @@ pycore_init_types(PyInterpreterState *interp)
760760
return status;
761761
}
762762

763+
status = _PyDateTime_InitTypes(interp);
764+
if (_PyStatus_EXCEPTION(status)) {
765+
return status;
766+
}
767+
763768
return _PyStatus_OK();
764769
}
765770

0 commit comments

Comments
 (0)