Skip to content

Commit 1469393

Browse files
gh-104614: Make Sure ob_type is Always Set Correctly by PyType_Ready() (gh-105122)
When I added the relevant condition to type_ready_set_bases() in gh-103912, I had missed that the function also sets tp_base and ob_type (if necessary). That led to problems for third-party static types. We fix that here, by making those extra operations distinct and by adjusting the condition to be more specific.
1 parent 3698fda commit 1469393

File tree

4 files changed

+128
-17
lines changed

4 files changed

+128
-17
lines changed

Lib/test/test_capi/test_misc.py

+37-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# Run the _testcapi module tests (tests for the Python/C API): by defn,
22
# these are all functions _testcapi exports whose name begins with 'test_'.
33

4-
from collections import OrderedDict
54
import _thread
5+
from collections import OrderedDict
6+
import contextlib
67
import importlib.machinery
78
import importlib.util
89
import os
@@ -1626,6 +1627,41 @@ def test_tp_mro_is_set(self):
16261627
self.assertIsNot(mro, None)
16271628

16281629

1630+
class TestStaticTypes(unittest.TestCase):
1631+
1632+
_has_run = False
1633+
1634+
@classmethod
1635+
def setUpClass(cls):
1636+
# The tests here don't play nice with our approach to refleak
1637+
# detection, so we bail out in that case.
1638+
if cls._has_run:
1639+
raise unittest.SkipTest('these tests do not support re-running')
1640+
cls._has_run = True
1641+
1642+
@contextlib.contextmanager
1643+
def basic_static_type(self, *args):
1644+
cls = _testcapi.get_basic_static_type(*args)
1645+
yield cls
1646+
1647+
def test_pytype_ready_always_sets_tp_type(self):
1648+
# The point of this test is to prevent something like
1649+
# https://github.com/python/cpython/issues/104614
1650+
# from happening again.
1651+
1652+
# First check when tp_base/tp_bases is *not* set before PyType_Ready().
1653+
with self.basic_static_type() as cls:
1654+
self.assertIs(cls.__base__, object);
1655+
self.assertEqual(cls.__bases__, (object,));
1656+
self.assertIs(type(cls), type(object));
1657+
1658+
# Then check when we *do* set tp_base/tp_bases first.
1659+
with self.basic_static_type(object) as cls:
1660+
self.assertIs(cls.__base__, object);
1661+
self.assertEqual(cls.__bases__, (object,));
1662+
self.assertIs(type(cls), type(object));
1663+
1664+
16291665
class TestThreadState(unittest.TestCase):
16301666

16311667
@threading_helper.reap_threads

Modules/_testcapimodule.c

+45
Original file line numberDiff line numberDiff line change
@@ -2640,6 +2640,50 @@ type_get_tp_mro(PyObject *self, PyObject *type)
26402640
}
26412641

26422642

