What happened?
NULL pointer dereference in the _datetime C module → SIGSEGV. Reproducer:
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 td(seconds=2) # -> Segmentation fault
gdb at the crash: st == (datetime_state *) 0x0, with an ImportError already pending.
Root cause
The datetime types are static and can outlive the module, so per-module state (gh-117398) is fetched via _get_current_state(), which can return NULL when the module weakref is dead and re-import fails (blocked in sys.modules):
| static datetime_state * | |
| _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; | |
| } | |
| /* The static types can outlive the module, | |
| * so we must re-import the module. */ | |
| mod = PyImport_ImportModule("_datetime"); | |
| if (mod == NULL) { | |
| return NULL; | |
| } | |
| } | |
| datetime_state *st = get_module_state(mod); | |
| *p_mod = mod; | |
| return st; | |
| } |
Callers dereference the returned state without a NULL check. At the crash site CONST_US_PER_SECOND(st) is st->us_per_second, so st == NULL → NULL deref:
| datetime_state *st = GET_CURRENT_STATE(current_mod); | |
| PyObject *x = NULL; /* running sum of microseconds */ | |
| PyObject *y = NULL; /* temp sum of microseconds */ | |
| double leftover_us = 0.0; | |
| x = PyLong_FromLong(0); | |
| if (x == NULL) | |
| goto Done; | |
| #define CLEANUP \ | |
| Py_DECREF(x); \ | |
| x = y; \ | |
| if (x == NULL) \ | |
| goto Done | |
| if (microseconds) { | |
| y = accum("microseconds", x, microseconds, _PyLong_GetOne(), &leftover_us); | |
| CLEANUP; | |
| } | |
| if (milliseconds) { | |
| y = accum("milliseconds", x, milliseconds, CONST_US_PER_MS(st), &leftover_us); | |
| CLEANUP; | |
| } | |
| if (seconds) { | |
| y = accum("seconds", x, seconds, CONST_US_PER_SECOND(st), &leftover_us); |
Originally hit via ./python -X lazy_imports=all -m test test_datetime (segfault in test_strptime; test_datetime is also listed in #149640).
Versions
- CPython main (3.16.0a0,
ab930175e7e). Present since 3.14 (Isolate the _datetime extension module #117398) → 3.14, 3.15, main. Linux. - Build:
CC=/opt/llvm/bin/clang CXX=/opt/llvm/bin/clang++ LDFLAGS='-fuse-ld=lld' ./configure; clang/lld 23.0.0git (4695c84).