Opened 9 years ago
Last modified 8 years ago
#13901 new defect
Fix cython's deep C-stacks upon deallocation
Reported by: | nbruin | Owned by: | rlm |
---|---|---|---|
Priority: | major | Milestone: | sage-6.4 |
Component: | memleak | Keywords: | |
Cc: | SimonKing | Merged in: | |
Authors: | Reviewers: | ||
Report Upstream: | Reported upstream. Developers acknowledge bug. | Work issues: | |
Branch: | Commit: | ||
Dependencies: | Stopgaps: |
Description (last modified by )
Once you know about python's trashcan
that is used in its dealloc methods and that cython does not make use of it, it's not hard to cause a crash in deallocation of a cython class:
cython(""" cdef class A(object): cdef object ref def __init__(self,ref): self.ref = ref """)
A long linked list of these will quickly exhaust the C-stack (set a ulimit on the stack if you have a lot of memory):
print "allocating a" a=A(None) for i in range(10**6): a=A(a) print "deleting a" del a print "done deleting a"
Once you interleave with a python container type (tuple, for instance), the trashcan starts to kick in. The following runs without problem.
b=A(None) print "allocating b" for i in range(10**6): b=A((b,)) print "deleting b" del b print "done deleting b"
This issue came up as a side issue on #13896. The trashcan is a rather complicated thing to get working right. In particular, cython must take precautions to ensure that once deallocation is started on an object, it isn't put into the trashcan in deallocs of base types.
This is now upstream http://trac.cython.org/cython_trac/ticket/797
Attachments (1)
Change History (12)
Changed 9 years ago by
comment:1 Changed 9 years ago by
Example output:
sh-4.2$ ulimit -s 8000 sh-4.2$ sage test.sage allocating b deleting b done deleting b allocating a deleting a /usr/local/sage/5.0/spkg/bin/sage: line 468: 4393 Segmentation fault (core dumped) python "$@"
comment:2 Changed 9 years ago by
- Cc SimonKing added
comment:3 follow-up: ↓ 5 Changed 9 years ago by
In fact, I think the precautions taken are not enough for general cython classes. With the little
++_PyTrash_delete_nesting; Py_TRASHCAN_SAFE_BEGIN(self); --_PyTrash_delete_nesting; ... ++_PyTrash_delete_nesting; Py_TRASHCAN_SAFE_END(self); --_PyTrash_delete_nesting;
dance they are making sure there is room for one extra trashcan nesting provided that that call doesn't >use the same trick. However, a cython class could have a whole inheritance hierarchy going here (that >would all use this trick too!), so I'm pretty sure that the exact scenario they describe could still >happen. You'd need to know the depth of the inheritance line (for deallocs, multiple inheritance can't >happen, right?) and ensure there's enough room for all those.
About what you pointed out in #13896 and I've reproduced here where it now belongs, I agree we can not use such a trick. But I don't feel really confortable with this trick anyway.
It seems to me the trashcan macros where designed to avoid the situation you produced here, that is objects pointing to objects pointing to... which could eventually blow up the stack.
But then it also includes the fact that a given type can extend another one and so they had to use that trick to fool the trashcan mecanism. This seems indeed necessary because it could surely lead to strange situation if the type specific action where happening between the trashcan macro and the super class action outside of it, especially if this super class doesn't use the trashcan macros. And hopefully the trick is enough because I guess that at runtime through the Python interpreter all you create use the subtype_dealloc function which is skipping through all the inheritance till it reaches some class with another allocator which will hopefully be a base type not extending anything else or not using the trashcan macros.
The problem to deal with here is that each layer of Cython type will have its own dealloc and without any knowledge on the depth of the type tower we cannot so easily trick the trashcan macros indeed... But shouldn't we just ignore such extensions? i.e. if we detect we are in the Cython world and giving hand to another Cython deallocator, we should not trick the trashcan magic by reincrementing _Py_Trash_delete_nesting until we hit a non Cython type.
comment:4 Changed 9 years ago by
This is now http://trac.cython.org/cython_trac/ticket/797
comment:5 in reply to: ↑ 3 ; follow-up: ↓ 6 Changed 9 years ago by
Replying to jpflori:
But shouldn't we just ignore such extensions? i.e. if we detect we are in the Cython world and giving hand to another Cython deallocator, we should not trick the trashcan magic by reincrementing _Py_Trash_delete_nesting until we hit a non Cython type.
The problem there is that if the type gets trashcanned halfway up the tower, the pointer would be added to the trashcan we'd be returning all the way up to the leaf class dealloc instance. That would decref and free the memory block. Once the trashcan gets emptied, the pointer (now masquerading as an instance of one of the supers) gets processed again and again decreffed and freed. That's the double-free we're trying to avoid. For cython, the appropriate solution is probably something along the lines of
Py_TRASHCAN_SAFE_BEGIN(self); ... Clear_Weakrefs() PY_CLEAR(slots) ... --_PyTrash_delete_nesting; super.dealloc(self) ++_PyTrash_delete_nesting; ... Py_TRASHCAN_SAFE_END(self);
i.e., PY_CLEAR
etc. can lead to trashcanning, but not the calling of the dealloc
of the super class. The python code figured that left a loophole open, but I think in cython that's not a loophole but a necessity: If you nest your inheritance deeper than the C-stack, you're asking for it.
comment:6 in reply to: ↑ 5 Changed 9 years ago by
Replying to nbruin:
Replying to jpflori:
But shouldn't we just ignore such extensions? i.e. if we detect we are in the Cython world and giving hand to another Cython deallocator, we should not trick the trashcan magic by reincrementing _Py_Trash_delete_nesting until we hit a non Cython type.
The problem there is that if the type gets trashcanned halfway up the tower, the pointer would be added to the trashcan we'd be returning all the way up to the leaf class dealloc instance. That would decref and free the memory block. Once the trashcan gets emptied, the pointer (now masquerading as an
I think I meant the other way around, that is emulating for Cython classes what subtype_dealloc does to skip the subtype themselves using subtype_dealloc. This would ensure that a tower of Cython classes gets deallocated at once, even though the objects pointed to (which typically could lead to stack blow up) can be trashcan, just as is the case with a tower of CPython types using subtype_dealloc.
Of course implementing that will be more complicated than in the subtype_dealloc, because we have to know we are at the top and the kind of types above us. And at each level we have some specific action to take (what is not the case with subtype_dealloc).
Not sure in fact now that the esaiest way is not to first check the depth of the type tower and use exactly the same trick as CPython but with that depth instead of 1 when decr/incrementing artificially the trashcan depth counter...
comment:7 Changed 9 years ago by
- Description modified (diff)
- Report Upstream changed from Reported upstream. No feedback yet. to Reported upstream. Developers acknowledge bug.
comment:8 Changed 9 years ago by
- Milestone changed from sage-5.11 to sage-5.12
comment:9 Changed 8 years ago by
- Milestone changed from sage-6.1 to sage-6.2
comment:10 Changed 8 years ago by
- Milestone changed from sage-6.2 to sage-6.3
comment:11 Changed 8 years ago by
- Milestone changed from sage-6.3 to sage-6.4
script to cause a C stack overflow.