This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: A tp_dealloc of a subclassed class cannot resurrect an object
Type: enhancement Stage: resolved
Components: Interpreter Core Versions: Python 3.3
process
Status: closed Resolution: fixed
Dependencies: Superseder:
Assigned To: Nosy List: benjamin.peterson, kristjan.jonsson, meador.inge, pitrou, vstinner
Priority: normal Keywords: patch

Created on 2010-03-23 13:28 by kristjan.jonsson, last changed 2022-04-11 14:56 by admin. This issue is now closed.

Files
File name Uploaded Description Edit
basedealloc.patch kristjan.jonsson, 2012-04-15 12:31 review
basedealloc.diff kristjan.jonsson, 2012-04-16 10:11 review
resurrect.patch kristjan.jonsson, 2012-04-16 22:56 review
Messages (14)
msg101580 - (view) Author: Kristján Valur Jónsson (kristjan.jonsson) * (Python committer) Date: 2010-03-23 13:28
The tp_dealloc of a type can chose to resurrect an object.  the subtype_dealloc() in typeobject.c does this when it calls the tp_del() member and it has increased the refcount.

The problem is, that if you subclass a custom C object, and that C object's tp_dealloc() chooses to resurrect a dying object, that doesn't go well with subtype_dealloc().

After calling basedealloc() (line 1002 in typeobject.c), the object's type is decrefed.  But if the object was resurrected by basedealloc() this shouldn't have been done.  The object will be alive, but the type will be missing a reference.  This will cause a crash later.

This could be fixable if we knew somehow after calling basedealloc() if the object were still alive.  But we cannot check "self" because it may have died.

The only way out of this conundrum that I can see is to change the signature of tp_dealloc() to return a flag, whether it did actually delete the object or not.

Of course, I see no easy way around not clearing the slots, but the clearing of the dict could be postponed until after the call to basedealloc().

Since tp_dealloc _can_ resurrect objects (subtype_dealloc does it), subclassing such objects should work, and not crash the interpreter if the base class' dp_dealloc() decides to do so.
No suggested
msg101585 - (view) Author: Antoine Pitrou (pitrou) * (Python committer) Date: 2010-03-23 15:25
Indeed. The io module has had to circumvent this and uses the following snippet when resurrecting an instance of a subclass of one of its types (see iobase_dealloc() in Modules/_io/iobase.c):

        /* When called from a heap type's dealloc, the type will be
           decref'ed on return (see e.g. subtype_dealloc in typeobject.c). */
        if (PyType_HasFeature(Py_TYPE(self), Py_TPFLAGS_HEAPTYPE))
            Py_INCREF(Py_TYPE(self));
        return;


I agree it would be nice to have an automatic way of handling this. I don't see an obvious solution, though.
msg158324 - (view) Author: Kristján Valur Jónsson (kristjan.jonsson) * (Python committer) Date: 2012-04-15 12:31
Here is a patch that rectifies this situation, albeit in a somewhat 'hacky' manner.
It works by injecting a monitoring 'tp_free' call into the type during the basedealloc call, which sets a flag if it was called with the object, i.e. if actual deletion took place.
This value is then returned, and a decision to decref the object's "type" is made on this result.
msg158328 - (view) Author: Kristján Valur Jónsson (kristjan.jonsson) * (Python committer) Date: 2012-04-15 14:12
Another, less hacky but more intrusive, way would be to change the signature of tp_dealloc in a backwards compatible way:
typedef void (*destructor)(PyObject *, int *destroyed);

The destructor can then set the flag pointed to by 'destroyed' to 1 or 0, depending on whether actual destruction took place.  The caller will set the flag to '1' by default.

We could then change all internal destructors to conform, and know that external destructors will continue to work in the old way.
msg158407 - (view) Author: Kristján Valur Jónsson (kristjan.jonsson) * (Python committer) Date: 2012-04-16 10:11
Updated the patch with better documentation, and recursion safety.
msg158444 - (view) Author: Antoine Pitrou (pitrou) * (Python committer) Date: 2012-04-16 13:31
Urg, that's a horrible hack.
How about instead having an API function to resurrect an object from a tp_dealloc?

That way the iobase_dealloc code would be written:

    if (_PyIOBase_finalize((PyObject *) self) < 0) {
        _PyObject_ResurrectFromDealloc(self);
        return;
    }

That API function could also perhaps take care of the _Py_NewReference stuff (see the end of _PyIOBase_finalize).
msg158449 - (view) Author: Kristján Valur Jónsson (kristjan.jonsson) * (Python committer) Date: 2012-04-16 13:46
Ok, that sounds reasonable, particularly in light of that _NewReference stuff.  I'll work out a different patch then.  But I think the API must be public, since it would need to work from extension modules.

So: if a c type decides that it wants to live after a tp_dealloc(), it must call this api ...
msg158452 - (view) Author: Antoine Pitrou (pitrou) * (Python committer) Date: 2012-04-16 13:57
> Ok, that sounds reasonable, particularly in light of that
> _NewReference stuff.  I'll work out a different patch then.  But I
> think the API must be public, since it would need to work from
> extension modules.

