@@ -142,6 +142,26 @@ PyEventLoop PyEventLoop::_loopNotFound() {
142142PyEventLoop PyEventLoop::_getLoopOnThread (PyThreadState *tstate) {
143143 // Modified from Python 3.9 `get_running_loop` https://github.com/python/cpython/blob/7cb3a44/Modules/_asynciomodule.c#L241-L278
144144
145+ // For the current thread, ask asyncio itself instead of reading CPython's private thread-state layout,
146+ // which differs between patch releases (e.g. 3.14.3 vs 3.14.4).
147+ if (tstate == PyThreadState_Get ()) {
148+ PyObject *asyncio = PyImport_ImportModule (" asyncio" );
149+ if (asyncio == NULL ) {
150+ return _loopNotFound ();
151+ }
152+ PyObject *loop = PyObject_CallMethod (asyncio, " _get_running_loop" , NULL );
153+ Py_DECREF (asyncio);
154+ if (loop == NULL ) {
155+ PyErr_Clear ();
156+ return _loopNotFound ();
157+ }
158+ if (loop == Py_None) {
159+ Py_DECREF (loop);
160+ return _loopNotFound ();
161+ }
162+ return PyEventLoop (loop); // takes the new reference
163+ }
164+
145165 PyObject *ts_dict;
146166 #if PY_VERSION_HEX >= 0x030d0000 // Python version is greater than 3.13
147167 // The private `_PyThreadState_GetDict(tstate)` API gets removed in Python 3.13.
@@ -204,6 +224,14 @@ PyEventLoop PyEventLoop::_getLoopOnThread(PyThreadState *tstate) {
204224 // (we only need the first few fields of `_PyThreadStateImpl`)
205225 };
206226
227+ #if PY_VERSION_HEX >= 0x030e0000 && PY_VERSION_HEX < 0x030f0000
228+ // Python 3.14.4 added a pointer field to the public `PyThreadState`, shifting the offsets of the private fields.
229+ // Refuse to read another thread's state when the runtime and build-time layouts differ.
230+ if ((PY_VERSION_HEX >= 0x030e0400 ) != (Py_Version >= 0x030e0400 )) {
231+ return _loopNotFound ();
232+ }
233+ #endif
234+
207235 // Modified from https://github.com/python/cpython/blob/v3.13.0rc1/Modules/_asynciomodule.c#L3205-L3210
208236 PyObject *loop = ((PyThreadStateHolder *)tstate)->asyncio_running_loop ;
209237 if (loop == NULL ) {
0 commit comments