From 3bb09947ec4837de75532e21dd4bd25db0a1f1b7 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 30 Apr 2021 12:46:15 +0200 Subject: bpo-43916: Add Py_TPFLAGS_DISALLOW_INSTANTIATION type flag (GH-25721) Add a new Py_TPFLAGS_DISALLOW_INSTANTIATION type flag to disallow creating type instances: set tp_new to NULL and don't create the "__new__" key in the type dictionary. The flag is set automatically on static types if tp_base is NULL or &PyBaseObject_Type and tp_new is NULL. Use the flag on the following types: * _curses.ncurses_version type * _curses_panel.panel * _tkinter.Tcl_Obj * _tkinter.tkapp * _tkinter.tktimertoken * _xxsubinterpretersmodule.ChannelID * sys.flags type * sys.getwindowsversion() type * sys.version_info type Update MyStr example in the C API documentation to use Py_TPFLAGS_DISALLOW_INSTANTIATION. Add _PyStructSequence_InitType() function to create a structseq type with the Py_TPFLAGS_DISALLOW_INSTANTIATION flag set. type_new() calls _PyType_CheckConsistency() at exit. --- Doc/c-api/typeobj.rst | 28 +++++- Doc/whatsnew/3.10.rst | 4 + Include/object.h | 4 + Include/structseq.h | 6 ++ Lib/test/test_sys.py | 5 +- .../C API/2021-04-29-17-35-48.bpo-43916.wvWt23.rst | 2 + Modules/_curses_panel.c | 3 +- Modules/_cursesmodule.c | 15 +--- Modules/_tkinter.c | 9 +- Modules/_xxsubinterpretersmodule.c | 20 ++--- Objects/structseq.c | 12 ++- Objects/typeobject.c | 99 +++++++++++++++------- Python/sysmodule.c | 39 ++------- 13 files changed, 144 insertions(+), 102 deletions(-) create mode 100644 Misc/NEWS.d/next/C API/2021-04-29-17-35-48.bpo-43916.wvWt23.rst diff --git a/Doc/c-api/typeobj.rst b/Doc/c-api/typeobj.rst index 85f0262..5e731bd 100644 --- a/Doc/c-api/typeobj.rst +++ b/Doc/c-api/typeobj.rst @@ -1199,6 +1199,25 @@ and :c:type:`PyType_Type` effectively act as defaults.) .. versionadded:: 3.10 + .. data:: Py_TPFLAGS_DISALLOW_INSTANTIATION + + Disallow creating instances of the type: set + :c:member:`~PyTypeObject.tp_new` to NULL and don't create the ``__new__`` + key in the type dictionary. + + The flag must be set before creating the type, not after. For example, it + must be set before :c:func:`PyType_Ready` is called on the type. + + The flag is set automatically on :ref:`static types ` if + :c:member:`~PyTypeObject.tp_base` is NULL or ``&PyBaseObject_Type`` and + :c:member:`~PyTypeObject.tp_new` is NULL. + + **Inheritance:** + + This flag is not inherited. + + .. versionadded:: 3.10 + .. c:member:: const char* PyTypeObject.tp_doc @@ -1761,6 +1780,9 @@ and :c:type:`PyType_Type` effectively act as defaults.) in :c:member:`~PyTypeObject.tp_new`, while for mutable types, most initialization should be deferred to :c:member:`~PyTypeObject.tp_init`. + Set the :const:`Py_TPFLAGS_DISALLOW_INSTANTIATION` flag to disallow creating + instances of the type in Python. + **Inheritance:** This field is inherited by subtypes, except it is not inherited by @@ -2596,7 +2618,8 @@ A type that supports weakrefs, instance dicts, and hashing:: }; A str subclass that cannot be subclassed and cannot be called -to create instances (e.g. uses a separate factory func):: +to create instances (e.g. uses a separate factory func) using +:c:data:`Py_TPFLAGS_DISALLOW_INSTANTIATION` flag:: typedef struct { PyUnicodeObject raw; @@ -2609,8 +2632,7 @@ to create instances (e.g. uses a separate factory func):: .tp_basicsize = sizeof(MyStr), .tp_base = NULL, // set to &PyUnicode_Type in module init .tp_doc = "my custom str", - .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_new = NULL, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION, .tp_repr = (reprfunc)myobj_repr, }; diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index 2580a03..1a39066 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -1708,6 +1708,10 @@ New Features These functions allow to activate, deactivate and query the state of the garbage collector from C code without having to import the :mod:`gc` module. +* Add a new :c:data:`Py_TPFLAGS_DISALLOW_INSTANTIATION` type flag to disallow + creating type instances. + (Contributed by Victor Stinner in :issue:`43916`.) + Porting to Python 3.10 ---------------------- diff --git a/Include/object.h b/Include/object.h index cffe72c..4c06999 100644 --- a/Include/object.h +++ b/Include/object.h @@ -327,6 +327,10 @@ given type object has a specified feature. #define Py_TPFLAGS_MAPPING (1 << 6) #endif +/* Disallow creating instances of the type: set tp_new to NULL and don't create + * the "__new__" key in the type dictionary. */ +#define Py_TPFLAGS_DISALLOW_INSTANTIATION (1UL << 7) + /* Set if the type object is immutable: type attributes cannot be set nor deleted */ #define Py_TPFLAGS_IMMUTABLETYPE (1UL << 8) diff --git a/Include/structseq.h b/Include/structseq.h index 8f51c89..af3af41 100644 --- a/Include/structseq.h +++ b/Include/structseq.h @@ -27,6 +27,12 @@ PyAPI_FUNC(void) PyStructSequence_InitType(PyTypeObject *type, PyAPI_FUNC(int) PyStructSequence_InitType2(PyTypeObject *type, PyStructSequence_Desc *desc); #endif +#ifdef Py_BUILD_CORE +extern int _PyStructSequence_InitType( + PyTypeObject *type, + PyStructSequence_Desc *desc, + unsigned long tp_flags); +#endif PyAPI_FUNC(PyTypeObject*) PyStructSequence_NewType(PyStructSequence_Desc *desc); PyAPI_FUNC(PyObject *) PyStructSequence_New(PyTypeObject* type); diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index ee39375..1fd5247 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -605,11 +605,12 @@ class SysModuleTest(unittest.TestCase): def assert_raise_on_new_sys_type(self, sys_attr): # Users are intentionally prevented from creating new instances of # sys.flags, sys.version_info, and sys.getwindowsversion. + arg = sys_attr attr_type = type(sys_attr) with self.assertRaises(TypeError): - attr_type() + attr_type(arg) with self.assertRaises(TypeError): - attr_type.__new__(attr_type) + attr_type.__new__(attr_type, arg) def test_sys_flags_no_instantiation(self): self.assert_raise_on_new_sys_type(sys.flags) diff --git a/Misc/NEWS.d/next/C API/2021-04-29-17-35-48.bpo-43916.wvWt23.rst b/Misc/NEWS.d/next/C API/2021-04-29-17-35-48.bpo-43916.wvWt23.rst new file mode 100644 index 0000000..0cec810 --- /dev/null +++ b/Misc/NEWS.d/next/C API/2021-04-29-17-35-48.bpo-43916.wvWt23.rst @@ -0,0 +1,2 @@ +Add a new :c:data:`Py_TPFLAGS_DISALLOW_INSTANTIATION` type flag to disallow +creating type instances. Patch by Victor Stinner. diff --git a/Modules/_curses_panel.c b/Modules/_curses_panel.c index 94caf8c..0b328f9 100644 --- a/Modules/_curses_panel.c +++ b/Modules/_curses_panel.c @@ -520,7 +520,7 @@ static PyType_Slot PyCursesPanel_Type_slots[] = { static PyType_Spec PyCursesPanel_Type_spec = { .name = "_curses_panel.panel", .basicsize = sizeof(PyCursesPanelObject), - .flags = Py_TPFLAGS_DEFAULT, + .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION, .slots = PyCursesPanel_Type_slots }; @@ -656,7 +656,6 @@ _curses_panel_exec(PyObject *mod) if (state->PyCursesPanel_Type == NULL) { return -1; } - ((PyTypeObject *)state->PyCursesPanel_Type)->tp_new = NULL; if (PyModule_AddType(mod, state->PyCursesPanel_Type) < 0) { return -1; diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index d221cf1..bcf9ad1 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -4793,9 +4793,11 @@ PyInit__curses(void) #ifdef NCURSES_VERSION /* ncurses_version */ if (NcursesVersionType.tp_name == NULL) { - if (PyStructSequence_InitType2(&NcursesVersionType, - &ncurses_version_desc) < 0) + if (_PyStructSequence_InitType(&NcursesVersionType, + &ncurses_version_desc, + Py_TPFLAGS_DISALLOW_INSTANTIATION) < 0) { return NULL; + } } v = make_ncurses_version(); if (v == NULL) { @@ -4803,15 +4805,6 @@ PyInit__curses(void) } PyDict_SetItemString(d, "ncurses_version", v); Py_DECREF(v); - - /* prevent user from creating new instances */ - NcursesVersionType.tp_init = NULL; - NcursesVersionType.tp_new = NULL; - if (PyDict_DelItemString(NcursesVersionType.tp_dict, "__new__") < 0 && - PyErr_ExceptionMatches(PyExc_KeyError)) - { - PyErr_Clear(); - } #endif /* NCURSES_VERSION */ SetDictInt("ERR", ERR); diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 46d6a6e..3a0e5de 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -1002,7 +1002,7 @@ static PyType_Spec PyTclObject_Type_spec = { "_tkinter.Tcl_Obj", sizeof(PyTclObject), 0, - Py_TPFLAGS_DEFAULT, + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION, PyTclObject_Type_slots, }; @@ -3294,7 +3294,7 @@ static PyType_Spec Tktt_Type_spec = { "_tkinter.tktimertoken", sizeof(TkttObject), 0, - Py_TPFLAGS_DEFAULT, + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION, Tktt_Type_slots, }; @@ -3349,7 +3349,7 @@ static PyType_Spec Tkapp_Type_spec = { "_tkinter.tkapp", sizeof(TkappObject), 0, - Py_TPFLAGS_DEFAULT, + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION, Tkapp_Type_slots, }; @@ -3537,7 +3537,6 @@ PyInit__tkinter(void) Py_DECREF(m); return NULL; } - ((PyTypeObject *)o)->tp_new = NULL; if (PyModule_AddObject(m, "TkappType", o)) { Py_DECREF(o); Py_DECREF(m); @@ -3550,7 +3549,6 @@ PyInit__tkinter(void) Py_DECREF(m); return NULL; } - ((PyTypeObject *)o)->tp_new = NULL; if (PyModule_AddObject(m, "TkttType", o)) { Py_DECREF(o); Py_DECREF(m); @@ -3563,7 +3561,6 @@ PyInit__tkinter(void) Py_DECREF(m); return NULL; } - ((PyTypeObject *)o)->tp_new = NULL; if (PyModule_AddObject(m, "Tcl_Obj", o)) { Py_DECREF(o); Py_DECREF(m); diff --git a/Modules/_xxsubinterpretersmodule.c b/Modules/_xxsubinterpretersmodule.c index b94b130..9290255 100644 --- a/Modules/_xxsubinterpretersmodule.c +++ b/Modules/_xxsubinterpretersmodule.c @@ -1780,7 +1780,12 @@ static PyTypeObject ChannelIDtype = { 0, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ + // Use Py_TPFLAGS_DISALLOW_INSTANTIATION so the type cannot be instantiated + // from Python code. We do this because there is a strong relationship + // between channel IDs and the channel lifecycle, so this limitation avoids + // related complications. Use the _channel_id() function instead. + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE + | Py_TPFLAGS_DISALLOW_INSTANTIATION, /* tp_flags */ channelid_doc, /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ @@ -1791,19 +1796,6 @@ static PyTypeObject ChannelIDtype = { 0, /* tp_methods */ 0, /* tp_members */ channelid_getsets, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - 0, /* tp_init */ - 0, /* tp_alloc */ - // Note that we do not set tp_new to channelid_new. Instead we - // set it to NULL, meaning it cannot be instantiated from Python - // code. We do this because there is a strong relationship between - // channel IDs and the channel lifecycle, so this limitation avoids - // related complications. - NULL, /* tp_new */ }; diff --git a/Objects/structseq.c b/Objects/structseq.c index 88e63b6..bf59f47 100644 --- a/Objects/structseq.c +++ b/Objects/structseq.c @@ -459,8 +459,10 @@ initialize_members(PyStructSequence_Desc *desc, PyMemberDef* members, members[k].name = NULL; } + int -PyStructSequence_InitType2(PyTypeObject *type, PyStructSequence_Desc *desc) +_PyStructSequence_InitType(PyTypeObject *type, PyStructSequence_Desc *desc, + unsigned long tp_flags) { PyMemberDef *members; Py_ssize_t n_members, n_unnamed_members; @@ -488,7 +490,7 @@ PyStructSequence_InitType2(PyTypeObject *type, PyStructSequence_Desc *desc) type->tp_base = &PyTuple_Type; type->tp_methods = structseq_methods; type->tp_new = structseq_new; - type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC; + type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | tp_flags; type->tp_traverse = (traverseproc) structseq_traverse; n_members = count_members(desc, &n_unnamed_members); @@ -516,6 +518,12 @@ PyStructSequence_InitType2(PyTypeObject *type, PyStructSequence_Desc *desc) return 0; } +int +PyStructSequence_InitType2(PyTypeObject *type, PyStructSequence_Desc *desc) +{ + return _PyStructSequence_InitType(type, desc, 0); +} + void PyStructSequence_InitType(PyTypeObject *type, PyStructSequence_Desc *desc) { diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 1f8e257..0f7f280 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -158,6 +158,11 @@ _PyType_CheckConsistency(PyTypeObject *type) CHECK(!(type->tp_flags & Py_TPFLAGS_READYING)); CHECK(type->tp_dict != NULL); + if (type->tp_flags & Py_TPFLAGS_DISALLOW_INSTANTIATION) { + CHECK(type->tp_new == NULL); + CHECK(_PyDict_ContainsId(type->tp_dict, &PyId___new__) == 0); + } + return 1; #undef CHECK } @@ -1111,8 +1116,7 @@ type_call(PyTypeObject *type, PyObject *args, PyObject *kwds) if (type->tp_new == NULL) { _PyErr_Format(tstate, PyExc_TypeError, - "cannot create '%.100s' instances", - type->tp_name); + "cannot create '%s' instances", type->tp_name); return NULL; } @@ -3185,6 +3189,8 @@ type_new_impl(type_new_ctx *ctx) if (type_new_init_subclass(type, ctx->kwds) < 0) { goto error; } + + assert(_PyType_CheckConsistency(type)); return (PyObject *)type; error: @@ -5651,7 +5657,6 @@ type_add_getset(PyTypeObject *type) static void inherit_special(PyTypeObject *type, PyTypeObject *base) { - /* Copying tp_traverse and tp_clear is connected to the GC flags */ if (!(type->tp_flags & Py_TPFLAGS_HAVE_GC) && (base->tp_flags & Py_TPFLAGS_HAVE_GC) && @@ -5662,23 +5667,7 @@ inherit_special(PyTypeObject *type, PyTypeObject *base) if (type->tp_clear == NULL) type->tp_clear = base->tp_clear; } - { - /* The condition below could use some explanation. - It appears that tp_new is not inherited for static types - whose base class is 'object'; this seems to be a precaution - so that old extension types don't suddenly become - callable (object.__new__ wouldn't insure the invariants - that the extension type's own factory function ensures). - Heap types, of course, are under our control, so they do - inherit tp_new; static extension types that specify some - other built-in type as the default also - inherit object.__new__. */ - if (base != &PyBaseObject_Type || - (type->tp_flags & Py_TPFLAGS_HEAPTYPE)) { - if (type->tp_new == NULL) - type->tp_new = base->tp_new; - } - } + if (type->tp_basicsize == 0) type->tp_basicsize = base->tp_basicsize; @@ -5941,6 +5930,7 @@ inherit_slots(PyTypeObject *type, PyTypeObject *base) } static int add_operators(PyTypeObject *); +static int add_tp_new_wrapper(PyTypeObject *type); static int @@ -5991,6 +5981,7 @@ type_ready_set_bases(PyTypeObject *type) type->tp_base = base; } } + assert(type->tp_base != NULL || type == &PyBaseObject_Type); /* Now the only way base can still be NULL is if type is * &PyBaseObject_Type. */ @@ -6249,6 +6240,50 @@ type_ready_add_subclasses(PyTypeObject *type) } +// Set tp_new and the "__new__" key in the type dictionary. +// Use the Py_TPFLAGS_DISALLOW_INSTANTIATION flag. +static int +type_ready_set_new(PyTypeObject *type) +{ + PyTypeObject *base = type->tp_base; + /* The condition below could use some explanation. + + It appears that tp_new is not inherited for static types whose base + class is 'object'; this seems to be a precaution so that old extension + types don't suddenly become callable (object.__new__ wouldn't insure the + invariants that the extension type's own factory function ensures). + + Heap types, of course, are under our control, so they do inherit tp_new; + static extension types that specify some other built-in type as the + default also inherit object.__new__. */ + if (type->tp_new == NULL + && base == &PyBaseObject_Type + && !(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) + { + type->tp_flags |= Py_TPFLAGS_DISALLOW_INSTANTIATION; + } + + if (!(type->tp_flags & Py_TPFLAGS_DISALLOW_INSTANTIATION)) { + if (type->tp_new != NULL) { + // If "__new__" key does not exists in the type dictionary, + // set it to tp_new_wrapper(). + if (add_tp_new_wrapper(type) < 0) { + return -1; + } + } + else { + // tp_new is NULL: inherit tp_new from base + type->tp_new = base->tp_new; + } + } + else { + // Py_TPFLAGS_DISALLOW_INSTANTIATION sets tp_new to NULL + type->tp_new = NULL; + } + return 0; +} + + static int type_ready(PyTypeObject *type) { @@ -6275,6 +6310,9 @@ type_ready(PyTypeObject *type) if (type_ready_mro(type) < 0) { return -1; } + if (type_ready_set_new(type) < 0) { + return -1; + } if (type_ready_fill_dict(type) < 0) { return -1; } @@ -6898,8 +6936,8 @@ tp_new_wrapper(PyObject *self, PyObject *args, PyObject *kwds) "__new__() called with non-type 'self'"); return NULL; } - type = (PyTypeObject *)self; + if (!PyTuple_Check(args) || PyTuple_GET_SIZE(args) < 1) { PyErr_Format(PyExc_TypeError, "%s.__new__(): not enough arguments", @@ -6961,16 +6999,18 @@ static struct PyMethodDef tp_new_methoddef[] = { static int add_tp_new_wrapper(PyTypeObject *type) { - PyObject *func; - int r = _PyDict_ContainsId(type->tp_dict, &PyId___new__); - if (r > 0) + if (r > 0) { return 0; - if (r < 0) + } + if (r < 0) { return -1; - func = PyCFunction_NewEx(tp_new_methoddef, (PyObject *)type, NULL); - if (func == NULL) + } + + PyObject *func = PyCFunction_NewEx(tp_new_methoddef, (PyObject *)type, NULL); + if (func == NULL) { return -1; + } r = _PyDict_SetItemId(type->tp_dict, &PyId___new__, func); Py_DECREF(func); return r; @@ -8558,11 +8598,6 @@ add_operators(PyTypeObject *type) Py_DECREF(descr); } } - if (type->tp_new != NULL) { - if (add_tp_new_wrapper(type) < 0) { - return -1; - } - } return 0; } diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 911c2d9..36297ff 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -2810,56 +2810,35 @@ _PySys_InitCore(PyThreadState *tstate, PyObject *sysdict) /* version_info */ if (VersionInfoType.tp_name == NULL) { - if (PyStructSequence_InitType2(&VersionInfoType, - &version_info_desc) < 0) { + if (_PyStructSequence_InitType(&VersionInfoType, + &version_info_desc, + Py_TPFLAGS_DISALLOW_INSTANTIATION) < 0) { goto type_init_failed; } } version_info = make_version_info(tstate); SET_SYS("version_info", version_info); - /* prevent user from creating new instances */ - VersionInfoType.tp_init = NULL; - VersionInfoType.tp_new = NULL; - res = PyDict_DelItemString(VersionInfoType.tp_dict, "__new__"); - if (res < 0 && _PyErr_ExceptionMatches(tstate, PyExc_KeyError)) { - _PyErr_Clear(tstate); - } /* implementation */ SET_SYS("implementation", make_impl_info(version_info)); // sys.flags: updated in-place later by _PySys_UpdateConfig() if (FlagsType.tp_name == 0) { - if (PyStructSequence_InitType2(&FlagsType, &flags_desc) < 0) { + if (_PyStructSequence_InitType(&FlagsType, &flags_desc, + Py_TPFLAGS_DISALLOW_INSTANTIATION) < 0) { goto type_init_failed; } } SET_SYS("flags", make_flags(tstate->interp)); - /* prevent user from creating new instances */ - FlagsType.tp_init = NULL; - FlagsType.tp_new = NULL; - res = PyDict_DelItemString(FlagsType.tp_dict, "__new__"); - if (res < 0) { - if (!_PyErr_ExceptionMatches(tstate, PyExc_KeyError)) { - goto err_occurred; - } - _PyErr_Clear(tstate); - } #if defined(MS_WINDOWS) /* getwindowsversion */ - if (WindowsVersionType.tp_name == 0) - if (PyStructSequence_InitType2(&WindowsVersionType, - &windows_version_desc) < 0) { + if (WindowsVersionType.tp_name == 0) { + if (_PyStructSequence_InitType(&WindowsVersionType, + &windows_version_desc, + Py_TPFLAGS_DISALLOW_INSTANTIATION) < 0) { goto type_init_failed; } - /* prevent user from creating new instances */ - WindowsVersionType.tp_init = NULL; - WindowsVersionType.tp_new = NULL; - assert(!_PyErr_Occurred(tstate)); - res = PyDict_DelItemString(WindowsVersionType.tp_dict, "__new__"); - if (res < 0 && _PyErr_ExceptionMatches(tstate, PyExc_KeyError)) { - _PyErr_Clear(tstate); } #endif -- cgit v0.12