Skip to content

Commit 32b2c5d

Browse files
authored
gh-137956: Guard against non-free-threaded extensions in free-threaded builds (GH-137957)
1 parent aa9ceb1 commit 32b2c5d

File tree

3 files changed

+58
-0
lines changed

3 files changed

+58
-0
lines changed

Include/object.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ whose size is determined when the object is allocated.
7171
*
7272
* Statically allocated objects might be shared between
7373
* interpreters, so must be marked as immortal.
74+
*
75+
* Before changing this, see the check in PyModuleDef_Init().
7476
*/
7577
#if defined(Py_GIL_DISABLED)
7678
#define PyObject_HEAD_INIT(type) \
@@ -634,6 +636,7 @@ given type object has a specified feature.
634636

635637
// Flag values for ob_flags (16 bits available, if SIZEOF_VOID_P > 4).
636638
#define _Py_IMMORTAL_FLAGS (1 << 0)
639+
#define _Py_LEGACY_ABI_CHECK_FLAG (1 << 1) /* see PyModuleDef_Init() */
637640
#define _Py_STATICALLY_ALLOCATED_FLAG (1 << 2)
638641
#if defined(Py_GIL_DISABLED) && defined(Py_DEBUG)
639642
#define _Py_TYPE_REVEALED_FLAG (1 << 3)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Display and raise an exception if an extension compiled for
2+
non-free-threaded Python is loaded in a free-threaded interpreter.

Objects/moduleobject.c

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,59 @@ _PyModule_IsExtension(PyObject *obj)
5252
PyObject*
5353
PyModuleDef_Init(PyModuleDef* def)
5454
{
55+
#ifdef Py_GIL_DISABLED
56+
// Check that this def does not come from a non-free-threading ABI.
57+
//
58+
// This is meant as a "sanity check"; users should never rely on it.
59+
// In particular, if we run out of ob_flags bits, or otherwise need to
60+
// change some of the internals, this check can go away. Still, it
61+
// would be nice to keep it for the free-threading transition.
62+
//
63+
// A PyModuleDef must be initialized with PyModuleDef_HEAD_INIT,
64+
// which (via PyObject_HEAD_INIT) sets _Py_STATICALLY_ALLOCATED_FLAG
65+
// and not _Py_LEGACY_ABI_CHECK_FLAG. For PyModuleDef, these flags never
66+
// change.
67+
// This means that the lower nibble of a valid PyModuleDef's ob_flags is
68+
// always `_10_` (in binary; `_` is don't care).
69+
//
70+
// So, a check for these bits won't reject valid PyModuleDef.
71+
// Rejecting incompatible extensions is slightly less important; here's
72+
// how that works:
73+
//
74+
// In the pre-free-threading stable ABI, PyModuleDef_HEAD_INIT is big
75+
// enough to overlap with free-threading ABI's ob_flags, is all zeros
76+
// except for the refcount field.
77+
// The refcount field can be:
78+
// - 1 (3.11 and below)
79+
// - UINT_MAX >> 2 (32-bit 3.12 & 3.13)
80+
// - UINT_MAX (64-bit 3.12 & 3.13)
81+
// - 7L << 28 (3.14)
82+
//
83+
// This means that the lower nibble of *any byte* in PyModuleDef_HEAD_INIT
84+
// is not `_10_` -- it can be:
85+
// - 0b0000
86+
// - 0b0001
87+
// - 0b0011 (from UINT_MAX >> 2)
88+
// - 0b0111 (from 7L << 28)
89+
// - 0b1111 (e.g. from UINT_MAX)
90+
// (The values may change at runtime as the PyModuleDef is used, but
91+
// PyModuleDef_Init is required before using the def as a Python object,
92+
// so we check at least once with the initial values.
93+
uint16_t flags = ((PyObject*)def)->ob_flags;
94+
uint16_t bits = _Py_STATICALLY_ALLOCATED_FLAG | _Py_LEGACY_ABI_CHECK_FLAG;
95+
if ((flags & bits) != _Py_STATICALLY_ALLOCATED_FLAG) {
96+
const char *message = "invalid PyModuleDef, extension possibly "
97+
"compiled for non-free-threaded Python";
98+
// Write the error as unraisable: if the extension tries calling
99+
// any API, it's likely to segfault and lose the exception.
100+
PyErr_SetString(PyExc_SystemError, message);
101+
PyErr_WriteUnraisable(NULL);
102+
// But also raise the exception normally -- this is technically
103+
// a recoverable state.
104+
PyErr_SetString(PyExc_SystemError, message);
105+
return NULL;
106+
}
107+
#endif
55108
assert(PyModuleDef_Type.tp_flags & Py_TPFLAGS_READY);
56109
if (def->m_base.m_index == 0) {
57110
Py_SET_REFCNT(def, 1);

0 commit comments

Comments
 (0)