Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-librt for openSUSE:Factory checked in at 2026-10-06 19:13:22 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-librt (Old) and /work/SRC/openSUSE:Factory/.python-librt.new.3698736 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-librt" Tue Oct 6 19:13:22 2026 rev:8 rq:1382324 version:0.16.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-librt/python-librt.changes 2026-08-24 12:02:07.321712846 +0200 +++ /work/SRC/openSUSE:Factory/.python-librt.new.3698736/python-librt.changes 2026-10-06 19:13:25.438966680 +0200 @@ -1,0 +2,6 @@ +Sun Oct 4 14:35:14 UTC 2026 - Dirk Müller <[email protected]> + +- update to 0.16.0: + * Sync mypy and bump version + +------------------------------------------------------------------- Old: ---- librt-0.15.0.tar.gz New: ---- librt-0.16.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-librt.spec ++++++ --- /var/tmp/diff_new_pack.r8lYC3/_old 2026-10-06 19:13:26.087993833 +0200 +++ /var/tmp/diff_new_pack.r8lYC3/_new 2026-10-06 19:13:26.089993917 +0200 @@ -17,7 +17,7 @@ Name: python-librt -Version: 0.15.0 +Version: 0.16.0 Release: 0 Summary: Mypyc runtime library License: MIT ++++++ librt-0.15.0.tar.gz -> librt-0.16.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/librt-0.15.0/CPy.h new/librt-0.16.0/CPy.h --- old/librt-0.15.0/CPy.h 2026-08-07 12:12:17.000000000 +0200 +++ new/librt-0.16.0/CPy.h 2026-09-29 01:22:41.000000000 +0200 @@ -22,6 +22,52 @@ #define CPYTHON_LARGE_INT_ERRMSG "Python int too large to convert to C ssize_t" +// Native module import synchronization + +typedef struct CPyModuleLockAPI CPyModuleLockAPI; + +typedef struct { + int32_t initialized; + int32_t executed; +} CPyImportState; + +enum { + CPY_LOCK_ERROR = -1, + CPY_LOCK_ACQUIRED = 0, + CPY_LOCK_DEADLOCK = 1, +}; + +CPyModuleLockAPI *CPyModuleLockAPI_Alloc(void); +void CPyModuleLockAPI_Free(CPyModuleLockAPI *api); +int CPyImport_AcquireLock(CPyModuleLockAPI *api, PyObject *module_name, + PyObject **module_lock); +int CPyImport_ReleaseLock(PyObject *module_lock); +bool CPyImport_IsModuleInitializing(PyObject *module); +bool CPyImport_IsInitialized(const CPyImportState *state); +void CPyImport_SetInitialized(CPyImportState *state, bool initialized); +bool CPyImport_IsExecuted(const CPyImportState *state); +void CPyImport_SetExecuted(CPyImportState *state); + +#define CPY_MODULE_CACHE_UNVERIFIED ((CPyModuleCache)1) + +static inline CPyModuleCache CPyImport_LoadModuleCache(CPyModuleCache *cache) { +#if PY_VERSION_HEX >= 0x030D0000 + return _Py_atomic_load_uintptr_acquire(cache); +#else + return *cache; +#endif +} + +static inline PyObject *CPyImport_GetModuleCache(CPyModuleCache *cache) { + return (PyObject *)(CPyImport_LoadModuleCache(cache) & ~CPY_MODULE_CACHE_UNVERIFIED); +} + +PyObject *CPyImport_GetModuleCacheForImport(CPyModuleCache *cache, PyObject *module_name); +void CPyImport_ReplaceModuleCache(CPyModuleCache *cache, PyObject *module); +void CPyImport_ReplaceModuleCacheUnverified(CPyModuleCache *cache, PyObject *module); +void CPyImport_ReplaceModuleCacheForImport(CPyModuleCache *cache, PyObject *module); + + // Naming conventions: // // Tagged: tagged int @@ -107,13 +153,13 @@ #define CPY_GET_ATTR_TRAIT(obj, trait, vtable_index, object_type, attr_type) \ ((attr_type (*)(object_type *))(CPy_FindTraitVtable(trait, ((object_type *)obj)->vtable))[vtable_index])((object_type *)obj) -// Set attribute value using vtable -#define CPY_SET_ATTR(obj, type, vtable_index, value, object_type, attr_type) \ - ((bool (*)(object_type *, attr_type))((object_type *)obj)->vtable[vtable_index])( \ +// Set attribute value using vtable. +#define CPY_SET_ATTR(obj, type, vtable_index, value, object_type, attr_type, ret_type) \ + ((ret_type (*)(object_type *, attr_type))((object_type *)obj)->vtable[vtable_index])( \ (object_type *)obj, value) -#define CPY_SET_ATTR_TRAIT(obj, trait, vtable_index, value, object_type, attr_type) \ - ((bool (*)(object_type *, attr_type))(CPy_FindTraitVtable(trait, ((object_type *)obj)->vtable))[vtable_index])( \ +#define CPY_SET_ATTR_TRAIT(obj, trait, vtable_index, value, object_type, attr_type, ret_type) \ + ((ret_type (*)(object_type *, attr_type))(CPy_FindTraitVtable(trait, ((object_type *)obj)->vtable))[vtable_index])( \ (object_type *)obj, value) #define CPY_GET_METHOD(obj, type, vtable_index, object_type, method_type) \ @@ -658,6 +704,7 @@ PyObject *CPyIter_Next(PyObject *iter); PyObject *CPyNumber_Power(PyObject *base, PyObject *index); PyObject *CPyNumber_InPlacePower(PyObject *base, PyObject *index); +// An omitted slice bound is represented by CPY_INT_TAG. PyObject *CPyObject_GetSlice(PyObject *obj, CPyTagged start, CPyTagged end); @@ -862,11 +909,37 @@ // Tuple operations - -PyObject *CPySequenceTuple_GetItem(PyObject *tuple, CPyTagged index); PyObject *CPySequenceTuple_GetSlice(PyObject *obj, CPyTagged start, CPyTagged end); -PyObject *CPySequenceTuple_GetItemUnsafe(PyObject *tuple, Py_ssize_t index); -void CPySequenceTuple_SetItemUnsafe(PyObject *tuple, Py_ssize_t index, PyObject *value); +PyObject *CPySequenceTuple_GetItem_(PyObject *tuple, CPyTagged index); + +static inline PyObject *CPySequenceTuple_GetItem(PyObject *tuple, CPyTagged index) +{ + if (likely(CPyTagged_CheckShort(index) && !CPyTagged_IsNegative(index))) { + Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); + Py_ssize_t size = PyTuple_GET_SIZE(tuple); + if (unlikely(n >= size)) { + PyErr_SetString(PyExc_IndexError, "tuple index out of range"); + return NULL; + } + PyObject *result = PyTuple_GET_ITEM(tuple, n); + Py_INCREF(result); + return result; + } else { + return CPySequenceTuple_GetItem_(tuple, index); + } +} + +static inline PyObject *CPySequenceTuple_GetItemUnsafe(PyObject *tuple, Py_ssize_t index) +{ + PyObject *result = PyTuple_GET_ITEM(tuple, index); + Py_INCREF(result); + return result; +} + +static inline void CPySequenceTuple_SetItemUnsafe(PyObject *tuple, Py_ssize_t index, PyObject *value) +{ + PyTuple_SET_ITEM(tuple, index, value); +} // Exception operations @@ -964,7 +1037,8 @@ PyObject *CPy_CalculateMetaclass(PyObject *type, PyObject *o); PyObject *CPy_GetCoro(PyObject *obj); -PyObject *CPyIter_Send(PyObject *iter, PyObject *val); +PyObject *CPyGen_AlreadyExecutingError(int is_coroutine); +PyObject *CPyIter_Send(PyObject *iter, PyObject *val, PyObject **stop_iter_value); int CPy_YieldFromErrorHandle(PyObject *iter, PyObject **outp); PyObject *CPy_FetchStopIterationValue(void); PyObject *CPyType_FromTemplate(PyObject *template_, @@ -1007,18 +1081,24 @@ PyObject *CPy_CallReverseOpMethod(PyObject *left, PyObject *right, const char *op, PyObject *method); -bool CPyImport_ImportMany(PyObject *modules, CPyModule **statics[], PyObject *globals, +bool CPyImport_ImportMany(PyObject *modules, CPyModuleCache *statics[], PyObject *globals, PyObject *tb_path, PyObject *tb_function, Py_ssize_t *tb_lines); PyObject *CPyImport_ImportFromMany(PyObject *mod_id, PyObject *names, PyObject *as_names, PyObject *globals); -PyObject *CPyImport_GetNativeAttrs(PyObject *mod_id, PyObject *names, PyObject *as_names, +PyObject *CPyImport_GetNativeAttrs(PyObject *mod, PyObject *names, PyObject *as_names, PyObject *globals); PyObject *CPyImport_ImportNative(PyObject *module_name, PyObject *(*init_only_fn)(void), int (*exec_fn)(PyObject *), CPyModule **module_static, + CPyModuleCache *module_cache, + CPyImportState *state, CPyModuleLockAPI *lock_api, PyObject *shared_lib_file, PyObject *ext_suffix, Py_ssize_t is_package); +int CPyImport_Exec(PyObject *module, int (*exec_fn)(PyObject *), CPyImportState *state, + CPyModuleCache *module_cache); +PyObject *CPyImport_BeginInitializing(PyObject *module); +int CPyImport_EndInitializing(PyObject *spec); int CPyImport_SetDunderAttrs(PyObject *module, PyObject *module_name, PyObject *shared_lib_file, PyObject *ext_suffix, Py_ssize_t is_package); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/librt-0.15.0/PKG-INFO new/librt-0.16.0/PKG-INFO --- old/librt-0.15.0/PKG-INFO 2026-08-07 12:12:21.037635300 +0200 +++ new/librt-0.16.0/PKG-INFO 2026-09-29 01:22:43.907548000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: librt -Version: 0.15.0 +Version: 0.16.0 Summary: Mypyc runtime library Author-email: Jukka Lehtosalo <[email protected]>, Ivan Levkivskyi <[email protected]> License-Expression: MIT diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/librt-0.15.0/bytearray_extra_ops.c new/librt-0.16.0/bytearray_extra_ops.c --- old/librt-0.15.0/bytearray_extra_ops.c 2026-08-07 12:12:17.000000000 +0200 +++ new/librt-0.16.0/bytearray_extra_ops.c 2026-09-29 01:22:41.000000000 +0200 @@ -3,3 +3,31 @@ PyObject *CPyByteArray_New(void) { return PyByteArray_FromStringAndSize(NULL, 0); } + +PyObject *CPyByteArray_FromBytesSlice(PyObject *obj, CPyTagged start, CPyTagged end) { + if (PyBytes_CheckExact(obj) + && (start == CPY_INT_TAG || CPyTagged_CheckShort(start)) + && (end == CPY_INT_TAG || CPyTagged_CheckShort(end))) { + Py_ssize_t size = PyBytes_GET_SIZE(obj); + Py_ssize_t startn = start == CPY_INT_TAG ? 0 : CPyTagged_ShortAsSsize_t(start); + Py_ssize_t endn = end == CPY_INT_TAG ? size : CPyTagged_ShortAsSsize_t(end); + if (startn < 0) { + startn += size; + } + if (endn < 0) { + endn += size; + } + if (0 <= startn && startn <= endn && endn <= size) { + return PyByteArray_FromStringAndSize(PyBytes_AS_STRING(obj) + startn, endn - startn); + } + } + + // Preserve general slice semantics, including bytes subclass overrides. + PyObject *slice = CPyObject_GetSlice(obj, start, end); + if (slice == NULL) { + return NULL; + } + PyObject *result = PyByteArray_FromObject(slice); + Py_DECREF(slice); + return result; +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/librt-0.15.0/bytearray_extra_ops.h new/librt-0.16.0/bytearray_extra_ops.h --- old/librt-0.15.0/bytearray_extra_ops.h 2026-08-07 12:12:17.000000000 +0200 +++ new/librt-0.16.0/bytearray_extra_ops.h 2026-09-29 01:22:41.000000000 +0200 @@ -7,4 +7,8 @@ // Construct empty bytearray PyObject *CPyByteArray_New(void); +// Construct a bytearray from a bytes slice, avoiding an intermediate bytes object. +// An omitted bound is represented by CPY_INT_TAG. +PyObject *CPyByteArray_FromBytesSlice(PyObject *obj, CPyTagged start, CPyTagged end); + #endif diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/librt-0.15.0/generic_ops.c new/librt-0.16.0/generic_ops.c --- old/librt-0.15.0/generic_ops.c 2026-08-07 12:12:17.000000000 +0200 +++ new/librt-0.16.0/generic_ops.c 2026-09-29 01:22:41.000000000 +0200 @@ -47,9 +47,11 @@ } PyObject *CPyObject_GetSlice(PyObject *obj, CPyTagged start, CPyTagged end) { - PyObject *start_obj = CPyTagged_AsObject(start); - PyObject *end_obj = CPyTagged_AsObject(end); + PyObject *start_obj = start == CPY_INT_TAG ? Py_NewRef(Py_None) : CPyTagged_AsObject(start); + PyObject *end_obj = end == CPY_INT_TAG ? Py_NewRef(Py_None) : CPyTagged_AsObject(end); if (unlikely(start_obj == NULL || end_obj == NULL)) { + Py_XDECREF(start_obj); + Py_XDECREF(end_obj); return NULL; } PyObject *slice = PySlice_New(start_obj, end_obj, NULL); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/librt-0.15.0/librt.egg-info/PKG-INFO new/librt-0.16.0/librt.egg-info/PKG-INFO --- old/librt-0.15.0/librt.egg-info/PKG-INFO 2026-08-07 12:12:20.000000000 +0200 +++ new/librt-0.16.0/librt.egg-info/PKG-INFO 2026-09-29 01:22:43.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: librt -Version: 0.15.0 +Version: 0.16.0 Summary: Mypyc runtime library Author-email: Jukka Lehtosalo <[email protected]>, Ivan Levkivskyi <[email protected]> License-Expression: MIT diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/librt-0.15.0/librt.egg-info/SOURCES.txt new/librt-0.16.0/librt.egg-info/SOURCES.txt --- old/librt-0.15.0/librt.egg-info/SOURCES.txt 2026-08-07 12:12:20.000000000 +0200 +++ new/librt-0.16.0/librt.egg-info/SOURCES.txt 2026-09-29 01:22:43.000000000 +0200 @@ -20,6 +20,7 @@ init.c int_ops.c list_ops.c +locks.c misc_ops.c mypyc_util.h pyproject.toml @@ -57,6 +58,7 @@ ./init.c ./int_ops.c ./list_ops.c +./locks.c ./misc_ops.c ./mypyc_util.h ./pythoncapi_compat.h diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/librt-0.15.0/list_ops.c new/librt-0.16.0/list_ops.c --- old/librt-0.15.0/list_ops.c 2026-08-07 12:12:17.000000000 +0200 +++ new/librt-0.16.0/list_ops.c 2026-09-29 01:22:41.000000000 +0200 @@ -283,7 +283,7 @@ Py_ssize_t index = PyList_GET_SIZE(list) - 1; return list_pop_index(list, index); #else - // I tried a specalized version of pop_impl for just removing the + // I tried a specialized version of pop_impl for just removing the // last element and it wasn't any faster in microbenchmarks than // the generic one so I ditched it. return list_pop_impl((PyListObject *)list, -1); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/librt-0.15.0/locks.c new/librt-0.16.0/locks.c --- old/librt-0.15.0/locks.c 1970-01-01 01:00:00.000000000 +0100 +++ new/librt-0.16.0/locks.c 2026-09-29 01:22:41.000000000 +0200 @@ -0,0 +1,268 @@ +#include "CPy.h" + +struct CPyModuleLockAPI { + PyObject *get_module_lock; + PyObject *deadlock_error; +}; + +CPyModuleLockAPI *CPyModuleLockAPI_Alloc(void) { + CPyModuleLockAPI *api = PyMem_Calloc(1, sizeof(CPyModuleLockAPI)); + if (api == NULL) { + PyErr_NoMemory(); + return NULL; + } + + PyObject *bootstrap = PyImport_ImportModule("importlib._bootstrap"); + if (bootstrap == NULL) { + PyMem_Free(api); + return NULL; + } + api->get_module_lock = PyObject_GetAttrString(bootstrap, "_get_module_lock"); + api->deadlock_error = PyObject_GetAttrString(bootstrap, "_DeadlockError"); + Py_DECREF(bootstrap); + if (api->get_module_lock == NULL || api->deadlock_error == NULL) { + CPyModuleLockAPI_Free(api); + return NULL; + } + return api; +} + +void CPyModuleLockAPI_Free(CPyModuleLockAPI *api) { + if (api == NULL) { + return; + } + Py_XDECREF(api->get_module_lock); + Py_XDECREF(api->deadlock_error); + PyMem_Free(api); +} + +int CPyImport_AcquireLock(CPyModuleLockAPI *api, PyObject *module_name, + PyObject **acquired_lock) { + *acquired_lock = NULL; + if (api == NULL) { + return CPY_LOCK_ACQUIRED; + } + + PyObject *module_lock = PyObject_CallOneArg(api->get_module_lock, module_name); + if (module_lock == NULL) { + return CPY_LOCK_ERROR; + } + PyObject *result = PyObject_CallMethod(module_lock, "acquire", NULL); + if (result == NULL) { + if (PyErr_ExceptionMatches(api->deadlock_error)) { + PyErr_Clear(); + Py_DECREF(module_lock); + return CPY_LOCK_DEADLOCK; + } + Py_DECREF(module_lock); + return CPY_LOCK_ERROR; + } + Py_DECREF(result); + *acquired_lock = module_lock; + return CPY_LOCK_ACQUIRED; +} + +int CPyImport_ReleaseLock(PyObject *module_lock) { + if (module_lock == NULL) { + return 0; + } + + PyObject *result = PyObject_CallMethod(module_lock, "release", NULL); + Py_DECREF(module_lock); + if (result == NULL) { + return -1; + } + Py_DECREF(result); + return 0; +} + +// CPython exposes pyatomic.h through Python.h starting in 3.13. Older supported +// versions are GIL-only, so plain accesses provide the same serialization there. +bool CPyImport_IsInitialized(const CPyImportState *state) { +#if PY_VERSION_HEX >= 0x030D0000 + return _Py_atomic_load_int32(&state->initialized) != 0; +#else + return state->initialized != 0; +#endif +} + +void CPyImport_SetInitialized(CPyImportState *state, bool initialized) { +#if PY_VERSION_HEX >= 0x030D0000 + _Py_atomic_store_int32(&state->initialized, initialized); +#else + state->initialized = initialized; +#endif +} + +bool CPyImport_IsExecuted(const CPyImportState *state) { +#if PY_VERSION_HEX >= 0x030D0000 + return _Py_atomic_load_int32(&state->executed) != 0; +#else + return state->executed != 0; +#endif +} + +void CPyImport_SetExecuted(CPyImportState *state) { +#if PY_VERSION_HEX >= 0x030D0000 + _Py_atomic_store_int32(&state->executed, true); +#else + state->executed = true; +#endif +} + +static PyObject *CPyImport_DecodeModuleCache(CPyModuleCache cached) { + return (PyObject *)(cached & ~CPY_MODULE_CACHE_UNVERIFIED); +} + +static CPyModuleCache CPyImport_EncodeModuleCache(PyObject *module, bool unverified) { + CPyModuleCache cached = (CPyModuleCache)module; + assert((cached & CPY_MODULE_CACHE_UNVERIFIED) == 0); + return cached | (unverified ? CPY_MODULE_CACHE_UNVERIFIED : 0); +} + +static void CPyImport_DecRefOld(PyObject *previous) { + if (previous == NULL) { + return; + } +#ifdef Py_GIL_DISABLED + // Atomic loads return borrowed references, so defer releasing the old + // reference until concurrent readers have passed a quiescent point. + if (!_Py_IsImmortal(previous)) { + _PyObject_XDecRefDelayed(previous); + } +#else + Py_DECREF(previous); +#endif +} + +bool CPyImport_IsModuleInitializing(PyObject *module) { + PyObject *spec = PyObject_GetAttrString(module, "__spec__"); + if (spec == NULL) { + PyErr_Clear(); + return false; + } + PyObject *initializing = PyObject_GetAttrString(spec, "_initializing"); + Py_DECREF(spec); + if (initializing == NULL) { + PyErr_Clear(); + return false; + } + bool result = Py_IsTrue(initializing); + Py_DECREF(initializing); + return result; +} + +PyObject *CPyImport_GetModuleCacheForImport(CPyModuleCache *cache, PyObject *module_name) { + for (;;) { + CPyModuleCache cached_value = CPyImport_LoadModuleCache(cache); + PyObject *cached = CPyImport_DecodeModuleCache(cached_value); + if (cached == Py_None) { + return Py_None; + } + + // An untagged module has already been verified. This is the steady-state + // fast path and avoids consulting sys.modules or the module spec again. + if ((cached_value & CPY_MODULE_CACHE_UNVERIFIED) == 0) { + return cached; + } + + // Generic imports may cache a partial module so compiled references after + // the import can use the value returned by CPython to break an import-lock + // deadlock. Such a module must not take the fast path on a later import. + // Comparing with sys.modules also rejects a partial module left behind in + // this cache after its initialization failed. + PyObject *current = PyImport_GetModule(module_name); + if (current == NULL) { + return PyErr_Occurred() ? NULL : Py_None; + } + bool valid = current == cached && !CPyImport_IsModuleInitializing(current); + Py_DECREF(current); + if (!valid) { + return Py_None; + } + + // Clearing the tag changes only the cache metadata and does not transfer + // a reference. Retry if another thread replaced the cache: generated code + // reloads the cache after this check, so only the value whose tag we + // successfully cleared may take the fast path. +#if PY_VERSION_HEX >= 0x030D0000 + CPyModuleCache expected = cached_value; + if (_Py_atomic_compare_exchange_uintptr(cache, &expected, (CPyModuleCache)cached)) { + return cached; + } +#else + if (*cache == cached_value) { + *cache = (CPyModuleCache)cached; + return cached; + } +#endif + } +} + +static void CPyImport_ReplaceModuleCacheValue(CPyModuleCache *cache, PyObject *module, + bool unverified) { + CPyModuleCache desired = CPyImport_EncodeModuleCache(module, unverified); +#if PY_VERSION_HEX >= 0x030D0000 + for (;;) { + CPyModuleCache previous = CPyImport_LoadModuleCache(cache); + if (previous == desired) { + return; + } + CPyModuleCache expected = previous; + PyObject *previous_module = CPyImport_DecodeModuleCache(previous); + if (previous_module == module) { + // Verification is monotonic for a particular module object. A + // thread that observed it while it was still initializing must not + // re-tag it after another thread has verified completion. + if ((previous & CPY_MODULE_CACHE_UNVERIFIED) == 0) { + return; + } + // Only the tag is changing, so the cache retains its existing + // reference to module. + if (_Py_atomic_compare_exchange_uintptr(cache, &expected, desired)) { + return; + } + } else { + Py_INCREF(module); + if (_Py_atomic_compare_exchange_uintptr(cache, &expected, desired)) { + if (previous_module != NULL && previous_module != Py_None) { + CPyImport_DecRefOld(previous_module); + } + return; + } + Py_DECREF(module); + } + } +#else + CPyModuleCache previous = *cache; + PyObject *previous_module = CPyImport_DecodeModuleCache(previous); + if (previous_module == module) { + if ((previous & CPY_MODULE_CACHE_UNVERIFIED) != 0) { + *cache = desired; + } + return; + } + Py_INCREF(module); + *cache = desired; + if (previous_module == NULL || previous_module == Py_None) { + return; + } + CPyImport_DecRefOld(previous_module); +#endif +} + +void CPyImport_ReplaceModuleCache(CPyModuleCache *cache, PyObject *module) { + CPyImport_ReplaceModuleCacheValue(cache, module, false); +} + +void CPyImport_ReplaceModuleCacheUnverified(CPyModuleCache *cache, PyObject *module) { + CPyImport_ReplaceModuleCacheValue(cache, module, true); +} + +void CPyImport_ReplaceModuleCacheForImport(CPyModuleCache *cache, PyObject *module) { + if (CPyImport_LoadModuleCache(cache) == (CPyModuleCache)module) { + return; + } + + CPyImport_ReplaceModuleCacheValue(cache, module, CPyImport_IsModuleInitializing(module)); +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/librt-0.15.0/misc_ops.c new/librt-0.16.0/misc_ops.c --- old/librt-0.15.0/misc_ops.c 2026-08-07 12:12:17.000000000 +0200 +++ new/librt-0.16.0/misc_ops.c 2026-09-29 01:22:41.000000000 +0200 @@ -22,14 +22,33 @@ } } -PyObject *CPyIter_Send(PyObject *iter, PyObject *val) +// Raise CPython-compatible errors after a failed CPyGen_TryEnter. +PyObject *CPyGen_AlreadyExecutingError(int is_coroutine) +{ + PyErr_SetString(PyExc_ValueError, + is_coroutine ? "coroutine already executing" + : "generator already executing"); + return NULL; +} + +PyObject *CPyIter_Send(PyObject *iter, PyObject *val, PyObject **stop_iter_value) { // Do a send, or a next if second arg is None. // (This behavior is to match the PEP 380 spec for yield from.) - if (Py_IsNone(val)) { - return CPyIter_Next(iter); - } else { - return PyObject_CallMethodOneArg(iter, mypyc_interned_str.send, val); + // + // If the iterator yields a value, return it. If it completes normally, + // return NULL and store the return value in *stop_iter_value, without + // raising StopIteration. If it raises, return NULL and leave + // *stop_iter_value untouched (the caller initializes it to NULL). + PyObject *result; + switch (PyIter_Send(iter, val, &result)) { + case PYGEN_NEXT: + return result; + case PYGEN_RETURN: + *stop_iter_value = result; + return NULL; + default: + return NULL; } } @@ -770,14 +789,19 @@ return result; } -static bool import_single(PyObject *mod_id, PyObject **mod_static, +static bool import_single(PyObject *mod_id, CPyModuleCache *mod_static, PyObject *globals_id, PyObject *globals_name, PyObject *globals) { - if (Py_IsNone(*mod_static)) { + PyObject *cached = CPyImport_GetModuleCacheForImport(mod_static, mod_id); + if (cached == NULL) { + return false; + } + if (Py_IsNone(cached)) { CPyModule *mod = PyImport_Import(mod_id); if (mod == NULL) { return false; } - *mod_static = mod; + CPyImport_ReplaceModuleCacheForImport(mod_static, mod); + Py_DECREF(mod); } PyObject *mod_dict = PyImport_GetModuleDict(); @@ -795,7 +819,7 @@ } // Table-driven import helper. See transform_import() in irbuild for the details. -bool CPyImport_ImportMany(PyObject *modules, CPyModule **statics[], PyObject *globals, +bool CPyImport_ImportMany(PyObject *modules, CPyModuleCache *statics[], PyObject *globals, PyObject *tb_path, PyObject *tb_function, Py_ssize_t *tb_lines) { for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(modules); i++) { PyObject *module = PyTuple_GET_ITEM(modules, i); @@ -882,33 +906,24 @@ return mod; } -// Import attributes from an already-imported native module and store them +// Import attributes from an already-imported native module object and store them // in the globals dict. Returns the module on success, NULL on error. -PyObject *CPyImport_GetNativeAttrs(PyObject *mod_id, PyObject *names, +PyObject *CPyImport_GetNativeAttrs(PyObject *mod, PyObject *names, PyObject *as_names, PyObject *globals) { - PyObject *mod = PyImport_GetModule(mod_id); - if (mod == NULL) { - if (!PyErr_Occurred()) { - PyErr_Format(PyExc_ImportError, "module '%U' is not in sys.modules", mod_id); - } - return NULL; - } for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(names); i++) { PyObject *name = PyTuple_GET_ITEM(names, i); PyObject *as_name = PyTuple_GET_ITEM(as_names, i); PyObject *obj = PyObject_GetAttr(mod, name); if (obj == NULL) { - Py_DECREF(mod); return NULL; } int ret = CPyDict_SetItem(globals, as_name, obj); Py_DECREF(obj); if (ret < 0) { - Py_DECREF(mod); return NULL; } } - return mod; + return Py_NewRef(mod); } // From CPython @@ -1506,46 +1521,244 @@ return 0; } +// Mark module's current spec as initializing and return an owned reference. +PyObject *CPyImport_BeginInitializing(PyObject *module) { + PyObject *spec = PyObject_GetAttrString(module, "__spec__"); + if (spec == NULL) { + return NULL; + } + if (PyObject_SetAttrString(spec, "_initializing", Py_True) < 0) { + Py_DECREF(spec); + return NULL; + } + return spec; +} + +// Clear initializing on spec and consume its reference. +int CPyImport_EndInitializing(PyObject *spec) { + int result = PyObject_SetAttrString(spec, "_initializing", Py_False); + Py_DECREF(spec); + return result; +} + +// Split module_name into owned parent and child-name references. +// Both outputs are NULL for a top-level module. +static int CPyImport_SplitName(PyObject *module_name, PyObject **parent_name, + PyObject **child_name) { + *parent_name = NULL; + *child_name = NULL; + Py_ssize_t name_len = PyUnicode_GetLength(module_name); + if (name_len < 0) { + return -1; + } + Py_ssize_t dot = PyUnicode_FindChar(module_name, '.', 0, name_len, -1); + if (dot < 0) { + return 0; + } + *parent_name = PyUnicode_Substring(module_name, 0, dot); + *child_name = PyUnicode_Substring(module_name, dot + 1, name_len); + if (*parent_name == NULL || *child_name == NULL) { + Py_CLEAR(*parent_name); + Py_CLEAR(*child_name); + return -1; + } + return 0; +} + +// Import module_name's parent, if any. +static int CPyImport_ImportParent(PyObject *module_name) { + PyObject *parent_name; + PyObject *child_name; + if (CPyImport_SplitName(module_name, &parent_name, &child_name) < 0) { + return -1; + } + Py_XDECREF(child_name); + if (parent_name == NULL) { + return 0; + } + PyObject *parent_module = PyImport_Import(parent_name); + Py_DECREF(parent_name); + if (parent_module == NULL) { + return -1; + } + Py_DECREF(parent_module); + return 0; +} + +// Bind the module under its child name on the current parent in sys.modules. +static int CPyImport_SetParentAttr(PyObject *module, PyObject *module_name) { + PyObject *parent_name; + PyObject *child_name; + if (CPyImport_SplitName(module_name, &parent_name, &child_name) < 0) { + return -1; + } + if (parent_name == NULL) { + return 0; + } + PyObject *parent_module = PyObject_GetItem(PyImport_GetModuleDict(), parent_name); + if (parent_module == NULL) { + Py_DECREF(parent_name); + Py_DECREF(child_name); + return -1; + } + int result = PyObject_SetAttr(parent_module, child_name, module); + Py_DECREF(parent_module); + if (result < 0 && PyErr_ExceptionMatches(PyExc_AttributeError)) { + PyErr_Clear(); + result = PyErr_WarnFormat( + PyExc_ImportWarning, 1, + "Cannot set an attribute on %R for child module %R", + parent_name, child_name); + } + Py_DECREF(parent_name); + Py_DECREF(child_name); + return result; +} + +// Check whether a dotted module is already bound to its parent. A top-level +// module has no parent binding to wait for. +static int CPyImport_IsBoundToParent(PyObject *module, PyObject *module_name) { + PyObject *parent_name; + PyObject *child_name; + if (CPyImport_SplitName(module_name, &parent_name, &child_name) < 0) { + return -1; + } + if (parent_name == NULL) { + return 1; + } + PyObject *parent_module = PyObject_GetItem(PyImport_GetModuleDict(), parent_name); + Py_DECREF(parent_name); + if (parent_module == NULL) { + Py_DECREF(child_name); + return -1; + } + PyObject *child = PyObject_GetAttr(parent_module, child_name); + Py_DECREF(parent_module); + Py_DECREF(child_name); + if (child == NULL) { + if (PyErr_ExceptionMatches(PyExc_AttributeError)) { + PyErr_Clear(); + return 0; + } + return -1; + } + int result = child == module; + Py_DECREF(child); + return result; +} + +static int CPyImport_ReleaseLockPreservingException(PyObject *module_lock) { + PyObject *exc_type, *exc_val, *exc_tb; + PyErr_Fetch(&exc_type, &exc_val, &exc_tb); + int result = CPyImport_ReleaseLock(module_lock); + if (result < 0) { + PyErr_Clear(); + } + PyErr_Restore(exc_type, exc_val, exc_tb); + return result; +} + +static void CPyImport_SetModuleReplacedError(PyObject *module_name) { + PyErr_Format(PyExc_ImportError, + "native module '%U' in sys.modules was replaced after initialization", + module_name); +} + +static void CPyImport_SetModuleMissingError(PyObject *module_name) { + PyErr_Format(PyExc_ImportError, + "initialized native module '%U' is missing from sys.modules", + module_name); +} + +// Execute a module once; caller holds the module lock. +int CPyImport_Exec(PyObject *module, int (*exec_fn)(PyObject *), CPyImportState *state, + CPyModuleCache *module_cache) { + if (CPyImport_IsExecuted(state)) { + const char *module_name = PyModule_GetName(module); + if (module_name != NULL) { + PyErr_Format(PyExc_ImportError, + "native module '%s' does not support reinitialization", + module_name); + } + return -1; + } + + int result = exec_fn(module); + if (result == 0) { + PyObject *cached_module = CPyImport_GetModuleCache(module_cache); + // Keep the cache lazy. A normal shim import should not populate it, since + // the first compiled native import must still validate sys.modules. If a + // compiled import already populated the cache (including with a partial + // module during a circular import), refresh it to this instance but keep + // it tagged as unverified until import finalization has completed. + if (cached_module != NULL && cached_module != Py_None) { + CPyImport_ReplaceModuleCacheUnverified(module_cache, module); + } + // A successfully executed native module body must not run again, even if + // import finalization subsequently fails. Publish this only after any + // partial cache entry has been refreshed to the module that was executed. + CPyImport_SetExecuted(state); + } + return result; +} + PyObject *CPyImport_ImportNative(PyObject *module_name, PyObject *(*init_only_fn)(void), int (*exec_fn)(PyObject *), CPyModule **module_static, + CPyModuleCache *module_cache, + CPyImportState *state, CPyModuleLockAPI *lock_api, PyObject *shared_lib_file, PyObject *ext_suffix, Py_ssize_t is_package) { - PyObject *parent_module = NULL; - PyObject *child_name = NULL; + PyObject *initializing_spec = NULL; PyObject *exc_type, *exc_val, *exc_tb; - Py_ssize_t name_len = PyUnicode_GetLength(module_name); - if (name_len < 0) { + int end_result; + + // Import the parent package first to preserve import ordering semantics. + if (CPyImport_ImportParent(module_name) < 0) { return NULL; } - Py_ssize_t dot = PyUnicode_FindChar(module_name, '.', 0, name_len, -1); - if (dot >= 0) { - // Import the parent package first to preserve import ordering semantics. - PyObject *parent_name = PyUnicode_Substring(module_name, 0, dot); - if (parent_name == NULL) { - CPyError_OutOfMemory(); - } - child_name = PyUnicode_Substring(module_name, dot + 1, name_len); - if (child_name == NULL) { - CPyError_OutOfMemory(); - } - parent_module = PyImport_Import(parent_name); - Py_DECREF(parent_name); - if (parent_module == NULL) { - Py_DECREF(child_name); - return NULL; - } - } // Create the module object without executing the module body. // CPyInitOnly_* uses an internal static to cache the module object. // We then check sys.modules to determine whether the module body // has already been executed (or is being executed in a circular import). + PyObject *module_lock; + int lock_result; + bool retried_after_execution = false; + while ((lock_result = CPyImport_AcquireLock(lock_api, module_name, &module_lock)) == + CPY_LOCK_DEADLOCK) { + PyObject *partial = PyDict_GetItemWithError(PyImport_GetModuleDict(), module_name); + if (partial != NULL && + (*module_static == NULL || partial == (PyObject *)*module_static)) { + CPyImport_ReplaceModuleCacheUnverified(module_cache, partial); + Py_INCREF(partial); + return partial; + } + // Multiple threads can detect the same cycle concurrently on a + // free-threaded build. If the target body has finished but the module + // is temporarily absent during importlib's pop/reinsert, retry once to + // wait for finalization. If it's still missing quit and report an error + // to not spin infinitely. + if (partial == NULL && !PyErr_Occurred() && CPyImport_IsExecuted(state) && + !retried_after_execution) { + retried_after_execution = true; + continue; + } + if (!PyErr_Occurred()) { + PyErr_Format(PyExc_ImportError, + "import deadlock for native module '%U' without a partial module", + module_name); + } + return NULL; + } + if (lock_result == CPY_LOCK_ERROR) { + return NULL; + } + PyObject *module_dict = PyImport_GetModuleDict(); if (module_dict == NULL) { - Py_XDECREF(parent_module); - Py_XDECREF(child_name); + CPyImport_ReleaseLockPreservingException(module_lock); return NULL; } @@ -1553,34 +1766,55 @@ if (existing != NULL) { if (*module_static != NULL) { if (existing == (PyObject *)*module_static) { + // Acquiring the module lock synchronizes with a regular import. + // A recursive acquisition can still happen during finalization, + // so also verify the spec and parent binding before publishing. + if (CPyImport_IsExecuted(state) + && !CPyImport_IsModuleInitializing(existing)) { + int is_bound = CPyImport_IsBoundToParent(existing, module_name); + if (is_bound < 0) { + CPyImport_ReleaseLockPreservingException(module_lock); + return NULL; + } + if (is_bound) { + // Publish the verified cache value before the completion + // flag. An acquire-load of initialized can then trust the + // cached module without re-reading it for validation. + CPyImport_ReplaceModuleCache(module_cache, existing); + CPyImport_SetInitialized(state, true); + } + } + if (!CPyImport_IsInitialized(state)) { + CPyImport_ReplaceModuleCacheUnverified(module_cache, existing); + } Py_INCREF(existing); - Py_XDECREF(parent_module); - Py_XDECREF(child_name); + if (CPyImport_ReleaseLock(module_lock) < 0) { + Py_DECREF(existing); + existing = NULL; + } return existing; } - PyErr_Format(PyExc_ImportError, - "native module '%U' in sys.modules was replaced after initialization", - module_name); - Py_XDECREF(parent_module); - Py_XDECREF(child_name); + CPyImport_SetModuleReplacedError(module_name); + CPyImport_ReleaseLockPreservingException(module_lock); return NULL; } } if (PyErr_Occurred()) { - Py_XDECREF(parent_module); - Py_XDECREF(child_name); + CPyImport_ReleaseLockPreservingException(module_lock); return NULL; } - PyObject *modobj = init_only_fn(); - if (modobj == NULL) { - Py_XDECREF(parent_module); - Py_XDECREF(child_name); + if (CPyImport_IsInitialized(state) || + CPyImport_GetModuleCache(module_cache) == (PyObject *)*module_static) { + CPyImport_SetModuleMissingError(module_name); + CPyImport_ReleaseLockPreservingException(module_lock); return NULL; } - if (PyObject_SetItem(module_dict, module_name, modobj) < 0) { - goto fail; + PyObject *modobj = init_only_fn(); + if (modobj == NULL) { + CPyImport_ReleaseLockPreservingException(module_lock); + return NULL; } if (*module_static != (CPyModule *)modobj) { @@ -1594,19 +1828,61 @@ goto fail; } + initializing_spec = CPyImport_BeginInitializing(modobj); + if (initializing_spec == NULL) { + goto fail; + } + + if (PyObject_SetItem(module_dict, module_name, modobj) < 0) { + goto fail; + } + // Now execute the module body, with __file__ and __package__ already set. if (exec_fn(modobj) != 0) { goto fail; } - // Match CPython import semantics: publish parent.child only after the - // child module finished executing successfully. - if (parent_module != NULL && PyObject_SetAttr(parent_module, child_name, modobj) < 0) { + // Direct imports bypass importlib's final sys.modules lookup. Reject changes + // to the entry instead of returning an object different from future imports. + PyObject *current = PyDict_GetItemWithError(module_dict, module_name); + if (current == NULL) { + if (!PyErr_Occurred()) { + CPyImport_SetModuleMissingError(module_name); + } + goto fail; + } + if (current != modobj) { + CPyImport_SetModuleReplacedError(module_name); goto fail; } - Py_XDECREF(parent_module); - Py_XDECREF(child_name); + end_result = CPyImport_EndInitializing(initializing_spec); + initializing_spec = NULL; + if (end_result < 0) { + goto fail; + } + + // Direct imports must publish parent.child themselves; normal extension + // loading leaves this to importlib. + int parent_result = CPyImport_SetParentAttr(modobj, module_name); + // The module body and import finalization are complete even if publishing + // parent.child reports an error. Cache the verified object before setting + // the monotonic completion flag. + CPyImport_ReplaceModuleCache(module_cache, modobj); + CPyImport_SetInitialized(state, true); + if (parent_result < 0) { + // The module finished initializing before parent binding was attempted, + // so preserve it even if the warning is promoted to an exception or + // setting the attribute fails with an exception other than AttributeError. + CPyImport_ReleaseLockPreservingException(module_lock); + Py_DECREF(modobj); + return NULL; + } + + if (CPyImport_ReleaseLock(module_lock) < 0) { + Py_DECREF(modobj); + return NULL; + } return modobj; fail: @@ -1615,10 +1891,13 @@ PyErr_Fetch(&exc_type, &exc_val, &exc_tb); PyObject_DelItem(module_dict, module_name); PyErr_Clear(); + if (initializing_spec != NULL) { + CPyImport_EndInitializing(initializing_spec); + PyErr_Clear(); + } PyErr_Restore(exc_type, exc_val, exc_tb); - Py_XDECREF(parent_module); - Py_XDECREF(child_name); Py_CLEAR(*module_static); + CPyImport_ReleaseLockPreservingException(module_lock); return NULL; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/librt-0.15.0/mypyc_util.h new/librt-0.16.0/mypyc_util.h --- old/librt-0.15.0/mypyc_util.h 2026-08-07 12:12:17.000000000 +0200 +++ new/librt-0.16.0/mypyc_util.h 2026-09-29 01:22:41.000000000 +0200 @@ -17,10 +17,13 @@ #if defined(__clang__) || defined(__GNUC__) #define CPy_NOINLINE __attribute__((noinline)) +#define CPy_COLD __attribute__((cold)) #elif defined(_MSC_VER) #define CPy_NOINLINE __declspec(noinline) +#define CPy_COLD #else #define CPy_NOINLINE +#define CPy_COLD #endif #ifndef Py_GIL_DISABLED @@ -130,6 +133,11 @@ typedef PyObject CPyModule; +// Module caches use the low bit to distinguish a module that may still be +// initializing from one whose initialization has been verified. PyObject +// pointers are aligned, so the low bit is otherwise unused. +typedef uintptr_t CPyModuleCache; + // Tag bit used for long integers #define CPY_INT_TAG 1 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/librt-0.15.0/pyproject.toml new/librt-0.16.0/pyproject.toml --- old/librt-0.15.0/pyproject.toml 2026-08-07 12:12:09.000000000 +0200 +++ new/librt-0.16.0/pyproject.toml 2026-09-29 01:22:34.000000000 +0200 @@ -19,7 +19,7 @@ {name = "Jukka Lehtosalo", email = "[email protected]"}, {name = "Ivan Levkivskyi", email = "[email protected]"}, ] -version = "0.15.0" +version = "0.16.0" license = "MIT" classifiers = [ "Development Status :: 3 - Alpha", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/librt-0.15.0/pythonsupport.c new/librt-0.16.0/pythonsupport.c --- old/librt-0.15.0/pythonsupport.c 2026-08-07 12:12:17.000000000 +0200 +++ new/librt-0.16.0/pythonsupport.c 2026-09-29 01:22:41.000000000 +0200 @@ -6,23 +6,31 @@ #include "pythonsupport.h" #ifdef Py_GIL_DISABLED -// Cold slow path of CPy_GetAttrRef (declared in pythonsupport.h). Reached only -// when the inline fast-path try-incref fails: the value is owned by another -// thread, so taking a reference requires an atomic shared-refcount operation. -// Kept out-of-line so the inline fast path stays small. +// Cold slow path of CPy_GetAttrRef (declared in pythonsupport.h). First try to +// acquire a shared reference without locking, then validate that the field still +// contains v. If validation fails, drop the provisional reference. If the shared +// incref or validation fails, take the owner's critical section, reload the field, +// and take a reference while the value cannot be replaced. _Py_XNewRefWithLock +// also sets maybe-weakref lazily, so later cross-thread reads generally use the +// lock-free shared-refcount path. Returns NULL if the field is NULL on reload. // -// First try the lock-free shared-refcount CAS. If the value has not had -// maybe-weakref set yet (for example, it was published by CPy_InitAttrRef), force -// a cross-thread reference via _Py_NewRefWithLock, which cannot fail and sets -// maybe-weakref so subsequent reads take the fast path. The value was already -// observed in the field by CPy_GetAttrRef; CPy_SetAttrRef's QSBR-delayed decref -// keeps any replaced value alive long enough for this reader. +// The reload can be relaxed: acquiring the owner's critical section synchronizes +// with the writer ending its critical section. A value stored by CPy_InitAttrRef is +// ordered by the later publication of the owner itself (see CPy_InitAttrRef). CPy_NOINLINE -PyObject *CPy_GetAttrRefSlow(PyObject *v) { +CPy_COLD +PyObject *CPy_GetAttrRefSlow(PyObject *v, PyObject *owner, PyObject **field) { if (_Py_TryIncRefShared(v)) { - return v; + if (v == (PyObject *)_Py_atomic_load_ptr(field)) { + return v; + } + Py_DECREF(v); } - return _Py_NewRefWithLock(v); // sets maybe-weakref; cannot fail + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(owner); + result = _Py_XNewRefWithLock((PyObject *)_Py_atomic_load_ptr_relaxed(field)); + Py_END_CRITICAL_SECTION(); + return result; } #endif diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/librt-0.15.0/pythonsupport.h new/librt-0.16.0/pythonsupport.h --- old/librt-0.15.0/pythonsupport.h 2026-08-07 12:12:17.000000000 +0200 +++ new/librt-0.16.0/pythonsupport.h 2026-09-29 01:22:41.000000000 +0200 @@ -44,30 +44,43 @@ // // On free-threaded builds a plain load followed by an incref races with a // concurrent setter that may decref the old value to zero and free it before the -// incref runs (use-after-free). CPy_SetAttrRef avoids that by reclaiming old -// values through QSBR-delayed decref, so a value observed in the field remains -// safe to touch while this reader is running. -// -// Only the hot case is inlined here: an incref of a value owned by this thread or -// immortal, via '_Py_TryIncrefFast' (no CAS, no loop). Everything colder -- the -// cross-thread shared-refcount CAS and the _Py_NewRefWithLock fallback -- lives -// out-of-line in 'CPy_GetAttrRefSlow'. Splitting it this way keeps each call -// site's fast path small enough to inline, which is measurably faster than -// letting the compiler auto-out-line the whole helper (that merges every read -// site's branch history into one shared copy and mispredicts). It is only used in -// free-threaded builds; the default (GIL) build keeps the plain load + incref -// generated inline by mypyc. -PyObject *CPy_GetAttrRefSlow(PyObject *v); +// incref runs (use-after-free). We avoid that with an optimistic try-incref, +// validating the field when taking a shared reference. If no reference can be +// secured that way, the fallback takes the same per-object lock as CPy_SetAttrRef +// and reloads the field before taking a reference. This mirrors CPython's own +// instance attribute read (in CPython 3.14: _Py_TryIncrefCompare, then a locked +// reload -- see _PyObject_TryGetInstanceAttribute in Objects/dictobject.c). +// +// CPython's QSBR-aware object heaps keep a stale pointer safe to inspect as a +// PyObject header. The try-incref protocol ensures that stale memory is never returned +// as the attribute value. These guarantees depend on CPython internals and must be +// rechecked when adding support for a new Python version. +// +// The hot path is lock-free and intentionally small: _Py_TryIncrefFast handles +// values owned by this thread and immortal values. Everything colder -- the +// shared-refcount CAS, field validation, and per-object lock fallback -- is in +// CPy_GetAttrRefSlow. The cold annotation also prevents those arguments and +// branches from bloating the caller's hot path. An unflagged cross-thread value +// takes the locked slow path once; _Py_XNewRefWithLock sets maybe-weakref lazily, +// so subsequent reads generally succeed through the lock-free shared-refcount +// path. The default (GIL) build keeps the plain load + incref generated inline by +// mypyc. +// +// Note that observing a non-NULL 'v' does not guarantee a non-NULL result: 'v' is +// only a provisional read, and if the attribute is deleted before a reference to it +// can be secured, this returns NULL and the caller raises AttributeError. That is a +// legal outcome for a racing read, since it is ordered after the delete. +CPy_COLD PyObject *CPy_GetAttrRefSlow(PyObject *v, PyObject *owner, PyObject **field); -static inline PyObject *CPy_GetAttrRef(PyObject **field) { +static inline PyObject *CPy_GetAttrRef(PyObject *owner, PyObject **field) { PyObject *v = (PyObject *)_Py_atomic_load_ptr_acquire(field); if (v == NULL) { return NULL; } - if (_Py_TryIncrefFast(v)) { + if (likely(_Py_TryIncrefFast(v))) { return v; } - return CPy_GetAttrRefSlow(v); + return CPy_GetAttrRefSlow(v, owner, field); } // Read a native attribute that is a single reference-counted 'PyObject *' field @@ -78,10 +91,10 @@ // use-after-free race that CPy_GetAttrRef guards against cannot happen: the field // holds a strong reference for the object's whole lifetime, and any thread reading // it necessarily holds 'self', which keeps the value alive. So the try-incref + -// _Py_NewRefWithLock fallback are unnecessary here -- a plain load + Py_INCREF is -// safe. A cross-thread Py_INCREF is an unconditional -// atomic add on ob_ref_shared, so (unlike CPy_GetAttrRef's try-incref) it needs no -// maybe-weakref and has no slow path. The load is relaxed rather than acquire: the +// _Py_XNewRefWithLock fallback are unnecessary here -- a plain load + Py_INCREF is +// safe. A cross-thread Py_INCREF is an unconditional atomic add on ob_ref_shared +// (CPython 3.14), so (unlike CPy_GetAttrRef's try-incref) it needs no maybe-weakref +// and has no slow path. The load is relaxed rather than acquire: the // reader reached 'self' through a synchronization edge (self's own publication) // that already ordered the construction stores before it, exactly as with // CPy_InitAttrRef's relaxed store. Relaxed keeps it TSan-clean at zero cost (plain @@ -94,57 +107,39 @@ return v; } -// Reclaim the previous value of a native attribute after it has been replaced. -// -// CPy_GetAttrRef reads the field optimistically without holding any lock the -// writer also takes, so a reader can load the old pointer and then try to take a -// reference after this store. The old value must therefore stay alive until every -// thread has passed a quiescent point, which is exactly what a QSBR-deferred -// decref guarantees. So all mortal old values are -// reclaimed via _PyObject_XDecRefDelayed, matching CPython's own replace-a-slot -// paths (e.g. _PyObject_SetDict / _PyObject_SetManagedDict). -// -// We deliberately do NOT take a "local refcount > 1, owned by this thread" fast -// path: dropping the field's reference is not the only decref of the object, so a -// non-freeing local decrement here does not prevent an unrelated reference holder -// from driving the object to zero (a plain, non-deferred Py_DECREF -> _Py_Dealloc) -// while an in-flight reader still holds the stale pointer -- a use-after-free that -// only QSBR deferral closes. Immortal objects are never freed, so skipping their -// decref entirely is safe and avoids queuing a no-op onto the delayed-free list. -static inline void CPy_DecRefAttrOld(PyObject *op) { - if (op == NULL) { - return; - } - if (_Py_IsImmortal(op)) { - return; - } - _PyObject_XDecRefDelayed(op); -} - // Set a native attribute that is a single reference-counted 'PyObject *' field, // stealing the reference to 'value' (which may be NULL to delete the attribute) // and safely reclaiming the previous value. // -// Memory safety does NOT depend on SetMaybeWeakref here, so (unlike an earlier -// version) we do not call it. Two things keep this safe: -// - The old value is reclaimed via CPy_DecRefAttrOld, a QSBR-deferred decref, so -// it cannot be freed while an in-flight CPy_GetAttrRef still holds the stale -// pointer. -// - A concurrent cross-thread reader whose inline fast-path try-incref fails on -// an unflagged 'value' still cannot fail and never blocks on this writer: -// CPy_GetAttrRef falls into CPy_GetAttrRefSlow, which is fully lock-free. It -// retries with a lock-free shared-refcount CAS (_Py_TryIncRefShared) and, only -// if that also fails, forces a reference via _Py_NewRefWithLock, which cannot -// fail and lazily sets maybe-weakref so later cross-thread reads take the CAS. -// This mirrors CPy_InitAttrRef, which already omits SetMaybeWeakref for the same -// reason. Setting the flag here would only be a possible performance tuning knob -// (it would let that first cross-thread reader succeed on the cheaper -// _Py_TryIncRefShared CAS instead of falling through to _Py_NewRefWithLock); it is -// not needed for correctness. The atomic exchange publishes the new pointer and -// hands back the old one without a writer/writer race. -static inline void CPy_SetAttrRef(PyObject **field, PyObject *value) { - PyObject *old = (PyObject *)_Py_atomic_exchange_ptr(field, value); - CPy_DecRefAttrOld(old); +// The owner's critical section serializes writers and synchronizes them with +// CPy_GetAttrRef's fallback path. A lock-free reader either secures a local or +// immortal reference directly, or secures a shared reference and validates the +// field. Otherwise it reloads the field under this lock. Thus once the replacement +// is published, the old field reference can be decrefed immediately, even if a +// reader is still dereferencing the old value (see CPy_GetAttrRef for why touching +// a freed value there is safe). Decref outside the critical section so an +// arbitrary destructor does not run while the owner is locked -- the equivalent in +// CPython 3.14 (store_instance_attr_lock_held) decrefs with the lock still held. +static inline void CPy_SetAttrRef(PyObject *owner, PyObject **field, PyObject *value) { + PyObject *old; + Py_BEGIN_CRITICAL_SECTION(owner); + old = (PyObject *)_Py_atomic_load_ptr_relaxed(field); + _Py_atomic_store_ptr_release(field, value); + Py_END_CRITICAL_SECTION(); + Py_XDECREF(old); +} + +// Publish a lazily created attribute once. Concurrent initializers may create +// separate values; only the first is installed, and the others are discarded. +// Decref outside the owner's critical section in case it runs Python code. +static inline void CPy_InitAttrRefIfNull(PyObject *owner, PyObject **field, PyObject *value) { + Py_BEGIN_CRITICAL_SECTION(owner); + if (_Py_atomic_load_ptr_relaxed(field) == NULL) { + _Py_atomic_store_ptr_release(field, value); + value = NULL; + } + Py_END_CRITICAL_SECTION(); + Py_XDECREF(value); } // Initialize a native attribute that is known to be previously undefined (NULL), @@ -158,14 +153,45 @@ // returned), and that publication carries the release barrier making all the // construction stores visible. A relaxed store therefore suffices. // -// Unlike CPy_SetAttrRef, this deliberately does NOT call SetMaybeWeakref (its CAS -// is pure overhead here, ~+2.6ns per fresh store, and construction-heavy code -// pays it on every attribute of every new object). The cost is moved off this hot -// path onto CPy_GetAttrRef's cold slow path, which sets maybe-weakref lazily on +// This deliberately does NOT call SetMaybeWeakref (its CAS is pure overhead here, +// ~+2.6ns per fresh store, and construction-heavy code pays it on every attribute +// of every new object). CPy_SetAttrRef likewise leaves the flag unset. The cost is +// moved onto CPy_GetAttrRef's cold slow path, which sets maybe-weakref lazily on // the first cross-thread read that needs it. static inline void CPy_InitAttrRef(PyObject **field, PyObject *value) { _Py_atomic_store_ptr_relaxed(field, value); } + +#endif + +// Generated generator and coroutine helpers claim this flag while executing, +// rejecting reentrant or concurrent resumes. +// +// On free-threaded builds, the atomic exchange provides mutual exclusion and acquire +// ordering; the release store publishes body writes to the next resume. This also +// permits plain access to private generator attributes. Claiming must be a single +// atomic operation, or two threads could both observe a clear flag and enter. Under +// the GIL, plain accesses suffice. +#ifdef Py_GIL_DISABLED +static inline int CPyGen_TryEnter(uint32_t *running) { + return _Py_atomic_exchange_uint32(running, 1) == 0; +} + +static inline void CPyGen_Exit(uint32_t *running) { + _Py_atomic_store_uint32_release(running, 0); +} +#else +static inline int CPyGen_TryEnter(uint32_t *running) { + if (*running) { + return 0; + } + *running = 1; + return 1; +} + +static inline void CPyGen_Exit(uint32_t *running) { + *running = 0; +} #endif PyObject* update_bases(PyObject *bases); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/librt-0.15.0/strings/librt_strings.c new/librt-0.16.0/strings/librt_strings.c --- old/librt-0.15.0/strings/librt_strings.c 2026-08-07 12:12:17.000000000 +0200 +++ new/librt-0.16.0/strings/librt_strings.c 2026-09-29 01:22:41.000000000 +0200 @@ -1358,6 +1358,8 @@ PyMODINIT_FUNC PyInit_strings(void) { - intern_strings(); + if (intern_strings() < 0) { + return NULL; + } return PyModuleDef_Init(&librt_strings_module); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/librt-0.15.0/threading/librt_threading_api.h new/librt-0.16.0/threading/librt_threading_api.h --- old/librt-0.15.0/threading/librt_threading_api.h 2026-08-07 12:12:17.000000000 +0200 +++ new/librt-0.16.0/threading/librt_threading_api.h 2026-09-29 01:22:41.000000000 +0200 @@ -1,6 +1,8 @@ #ifndef LIBRT_THREADING_API_H #define LIBRT_THREADING_API_H +#include <stdbool.h> + #include "librt_threading.h" int @@ -17,4 +19,8 @@ #define LibRTThreading_Lock_locked_internal (*(char (*)(PyObject *self)) LibRTThreading_API[6]) #define LibRTThreading_Lock_acquire_blocking_internal (*(char (*)(PyObject *self, char blocking)) LibRTThreading_API[7]) +static inline bool CPyLock_Check(PyObject *obj) { + return Py_TYPE(obj) == LibRTThreading_Lock_type_internal(); +} + #endif // LIBRT_THREADING_API_H diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/librt-0.15.0/tuple_ops.c new/librt-0.16.0/tuple_ops.c --- old/librt-0.15.0/tuple_ops.c 2026-08-07 12:12:17.000000000 +0200 +++ new/librt-0.16.0/tuple_ops.c 2026-09-29 01:22:41.000000000 +0200 @@ -5,21 +5,16 @@ #include <Python.h> #include "CPy.h" -PyObject *CPySequenceTuple_GetItem(PyObject *tuple, CPyTagged index) { +PyObject *CPySequenceTuple_GetItem_(PyObject *tuple, CPyTagged index) { if (CPyTagged_CheckShort(index)) { Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); Py_ssize_t size = PyTuple_GET_SIZE(tuple); - if (n >= 0) { - if (n >= size) { - PyErr_SetString(PyExc_IndexError, "tuple index out of range"); - return NULL; - } - } else { + if (n < 0) { n += size; - if (n < 0) { - PyErr_SetString(PyExc_IndexError, "tuple index out of range"); - return NULL; - } + } + if (n < 0 || n >= size) { + PyErr_SetString(PyExc_IndexError, "tuple index out of range"); + return NULL; } PyObject *result = PyTuple_GET_ITEM(tuple, n); Py_INCREF(result); @@ -45,18 +40,3 @@ } return CPyObject_GetSlice(obj, start, end); } - -// No error checking -PyObject *CPySequenceTuple_GetItemUnsafe(PyObject *tuple, Py_ssize_t index) -{ - PyObject *result = PyTuple_GET_ITEM(tuple, index); - Py_INCREF(result); - return result; -} - -// PyTuple_SET_ITEM does no error checking, -// and should only be used to fill in brand new tuples. -void CPySequenceTuple_SetItemUnsafe(PyObject *tuple, Py_ssize_t index, PyObject *value) -{ - PyTuple_SET_ITEM(tuple, index, value); -}
