Skip to content

Commit 972be3a

Browse files
authored
[mypyc] Fix crash and reference leaks on base resolution failures (#22244)
`update_bases` returns `NULL` on error but the result was not checked so the null pointer could be passed to a function that doesn't expect it and crash. Add a null check and missing decrefs in `update_bases` on failures. When creating a type object from a template, the `tp_doc` pointer was allocated twice, so keep just one allocation.
1 parent 6ff2361 commit 972be3a

3 files changed

Lines changed: 100 additions & 35 deletions

File tree

‎mypyc/lib-rt/misc_ops.c‎

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,8 @@ PyObject *CPyType_FromTemplate(PyObject *template,
230230

231231
if (orig_bases) {
232232
bases = update_bases(orig_bases);
233+
if (bases == NULL)
234+
goto error;
233235
// update_bases doesn't increment the refcount if nothing changes,
234236
// so we do it to make sure we have distinct "references" to both
235237
if (bases == orig_bases)
@@ -252,17 +254,6 @@ PyObject *CPyType_FromTemplate(PyObject *template,
252254
if (!name)
253255
goto error;
254256

255-
if (template_->tp_doc) {
256-
// cpython expects tp_doc to be heap-allocated so convert it here to
257-
// avoid segfaults on deallocation.
258-
Py_ssize_t size = strlen(template_->tp_doc) + 1;
259-
char *doc = (char *)PyMem_Malloc(size);
260-
if (!doc)
261-
goto error;
262-
memcpy(doc, template_->tp_doc, size);
263-
template_->tp_doc = doc;
264-
}
265-
266257
// Allocate the type and then copy the main stuff in.
267258
t = (PyHeapTypeObject*)PyType_GenericAlloc(&PyType_Type, 0);
268259
if (!t)
@@ -271,6 +262,21 @@ PyObject *CPyType_FromTemplate(PyObject *template,
271262
(char *)template_ + sizeof(PyVarObject),
272263
sizeof(PyTypeObject) - sizeof(PyVarObject));
273264

265+
t->ht_type.tp_doc = NULL;
266+
// Unlike the tp_doc slots of most other object, a heap type's tp_doc
267+
// must be heap allocated.
268+
if (template_->tp_doc) {
269+
// Silently truncate the docstring if it contains a null byte
270+
size_t size = strlen(template_->tp_doc) + 1;
271+
char *doc = (char *)PyMem_Malloc(size);
272+
if (doc == NULL) {
273+
PyErr_NoMemory();
274+
goto error;
275+
}
276+
memcpy(doc, template_->tp_doc, size);
277+
t->ht_type.tp_doc = doc;
278+
}
279+
274280
if (bases != orig_bases) {
275281
if (PyObject_SetAttr((PyObject *)t, mypyc_interned_str.__orig_bases__, orig_bases) < 0)
276282
goto error;
@@ -333,21 +339,6 @@ PyObject *CPyType_FromTemplate(PyObject *template,
333339

334340
Py_XDECREF(dummy_class);
335341

336-
// Unlike the tp_doc slots of most other object, a heap type's tp_doc
337-
// must be heap allocated.
338-
if (template_->tp_doc) {
339-
// Silently truncate the docstring if it contains a null byte
340-
Py_ssize_t size = strlen(template_->tp_doc) + 1;
341-
char *tp_doc = (char *)PyMem_Malloc(size);
342-
if (tp_doc == NULL) {
343-
PyErr_NoMemory();
344-
goto error;
345-
}
346-
347-
memcpy(tp_doc, template_->tp_doc, size);
348-
t->ht_type.tp_doc = tp_doc;
349-
}
350-
351342
#if PY_MINOR_VERSION == 11
352343
// This is a hack. Python 3.11 doesn't include good public APIs to work with managed
353344
// dicts, which are the default for heap types. So we try to opt-out until Python 3.12.

‎mypyc/lib-rt/pythonsupport.c‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ update_bases(PyObject *bases)
130130
/* If this is a first successful replacement, create new_bases list and
131131
copy previously encountered bases. */
132132
if (!(new_bases = PyList_New(i))) {
133+
Py_DECREF(new_base);
133134
goto error;
134135
}
135136
for (j = 0; j < i; j++) {
@@ -140,6 +141,7 @@ update_bases(PyObject *bases)
140141
}
141142
j = PyList_GET_SIZE(new_bases);
142143
if (PyList_SetSlice(new_bases, j, j, new_base) < 0) {
144+
Py_DECREF(new_base);
143145
goto error;
144146
}
145147
Py_DECREF(new_base);

‎mypyc/test-data/run-classes.test‎

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2272,29 +2272,101 @@ native.test()
22722272
ok
22732273

22742274
[case testNativeClassHiddenMetaclass]
2275-
# The stub hides the metaclass of the base class, so it's only detected at runtime
2276-
from hidden import Base
2275+
# The stub hides the base classes' metaclass and base resolution behavior.
2276+
from hidden import Prefix, Base, Second
22772277

2278-
class C(Base):
2278+
class C(Prefix, Base, Second):
22792279
pass
22802280

22812281
[file hidden.pyi]
2282+
class Prefix: ...
22822283
class Base: ...
2284+
class Second: ...
22832285

22842286
[file hidden.py]
2287+
class Prefix:
2288+
pass
2289+
22852290
class Meta(type):
22862291
pass
22872292

22882293
class Base(metaclass=Meta):
22892294
pass
22902295

2296+
class Second:
2297+
pass
2298+
2299+
class SlotsBase:
2300+
__slots__ = ("attribute",)
2301+
2302+
# Resolution fails before type creation, so a sentinel can stand in for a base.
2303+
# Unlike types, its refcount is reliable on free-threaded Python too.
2304+
Resolved = object()
2305+
2306+
class Proxy:
2307+
def __init__(self, result):
2308+
self.result = result
2309+
self.failure = None
2310+
2311+
@property
2312+
def __mro_entries__(self):
2313+
if self.failure == "lookup":
2314+
raise RuntimeError("base lookup failed")
2315+
return self.resolve
2316+
2317+
def resolve(self, bases):
2318+
if self.failure == "call":
2319+
raise RuntimeError("base resolution failed")
2320+
return self.result
2321+
22912322
[file driver.py]
2292-
try:
2293-
import native
2294-
except TypeError as e:
2295-
assert str(e) == "mypyc classes can't have a metaclass"
2296-
else:
2297-
assert False
2323+
import gc
2324+
import sys
2325+
import hidden
2326+
from testutil import assertRaises
2327+
2328+
for _ in range(3):
2329+
with assertRaises(TypeError, "mypyc classes can't have a metaclass"):
2330+
import native
2331+
gc.collect()
2332+
2333+
hidden.Base = hidden.SlotsBase
2334+
for _ in range(3):
2335+
with assertRaises(TypeError, "mypyc classes can't have __slots__"):
2336+
import native
2337+
gc.collect()
2338+
2339+
second_base = hidden.Second
2340+
hidden.Base = hidden.Proxy((hidden.Resolved,))
2341+
hidden.Second = hidden.Proxy([])
2342+
# Base creates a replacement list containing Prefix and Resolved before
2343+
# Second fails. Neither that list nor a non-tuple result may leak references.
2344+
tracked = (hidden.Resolved, hidden.Base, hidden.Second,
2345+
hidden.Base.result, hidden.Second.result)
2346+
for failure, error, message in [
2347+
("lookup", RuntimeError, "base lookup failed"),
2348+
("call", RuntimeError, "base resolution failed"),
2349+
(None, TypeError, "__mro_entries__ must return a tuple"),
2350+
]:
2351+
hidden.Second.failure = failure
2352+
gc.collect()
2353+
before = [sys.getrefcount(obj) for obj in tracked]
2354+
for _ in range(3):
2355+
with assertRaises(error, message):
2356+
import native
2357+
gc.collect()
2358+
after = [sys.getrefcount(obj) for obj in tracked]
2359+
assert after == before, (failure, before, after)
2360+
2361+
# Keep the base layout but permit the next initialization to complete.
2362+
hidden.Base = hidden.SlotsBase
2363+
hidden.Base.__slots__ = ()
2364+
hidden.Second = second_base
2365+
import native
2366+
# The template's tp_doc contains only the generated constructor signature.
2367+
# CPython extracts it into __text_signature__, leaving __doc__ empty.
2368+
assert native.C.__doc__ == ""
2369+
assert native.C.__text_signature__ == "()"
22982370

22992371
[case testMetaclass]
23002372
from meta import Meta

0 commit comments

Comments
 (0)