Needing to work from (stdlib) extension modules and being public are too
different things. I don't think we want to encourage third-party types
to resurrect their instances.
msg158504 - (view) Author: Kristján Valur Jónsson (kristjan.jonsson) * (Python committer) Date: 2012-04-16 21:36
Incidentally, Looking at this, I noticed code such as: (textio.c)

static int
_textiowrapper_clear(textio *self)
{
    if (self->ok && _PyIOBase_finalize((PyObject *) self) < 0)
        return -1;

This shows something scary:  During a GC run, it is possible to invoke the  "close()" method on a textio object.  This is dangerous, and probably not allowed at all.
Returning -1 from a tp_clear() slot has no particular meaning, and the return value appears to be ignored by gcmodule.c (also, it won't ever ber resurrected through that pathway, since it is incref'd first:
    Py_INCREF(op);
    clear(op);
    Py_DECREF(op);
)

See Issue 9141 for more info.
IMHO, this is another case where objects must tell GC that they cannot be collected but instead must be placed in gc.garbage.

This probably requires a defect of its own.
msg158505 - (view) Author: Antoine Pitrou (pitrou) * (Python committer) Date: 2012-04-16 21:39
> static int
> _textiowrapper_clear(textio *self)
> {
>     if (self->ok && _PyIOBase_finalize((PyObject *) self) < 0)
>         return -1;
> 
> This shows something scary:  During a GC run, it is possible to invoke
> the  "close()" method on a textio object.  This is dangerous, and
> probably not allowed at all.

How is it scary? This is not worse than invoking a __del__ method.

> Returning -1 from a tp_clear() slot has no particular meaning, and the
> return value appears to be ignored by gcmodule.c

I don't think this is a problem.
msg158508 - (view) Author: Kristján Valur Jónsson (kristjan.jonsson) * (Python committer) Date: 2012-04-16 21:59
__del__ methods are never invoked from GC.  All objects that have finalizers, and all objects reachable from them _must_ be put in gc.garbage.
If I'm not mistaken, this is a fundamental rule of python's gc module and it is not because of the unknown order of finalizers as some people seem to think, but because the state of the interpreter during the collection phase is so fragile, with objects being linked up in various lists, and so on, that it isn't permissable to run any dynamic code.
msg158512 - (view) Author: Kristján Valur Jónsson (kristjan.jonsson) * (Python committer) Date: 2012-04-16 22:56
Here is a patch with the suggested change.
I put _PyObject_ResurrectFromDealloc into typeobject.c since it seems more at home there than in object.c, but I'm not sure.
Also note that other types, that were calling _PyIOBase_finalize() from their tp_dealloc slots (bufferedio, fileio) will now also get the correct behavior with the incref'd ob_type slot, if the object is resurrected.
msg227608 - (view) Author: Antoine Pitrou (pitrou) * (Python committer) Date: 2014-09-26 11:32
I think PEP 442 makes this request obsolete: you can simply implement tp_finalize() and incref the object naturally from there. Kristjan, what do you think?
msg372323 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2020-06-25 09:31
> I think PEP 442 makes this request obsolete: you can simply implement tp_finalize() and incref the object naturally from there.

Right. I close the issue.
History
Date User Action Args
2022-04-11 14:56:59adminsetgithub: 52459
2020-06-25 09:31:43vstinnersetstatus: open -> closed

nosy: + vstinner
messages: + msg372323

resolution: fixed
stage: needs patch -> resolved
2014-09-26 11:32:56pitrousetmessages: + msg227608
2012-04-16 22:56:57kristjan.jonssonsetfiles: + resurrect.patch

messages: + msg158512
2012-04-16 21:59:42kristjan.jonssonsetmessages: + msg158508
2012-04-16 21:39:27pitrousetmessages: + msg158505
2012-04-16 21:36:05kristjan.jonssonsetmessages: + msg158504
2012-04-16 13:57:12pitrousetmessages: + msg158452
2012-04-16 13:46:28kristjan.jonssonsetmessages: + msg158449
2012-04-16 13:31:12pitrousetmessages: + msg158444
2012-04-16 10:11:05kristjan.jonssonsetfiles: + basedealloc.diff

messages: + msg158407
2012-04-15 15:59:20meador.ingesetnosy: + meador.inge
2012-04-15 14:12:52kristjan.jonssonsetmessages: + msg158328
2012-04-15 12:31:09kristjan.jonssonsetfiles: + basedealloc.patch
keywords: + patch
messages: + msg158324

versions: + Python 3.3, - Python 3.2
2010-08-09 18:38:19terry.reedysetversions: - Python 2.7
2010-03-23 15:25:38pitrousetpriority: normal

type: crash -> enhancement
components: + Interpreter Core
versions: + Python 3.2
nosy: + pitrou, benjamin.peterson

messages: + msg101585
stage: needs patch
2010-03-23 13:28:30kristjan.jonssoncreate