Skip to content

Commit 9a039df

Browse files
committed
Fix potential deadlock in make_iterator_impl for Python 3.13t
Refactor pycritical_section into a unified class with internal version checks instead of using a type alias fallback. Skip locking in make_iterator_impl for Python < 3.14.0rc1 to avoid deadlock during type registration, as pycritical_section cannot release the mutex during Python callbacks without PyCriticalSection_BeginMutex.
1 parent 89b2879 commit 9a039df

File tree

2 files changed

+19
-8
lines changed

2 files changed

+19
-8
lines changed

include/pybind11/detail/internals.h

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,7 @@ using instance_map = std::unordered_multimap<const void *, instance *>;
230230
#ifdef Py_GIL_DISABLED
231231
// Wrapper around PyMutex to provide BasicLockable semantics
232232
class pymutex {
233-
# if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1
234233
friend class pycritical_section;
235-
# endif
236234
PyMutex mutex;
237235

238236
public:
@@ -241,27 +239,36 @@ class pymutex {
241239
void unlock() { PyMutex_Unlock(&mutex); }
242240
};
243241

244-
// PyCriticalSection_BeginMutex was added in Python 3.15.0a1 and backported to 3.14.0rc1
245-
# if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1
246242
class pycritical_section {
247243
pymutex &mutex;
244+
# if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1
248245
PyCriticalSection cs;
246+
# endif
249247

250248
public:
251249
explicit pycritical_section(pymutex &m) : mutex(m) {
250+
// PyCriticalSection_BeginMutex was added in Python 3.15.0a1 and backported to 3.14.0rc1
251+
# if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1
252252
PyCriticalSection_BeginMutex(&cs, &mutex.mutex);
253+
# else
254+
// Fall back to direct mutex locking for older free-threaded Python versions
255+
mutex.lock();
256+
# endif
257+
}
258+
~pycritical_section() {
259+
# if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1
260+
PyCriticalSection_End(&cs);
261+
# else
262+
mutex.unlock();
263+
# endif
253264
}
254-
~pycritical_section() { PyCriticalSection_End(&cs); }
255265

256266
// Non-copyable and non-movable to prevent double-unlock
257267
pycritical_section(const pycritical_section &) = delete;
258268
pycritical_section &operator=(const pycritical_section &) = delete;
259269
pycritical_section(pycritical_section &&) = delete;
260270
pycritical_section &operator=(pycritical_section &&) = delete;
261271
};
262-
# else
263-
using pycritical_section = std::unique_lock<pymutex>;
264-
# endif
265272

266273
// Instance map shards are used to reduce mutex contention in free-threaded Python.
267274
struct instance_map_shard {

include/pybind11/pybind11.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3173,7 +3173,11 @@ iterator make_iterator_impl(Iterator first, Sentinel last, Extra &&...extra) {
31733173
using state = detail::iterator_state<Access, Policy, Iterator, Sentinel, ValueType, Extra...>;
31743174
// TODO: state captures only the types of Extra, not the values
31753175

3176+
// For Python < 3.14.0rc1, pycritical_section uses direct mutex locking (same as a unique
3177+
// lock), which may deadlock during type registration. See detail/internals.h for details.
3178+
#if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1
31763179
PYBIND11_LOCK_INTERNALS(get_internals());
3180+
#endif
31773181
if (!detail::get_type_info(typeid(state), false)) {
31783182
class_<state>(handle(), "iterator", pybind11::module_local())
31793183
.def(

0 commit comments

Comments
 (0)