Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-104614: Make Sure ob_type is Always Set Correctly by PyType_Ready() #105122

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion Lib/test/test_capi/test_misc.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Run the _testcapi module tests (tests for the Python/C API): by defn,
# these are all functions _testcapi exports whose name begins with 'test_'.

from collections import OrderedDict
import _thread
from collections import OrderedDict
import contextlib
import importlib.machinery
import importlib.util
import os
Expand Down Expand Up @@ -1626,6 +1627,41 @@ def test_tp_mro_is_set(self):
self.assertIsNot(mro, None)


class TestStaticTypes(unittest.TestCase):

_has_run = False

@classmethod
def setUpClass(cls):
# The tests here don't play nice with our approach to refleak
# detection, so we bail out in that case.
if cls._has_run:
raise unittest.SkipTest('these tests do not support re-running')
cls._has_run = True
Comment on lines +1634 to +1640
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is nice. IMO, we should use a similar approach in test_import instead of the hack suggested in gh-104796.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps also gh-105085 should have been solved like this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm glad you appreciate the solution 😄, but actually am a bit torn about this. On the one hand, it gets us what we want in a simple, clear way (IMHO). On the other hand, this approach means there may be code paths that don't get checked for ref leaks, which we'd want to avoid.

If we use this pattern elsewhere, we'll need to be vigilant about making sure we don't miss any ref leak coverage. I'm not sure how easy that would be generally, though it's unlikely that there are more than a handful of tests where we'd want to skip refleak detection. FWIW, in this case I don't think ref leaks are much of a concern.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should use a similar approach

Wouldn't this skip the whole class Test instead of a smaller def test_...?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, so you make sure to collect all your "ref leak unsafe" tests in a single unittest class.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, create a decorator for it.


@contextlib.contextmanager
def basic_static_type(self, *args):
cls = _testcapi.get_basic_static_type(*args)
yield cls

def test_pytype_ready_always_sets_tp_type(self):
# The point of this test is to prevent something like
# https://github.com/python/cpython/issues/104614
# from happening again.

# First check when tp_base/tp_bases is *not* set before PyType_Ready().
with self.basic_static_type() as cls:
self.assertIs(cls.__base__, object);
self.assertEqual(cls.__bases__, (object,));
self.assertIs(type(cls), type(object));

# Then check when we *do* set tp_base/tp_bases first.
with self.basic_static_type(object) as cls:
self.assertIs(cls.__base__, object);
self.assertEqual(cls.__bases__, (object,));
self.assertIs(type(cls), type(object));


class TestThreadState(unittest.TestCase):

@threading_helper.reap_threads
Expand Down
45 changes: 45 additions & 0 deletions Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2627,6 +2627,50 @@ type_get_tp_mro(PyObject *self, PyObject *type)
}


/* We only use 2 in test_capi/test_misc.py. */
#define NUM_BASIC_STATIC_TYPES 2
static PyTypeObject BasicStaticTypes[NUM_BASIC_STATIC_TYPES] = {
#define INIT_BASIC_STATIC_TYPE \
{ \
PyVarObject_HEAD_INIT(NULL, 0) \
.tp_name = "BasicStaticType", \
.tp_basicsize = sizeof(PyObject), \
}
INIT_BASIC_STATIC_TYPE,
INIT_BASIC_STATIC_TYPE,
#undef INIT_BASIC_STATIC_TYPE
};
static int num_basic_static_types_used = 0;

static PyObject *
get_basic_static_type(PyObject *self, PyObject *args)
{
PyObject *base = NULL;
if (!PyArg_ParseTuple(args, "|O", &base)) {
return NULL;
}
assert(base == NULL || PyType_Check(base));

if(num_basic_static_types_used >= NUM_BASIC_STATIC_TYPES) {
PyErr_SetString(PyExc_RuntimeError, "no more available basic static types");
return NULL;
}
PyTypeObject *cls = &BasicStaticTypes[num_basic_static_types_used++];

if (base != NULL) {
cls->tp_base = (PyTypeObject *)Py_NewRef(base);
cls->tp_bases = Py_BuildValue("(O)", base);
if (cls->tp_bases == NULL) {
return NULL;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should decref tp_base and tp_bases before returning.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened gh-105225.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for following up after-the-fact!

}
}
if (PyType_Ready(cls) < 0) {
return NULL;
}
return (PyObject *)cls;
}