2643+
/* We only use 2 in test_capi/test_misc.py. */
2644+
#define NUM_BASIC_STATIC_TYPES 2
2645+
static PyTypeObject BasicStaticTypes[NUM_BASIC_STATIC_TYPES] = {
2646+
#define INIT_BASIC_STATIC_TYPE \
2647+
{ \
2648+
PyVarObject_HEAD_INIT(NULL, 0) \
2649+
.tp_name = "BasicStaticType", \
2650+
.tp_basicsize = sizeof(PyObject), \
2651+
}
2652+
INIT_BASIC_STATIC_TYPE,
2653+
INIT_BASIC_STATIC_TYPE,
2654+
#undef INIT_BASIC_STATIC_TYPE
2655+
};
2656+
static int num_basic_static_types_used = 0;
2657+
2658+
static PyObject *
2659+
get_basic_static_type(PyObject *self, PyObject *args)
2660+
{
2661+
PyObject *base = NULL;
2662+
if (!PyArg_ParseTuple(args, "|O", &base)) {
2663+
return NULL;
2664+
}
2665+
assert(base == NULL || PyType_Check(base));
2666+
2667+
if(num_basic_static_types_used >= NUM_BASIC_STATIC_TYPES) {
2668+
PyErr_SetString(PyExc_RuntimeError, "no more available basic static types");
2669+
return NULL;
2670+
}
2671+
PyTypeObject *cls = &BasicStaticTypes[num_basic_static_types_used++];
2672+
2673+
if (base != NULL) {
2674+
cls->tp_base = (PyTypeObject *)Py_NewRef(base);
2675+
cls->tp_bases = Py_BuildValue("(O)", base);
2676+
if (cls->tp_bases == NULL) {
2677+
return NULL;
2678+
}
2679+
}
2680+
if (PyType_Ready(cls) < 0) {
2681+
return NULL;
2682+
}
2683+
return (PyObject *)cls;
2684+
}
2685+
2686+
26432687
// Test PyThreadState C API
26442688
static PyObject *
26452689
test_tstate_capi(PyObject *self, PyObject *Py_UNUSED(args))
@@ -3395,6 +3439,7 @@ static PyMethodDef TestMethods[] = {
33953439
{"type_assign_version", type_assign_version, METH_O, PyDoc_STR("PyUnstable_Type_AssignVersionTag")},
33963440
{"type_get_tp_bases", type_get_tp_bases, METH_O},
33973441
{"type_get_tp_mro", type_get_tp_mro, METH_O},
3442+
{"get_basic_static_type", get_basic_static_type, METH_VARARGS, NULL},
33983443
{"test_tstate_capi", test_tstate_capi, METH_NOARGS, NULL},
33993444
{"frame_getlocals", frame_getlocals, METH_O, NULL},
34003445
{"frame_getglobals", frame_getglobals, METH_O, NULL},

Objects/typeobject.c

+44-16
Original file line numberDiff line numberDiff line change
@@ -306,11 +306,14 @@ clear_tp_bases(PyTypeObject *self)
306306
{
307307
if (self->tp_flags & _Py_TPFLAGS_STATIC_BUILTIN) {
308308
if (_Py_IsMainInterpreter(_PyInterpreterState_GET())) {
309-
if (self->tp_bases != NULL
310-
&& PyTuple_GET_SIZE(self->tp_bases) > 0)
311-
{
312-
assert(_Py_IsImmortal(self->tp_bases));
313-
_Py_ClearImmortal(self->tp_bases);
309+
if (self->tp_bases != NULL) {
310+
if (PyTuple_GET_SIZE(self->tp_bases) == 0) {
311+
Py_CLEAR(self->tp_bases);
312+
}
313+
else {
314+
assert(_Py_IsImmortal(self->tp_bases));
315+
_Py_ClearImmortal(self->tp_bases);
316+
}
314317
}
315318
}
316319
return;
@@ -352,11 +355,14 @@ clear_tp_mro(PyTypeObject *self)
352355
{
353356
if (self->tp_flags & _Py_TPFLAGS_STATIC_BUILTIN) {
354357
if (_Py_IsMainInterpreter(_PyInterpreterState_GET())) {
355-
if (self->tp_mro != NULL
356-
&& PyTuple_GET_SIZE(self->tp_mro) > 0)
357-
{
358-
assert(_Py_IsImmortal(self->tp_mro));
359-
_Py_ClearImmortal(self->tp_mro);
358+
if (self->tp_mro != NULL) {
359+
if (PyTuple_GET_SIZE(self->tp_mro) == 0) {
360+
Py_CLEAR(self->tp_mro);
361+
}
362+
else {
363+
assert(_Py_IsImmortal(self->tp_mro));
364+
_Py_ClearImmortal(self->tp_mro);
365+
}
360366
}
361367
}
362368
return;
@@ -6996,12 +7002,8 @@ type_ready_pre_checks(PyTypeObject *type)
69967002

69977003

69987004
static int
6999-
type_ready_set_bases(PyTypeObject *type)
7005+
type_ready_set_base(PyTypeObject *type)
70007006
{
7001-
if (lookup_tp_bases(type) != NULL) {
7002-
return 0;
7003-
}
7004-
70057007
/* Initialize tp_base (defaults to BaseObject unless that's us) */
70067008
PyTypeObject *base = type->tp_base;
70077009
if (base == NULL && type != &PyBaseObject_Type) {
@@ -7025,18 +7027,38 @@ type_ready_set_bases(PyTypeObject *type)
70257027
}
70267028
}
70277029

7030+
return 0;
7031+
}
7032+
7033+
static int
7034+
type_ready_set_type(PyTypeObject *type)
7035+
{
70287036
/* Initialize ob_type if NULL. This means extensions that want to be
70297037
compilable separately on Windows can call PyType_Ready() instead of
70307038
initializing the ob_type field of their type objects. */
70317039
/* The test for base != NULL is really unnecessary, since base is only
70327040
NULL when type is &PyBaseObject_Type, and we know its ob_type is
70337041
not NULL (it's initialized to &PyType_Type). But coverity doesn't
70347042
know that. */
7043+
PyTypeObject *base = type->tp_base;
70357044
if (Py_IS_TYPE(type, NULL) && base != NULL) {
70367045
Py_SET_TYPE(type, Py_TYPE(base));
70377046
}
70387047

7039-
/* Initialize tp_bases */
7048+
return 0;
7049+
}
7050+
7051+
static int
7052+
type_ready_set_bases(PyTypeObject *type)
7053+
{
7054+
if (type->tp_flags & _Py_TPFLAGS_STATIC_BUILTIN) {
7055+
if (!_Py_IsMainInterpreter(_PyInterpreterState_GET())) {
7056+
assert(lookup_tp_bases(type) != NULL);
7057+
return 0;
7058+
}
7059+
assert(lookup_tp_bases(type) == NULL);
7060+
}
7061+
70407062
PyObject *bases = lookup_tp_bases(type);
70417063
if (bases == NULL) {
70427064
PyTypeObject *base = type->tp_base;
@@ -7446,6 +7468,12 @@ type_ready(PyTypeObject *type, int rerunbuiltin)
74467468
if (type_ready_set_dict(type) < 0) {
74477469
goto error;
74487470
}
7471+
if (type_ready_set_base(type) < 0) {
7472+
goto error;
7473+
}
7474+
if (type_ready_set_type(type) < 0) {
7475+
goto error;
7476+
}
74497477
if (type_ready_set_bases(type) < 0) {
74507478
goto error;
74517479
}

Tools/c-analyzer/cpython/ignored.tsv

+2
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,8 @@ Modules/_testcapi/watchers.c - num_code_object_destroyed_events -
420420
Modules/_testcapi/watchers.c - pyfunc_watchers -
421421
Modules/_testcapi/watchers.c - func_watcher_ids -
422422
Modules/_testcapi/watchers.c - func_watcher_callbacks -
423+
Modules/_testcapimodule.c - BasicStaticTypes -
424+
Modules/_testcapimodule.c - num_basic_static_types_used -
423425
Modules/_testcapimodule.c - ContainerNoGC_members -
424426
Modules/_testcapimodule.c - ContainerNoGC_type -
425427
Modules/_testcapimodule.c - FmData -

0 commit comments

Comments
 (0)