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

Using typename.__setattr__ in extension type with Py_TPFLAGS_HEAPTYPE is broken (hackcheck too eager?) #84141

Closed
MatzeB mannequin opened this issue Mar 13, 2020 · 20 comments
Labels
3.8 only security fixes 3.9 only security fixes 3.10 only security fixes topic-C-API type-bug An unexpected behavior, bug, or error

Comments

@MatzeB
Copy link
Mannequin

MatzeB mannequin commented Mar 13, 2020

BPO 39960
Nosy @gvanrossum, @scoder, @encukou, @miss-islington, @MatzeB, @david-caro
PRs
  • bpo-39960: Allow heap types in the "Carlo Verre" hack check that override "tp_setattro()" #21092
  • [3.9] bpo-39960: Allow heap types in the "Carlo Verre" hack check that override "tp_setattro()" (GH-21092) #21288
  • [3.8] bpo-39960: Allow heap types in the "Carlo Verre" hack check that override "tp_setattro()" (GH-21092) #21339
  • bpo-41295: Allow overriding __setattr__ on multiple inheritance #21473
  • Files
  • demomodule.zip: Minimal extension module overriding tp_setattro
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = None
    closed_at = <Date 2020-09-20.07:49:10.772>
    created_at = <Date 2020-03-13.22:31:04.481>
    labels = ['expert-C-API', 'type-bug', '3.8', '3.9', '3.10']
    title = 'Using typename.__setattr__ in extension type with Py_TPFLAGS_HEAPTYPE is broken (hackcheck too eager?)'
    updated_at = <Date 2020-09-20.07:49:10.771>
    user = 'https://github.com/MatzeB'

    bugs.python.org fields:

    activity = <Date 2020-09-20.07:49:10.771>
    actor = 'scoder'
    assignee = 'none'
    closed = True
    closed_date = <Date 2020-09-20.07:49:10.772>
    closer = 'scoder'
    components = ['C API']
    creation = <Date 2020-03-13.22:31:04.481>
    creator = 'Matthias Braun'
    dependencies = []
    files = ['48974']
    hgrepos = []
    issue_num = 39960
    keywords = ['patch']
    message_count = 20.0
    messages = ['364123', '371932', '371976', '372020', '372024', '372179', '372202', '372882', '372895', '372896', '372898', '373043', '373044', '373052', '373647', '373648', '373875', '373877', '373882', '377210']
    nosy_count = 6.0
    nosy_names = ['gvanrossum', 'scoder', 'petr.viktorin', 'miss-islington', 'Matthias Braun', 'David Caro']
    pr_nums = ['21092', '21288', '21339', '21473']
    priority = 'normal'
    resolution = 'fixed'
    stage = 'resolved'
    status = 'closed'
    superseder = None
    type = 'behavior'
    url = 'https://bugs.python.org/issue39960'
    versions = ['Python 3.8', 'Python 3.9', 'Python 3.10']

    @MatzeB
    Copy link
    Mannequin Author

    MatzeB mannequin commented Mar 13, 2020

    This is about an extension type created via PyType_FromSpec that overrides tp_setattro (minimal example attached). In this case cpython does not let me grab and use the __setattr__ function "manually". Example:

    >>> import demo
    >>> mytype_setattr = demo.MyType.__setattr__
    >>> i = demo.MyType()
    >>> mytype_setattr(i, "foo", "bar")
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: can't apply this __setattr__ to object object
    

    I suspect this is related to the loop at the beginning of typeobject.c / hackcheck() that skips over types with Py_TPFLAGS_HEAPOBJECT. (Though removing the loop breaks things like the enum module).

    @MatzeB MatzeB mannequin added 3.7 (EOL) end of life topic-C-API labels Mar 13, 2020
    @scoder
    Copy link
    Contributor

    scoder commented Jun 20, 2020

    I ran into this, too. I agree that the "hackcheck" loop on heap types is probably wrong.

    cpython/Objects/typeobject.c

    Lines 5947 to 5977 in 04fc4f2

    /* Helper to check for object.__setattr__ or __delattr__ applied to a type.
    This is called the Carlo Verre hack after its discoverer. */
    static int
    hackcheck(PyObject *self, setattrofunc func, const char *what)
    {
    PyTypeObject *type = Py_TYPE(self);
    while (type && type->tp_flags & Py_TPFLAGS_HEAPTYPE)
    type = type->tp_base;
    /* If type is NULL now, this is a really weird type.
    In the spirit of backwards compatibility (?), just shut up. */
    if (type && type->tp_setattro != func) {
    PyErr_Format(PyExc_TypeError,
    "can't apply this %s to %s object",
    what,
    type->tp_name);
    return 0;
    }
    return 1;
    }
    static PyObject *
    wrap_setattr(PyObject *self, PyObject *args, void *wrapped)
    {
    setattrofunc func = (setattrofunc)wrapped;
    int res;
    PyObject *name, *value;
    if (!PyArg_UnpackTuple(args, "", 2, 2, &name, &value))
    return NULL;
    if (!hackcheck(self, func, "__setattr__"))
    return NULL;

    It was written at a time (Py2.3?) when (practically) only Python implemented types were heap types, not extension types. I think what it tried to do was to find the builtin base type of a Python type, and check that no-one played tricks on the C slot functions of that C implemented type. With extension types implemented as heap types, having a different slot function in there is a perfectly valid thing.

    I'll call in a couple of people since I'm also not sure how to fix the "hackcheck".

    I'll also leave Py3.7 in the list of affected Python versions since we still have a short week before its final non-secfix-release. :)

    @scoder scoder added 3.8 only security fixes 3.9 only security fixes 3.10 only security fixes type-bug An unexpected behavior, bug, or error labels Jun 20, 2020
    @gvanrossum
    Copy link
    Member

    See also test_carloverre() in test_descr.py. I don't recall exactly what this was about, but that test is definitely relevant.

    @scoder
    Copy link
    Contributor

    scoder commented Jun 21, 2020

    This SO answer by Martijn Pieters explains the background:

    https://stackoverflow.com/a/44854477

    and links to the original python-dev discussion:

    https://mail.python.org/pipermail/python-dev/2003-April/034535.html

    The current implementation checks that the function being called is the one defined in the first non-heap type up the hierarchy. As long as heap types are only Python types, this is the first builtin/native/C-implemented (super)type. With extension types as heap types, however, it's no longer necessarily the type that defines the correct slot function.

    IMHO, Greg Ewing gives the right direction here:

    https://mail.python.org/pipermail/python-dev/2003-April/034569.html

    Providing some way for objects to prevent superclass
    methods from being called on them when they're not looking

    So, I think we should do something like walking up the hierarchy to find the C function in it that is currently being called, and then check if it's the one we would expect or if a subclass defines a different one. The current check does not bother to search and just assumes that it knows which (super)type defines the right function to call.

    @gvanrossum
    Copy link
    Member

    That sounds like the right thing to do. You should be able to recognize pure Python __setattr__ implementations by the presence of slot_tp_setattro in the tp_setattro slot. (Likewise for __delattr__.)

    It's been really long since I looked at this -- do we still only support single inheritance at the C level? Because otherwise there would be another backdoor.

    There's also tp_setattr (no 'o') but I think that's basically unused, and we ignored that in 2003 so we should be able to ignore it now. :-)

    @encukou
    Copy link
    Member

    encukou commented Jun 23, 2020

    do we still only support single inheritance at the C level?

    Not any more. Heap types may have multiple bases, and they can be created at the C level (since Python 3.2, PEP-384).

    So, I think we should do something like walking up the hierarchy to find the C function in it that is currently being called, and then check if it's the one we would expect or if a subclass defines a different one. The current check does not bother to search and just assumes that it knows which (super)type defines the right function to call.

    Should we be bold and skip the check for heap types?
    That would mean that when/if str is converted to a heap type, you could do object.__delattr__(str, "lower"). That would break your Python, but only at the Python level (similar to things like import builtins; del builtins.__build_class__).
    Heap types are not shared between interpreters, so that reason is gone. But Guido's [2003 mail] suggests there are other reasons to prevent changing built-in types. What are they?

    [2003 mail] https://mail.python.org/pipermail/python-dev/2003-April/034535.html

    @scoder
    Copy link
    Contributor

    scoder commented Jun 23, 2020

    I chose to go through the MRO, which takes multiple inheritance into account.

    @scoder scoder removed 3.7 (EOL) end of life labels Jul 2, 2020
    @scoder
    Copy link
    Contributor

    scoder commented Jul 2, 2020

    I think we missed the train for fixing 3.7 (which was questionable anyway), but I added a test, so it's ready for merging into 3.8+ (minus merge conflicts for the test in 3.8, probably).

    @miss-islington
    Copy link
    Contributor

    New changeset 148f329 by scoder in branch 'master':
    bpo-39960: Allow heap types in the "Carlo Verre" hack check that override "tp_setattro()" (GH-21092)
    148f329

    @gvanrossum
    Copy link
    Member

    Stefan can you do the 3.8 backport?

    @miss-islington
    Copy link
    Contributor

    New changeset bfec674 by Miss Islington (bot) in branch '3.9':
    bpo-39960: Allow heap types in the "Carlo Verre" hack check that override "tp_setattro()" (GH-21092)
    bfec674

    @scoder
    Copy link
    Contributor

    scoder commented Jul 5, 2020

    New changeset 8912c18 by scoder in branch '3.8':
    bpo-39960: Allow heap types in the "Carlo Verre" hack check that override "tp_setattro()" (GH-21092) (GH-21339)
    8912c18

    @scoder
    Copy link
    Contributor

    scoder commented Jul 5, 2020

    Fixed in 3.8+. Closing.
    Thanks for the feedback.

    @scoder scoder closed this as completed Jul 5, 2020
    @scoder scoder closed this as completed Jul 5, 2020
    @gvanrossum
    Copy link
    Member

    You're welcome. Hope this helps Cpython users.

    @gvanrossum
    Copy link
    Member

    Reopening because there appears to be a problem with the fix (see PR 21473).

    @gvanrossum gvanrossum reopened this Jul 14, 2020
    @gvanrossum gvanrossum reopened this Jul 14, 2020
    @gvanrossum
    Copy link
    Member

    @Stefan Do you agree that the fix proposed in PR 21473 needs to be made?

    @scoder
    Copy link
    Contributor

    scoder commented Jul 18, 2020

    The problem in the test added in PR 21473 is that there is an intermediate base type "object" in the hierarchy:

        class A(type):
            def __setattr__(cls, key, value):
                type.__setattr__(cls, key, value)
    
        class B:
            pass
    
        class C(B, A):
            pass
    >>> [c for c in C.__mro__]
    [<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'type'>, <class 'object'>]
    
    >>> [c.__setattr__ for c in C.__mro__]
    [<function A.__setattr__ at 0x7f3ca4308378>, <slot wrapper '__setattr__' of 'object' objects>, <function A.__setattr__ at 0x7f3ca4308378>, <slot wrapper '__setattr__' of 'type' objects>, <slot wrapper '__setattr__' of 'object' objects>]

    I think the change to the algorithm there is too broad, it disables much of what the check was written for (or against).

    Given Guido's second (negative) test case, I think it would also not be correct to add "object.__setattr__" to the list of allowed (intermediate) slot methods:

        class A(type):
            def __setattr__(cls, key, value):
                object.__setattr__(cls, key, value)   # this should fail!
    
        class B:
            pass
    
        class C(B, A):
            pass

    It's difficult to turn this into an algorithm. Is the MRO really the place to look? For "A", we're only really interested in the C-level bases, aren't we?

    @scoder
    Copy link
    Contributor

    scoder commented Jul 18, 2020

    intermediate base type "object" in the hierarchy

    Sorry, I meant an intermediate base type "B", which inherits its setattr from "object".

    @scoder
    Copy link
    Contributor

    scoder commented Jul 18, 2020

    I pushed PR 21528 with a new proposal. See bpo-41295.

    @scoder
    Copy link
    Contributor

    scoder commented Sep 20, 2020

    Closing again since #65727 has been merged in bpo-41295.

    @scoder scoder closed this as completed Sep 20, 2020
    @scoder scoder closed this as completed Sep 20, 2020
    @ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    3.8 only security fixes 3.9 only security fixes 3.10 only security fixes topic-C-API type-bug An unexpected behavior, bug, or error
    Projects
    None yet
    Development

    No branches or pull requests

    4 participants