// Test PyThreadState C API
static PyObject *
test_tstate_capi(PyObject *self, PyObject *Py_UNUSED(args))
Expand Down Expand Up @@ -3384,6 +3428,7 @@ static PyMethodDef TestMethods[] = {
{"type_assign_version", type_assign_version, METH_O, PyDoc_STR("PyUnstable_Type_AssignVersionTag")},
{"type_get_tp_bases", type_get_tp_bases, METH_O},
{"type_get_tp_mro", type_get_tp_mro, METH_O},
{"get_basic_static_type", get_basic_static_type, METH_VARARGS, NULL},
{"test_tstate_capi", test_tstate_capi, METH_NOARGS, NULL},
{"frame_getlocals", frame_getlocals, METH_O, NULL},
{"frame_getglobals", frame_getglobals, METH_O, NULL},
Expand Down
60 changes: 44 additions & 16 deletions Objects/typeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -306,11 +306,14 @@ clear_tp_bases(PyTypeObject *self)
{
if (self->tp_flags & _Py_TPFLAGS_STATIC_BUILTIN) {
if (_Py_IsMainInterpreter(_PyInterpreterState_GET())) {
if (self->tp_bases != NULL
&& PyTuple_GET_SIZE(self->tp_bases) > 0)
{
assert(_Py_IsImmortal(self->tp_bases));
_Py_ClearImmortal(self->tp_bases);
if (self->tp_bases != NULL) {
if (PyTuple_GET_SIZE(self->tp_bases) == 0) {
Py_CLEAR(self->tp_bases);
}
else {
assert(_Py_IsImmortal(self->tp_bases));
_Py_ClearImmortal(self->tp_bases);
}
}
}
return;
Expand Down Expand Up @@ -352,11 +355,14 @@ clear_tp_mro(PyTypeObject *self)
{
if (self->tp_flags & _Py_TPFLAGS_STATIC_BUILTIN) {
if (_Py_IsMainInterpreter(_PyInterpreterState_GET())) {
if (self->tp_mro != NULL
&& PyTuple_GET_SIZE(self->tp_mro) > 0)
{
assert(_Py_IsImmortal(self->tp_mro));
_Py_ClearImmortal(self->tp_mro);
if (self->tp_mro != NULL) {
if (PyTuple_GET_SIZE(self->tp_mro) == 0) {
Py_CLEAR(self->tp_mro);
}
else {
assert(_Py_IsImmortal(self->tp_mro));
_Py_ClearImmortal(self->tp_mro);
}
}
}
return;
Expand Down Expand Up @@ -6996,12 +7002,8 @@ type_ready_pre_checks(PyTypeObject *type)


static int
type_ready_set_bases(PyTypeObject *type)
type_ready_set_base(PyTypeObject *type)
{
if (lookup_tp_bases(type) != NULL) {
return 0;
}

/* Initialize tp_base (defaults to BaseObject unless that's us) */
PyTypeObject *base = type->tp_base;
if (base == NULL && type != &PyBaseObject_Type) {
Expand All @@ -7025,18 +7027,38 @@ type_ready_set_bases(PyTypeObject *type)
}
}

return 0;
}

static int
type_ready_set_type(PyTypeObject *type)
{
/* Initialize ob_type if NULL. This means extensions that want to be
compilable separately on Windows can call PyType_Ready() instead of
initializing the ob_type field of their type objects. */
/* The test for base != NULL is really unnecessary, since base is only
NULL when type is &PyBaseObject_Type, and we know its ob_type is
not NULL (it's initialized to &PyType_Type). But coverity doesn't
know that. */
PyTypeObject *base = type->tp_base;
if (Py_IS_TYPE(type, NULL) && base != NULL) {
Py_SET_TYPE(type, Py_TYPE(base));
}

/* Initialize tp_bases */
return 0;
}

static int
type_ready_set_bases(PyTypeObject *type)
{
if (type->tp_flags & _Py_TPFLAGS_STATIC_BUILTIN) {
if (!_Py_IsMainInterpreter(_PyInterpreterState_GET())) {
assert(lookup_tp_bases(type) != NULL);
return 0;
}
assert(lookup_tp_bases(type) == NULL);
}

PyObject *bases = lookup_tp_bases(type);
if (bases == NULL) {
PyTypeObject *base = type->tp_base;
Expand Down Expand Up @@ -7446,6 +7468,12 @@ type_ready(PyTypeObject *type, int rerunbuiltin)
if (type_ready_set_dict(type) < 0) {
goto error;
}
if (type_ready_set_base(type) < 0) {
goto error;
}
if (type_ready_set_type(type) < 0) {
goto error;
}
if (type_ready_set_bases(type) < 0) {
goto error;
}
Expand Down
2 changes: 2 additions & 0 deletions Tools/c-analyzer/cpython/ignored.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,8 @@ Modules/_testcapi/watchers.c - num_code_object_destroyed_events -
Modules/_testcapi/watchers.c - pyfunc_watchers -
Modules/_testcapi/watchers.c - func_watcher_ids -
Modules/_testcapi/watchers.c - func_watcher_callbacks -
Modules/_testcapimodule.c - BasicStaticTypes -
Modules/_testcapimodule.c - num_basic_static_types_used -
Modules/_testcapimodule.c - ContainerNoGC_members -
Modules/_testcapimodule.c - ContainerNoGC_type -
Modules/_testcapimodule.c - FmData -
Expand Down