Skip to content

The garbage collector doesn't take in account that objects of heap allocated types hold a strong reference to their type #84398

Closed

Description

@vstinner
BPO 40217
Nosy @tim-one, @vstinner, @encukou, @ambv, @serhiy-storchaka, @corona10, @pablogsal, @miss-islington, @shihai1991
PRs
  • bpo-40217: Ensure Py_VISIT(Py_TYPE(self)) is always called for PyType_FromSpec types #19414
  • bpo-40217: Clean code in PyType_FromSpec_Alloc and add NEWS entry #19733
  • bpo-40217: Ensure Py_VISIT(Py_TYPE(self)) is always called for PyType_FromSpec types (reverts GH-19414) #20264
  • bpo-40217: Make heap types always visit Py_TYPE(self) from tp_traverse #20433
  • [3.9] bpo-40217: Ensure Py_VISIT(Py_TYPE(self)) is always called for PyType_FromSpec types (reverts GH-19414) (GH-20264) #20490
  • Files
  • reproducer.c
  • 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-06-01.07:35:10.755>
    created_at = <Date 2020-04-07.16:23:41.763>
    labels = ['interpreter-core', '3.9', 'release-blocker']
    title = "The garbage collector doesn't take in account that objects of heap allocated types hold a strong reference to their type"
    updated_at = <Date 2020-06-01.07:35:10.755>
    user = 'https://github.com/vstinner'

    bugs.python.org fields:

    activity = <Date 2020-06-01.07:35:10.755>
    actor = 'petr.viktorin'
    assignee = 'none'
    closed = True
    closed_date = <Date 2020-06-01.07:35:10.755>
    closer = 'petr.viktorin'
    components = ['Interpreter Core']
    creation = <Date 2020-04-07.16:23:41.763>
    creator = 'vstinner'
    dependencies = []
    files = ['49182']
    hgrepos = []
    issue_num = 40217
    keywords = ['patch']
    message_count = 44.0
    messages = ['365911', '365918', '365921', '365922', '365923', '365924', '365925', '365926', '365927', '365928', '365929', '365930', '365931', '365932', '365933', '365963', '365965', '367119', '367120', '367122', '367124', '367125', '367150', '367270', '367412', '367413', '367416', '367424', '367425', '367681', '368813', '369460', '369462', '369464', '369532', '369599', '369632', '369636', '369668', '369669', '369675', '369713', '370057', '370225']
    nosy_count = 9.0
    nosy_names = ['tim.peters', 'vstinner', 'petr.viktorin', 'lukasz.langa', 'serhiy.storchaka', 'corona10', 'pablogsal', 'miss-islington', 'shihai1991']
    pr_nums = ['19414', '19733', '20264', '20433', '20490']
    priority = 'release blocker'
    resolution = 'fixed'
    stage = 'resolved'
    status = 'closed'
    superseder = None
    type = None
    url = 'https://bugs.python.org/issue40217'
    versions = ['Python 3.9']

    Activity

    1. vstinner commented on Apr 7, 2020

      @vstinner
      MemberAuthor

      The bpo-35810 modified the object allocate to hold a *strong* reference to the type in PyObject.ob_type, whereas PyObject.ob_type is a *borrowed* references if the type is statically allocated.

      commit 364f0b0
      Author: Eddie Elizondo <eduardo.elizondorueda@gmail.com>
      Date: Wed Mar 27 07:52:18 2019 -0400

      bpo-35810: Incref heap-allocated types in PyObject_Init (GH-11661)
      
      * Incref heap-allocated types in PyObject_Init
      * Add documentation and porting notes to What's New
      

      The problem is now in some corner cases, the GC fails to visit all referrer of a type and so considers that the type is still alive.

      bpo-40149 is a concrete example of such bug.

      I propose to modify the GC to take bpo-35810 in account.

      ... or maybe I just misunderstood bpo-40149 bug :-)

    2. pablogsal commented on Apr 7, 2020

      @pablogsal
      Member

      I propose to modify the GC to take bpo-35810 in account.

      What? The GC is agnostic of what it receives. It works with objects in general that implement the gc support, but it does not care about what those objects are. The only specal case are weakreferences and because those have inherit GC semantics.

      I am not sure about what you are proposing, could you elaborate?

    3. pablogsal commented on Apr 7, 2020

      @pablogsal
      Member

      Also, heap types (created with type_new()) are already taking into account the type when visiting references:

      https://github.com/python/cpython/blob/master/Objects/typeobject.c#L1111

    4. tim-one commented on Apr 7, 2020

      @tim-one
      Member

      If object A owns a strong reference to object B, and A participates in cyclic gc, and B may be part of a cycle, then it's necessary and sufficient that A's type's tp_traverse implementation invoke Py_VISIT() passing A's pointer to B.

      It would be a Really Bad Idea to add special cases to the gc module to spare some specific type(s) from following that (currently) utterly uniform rule.

    5. serhiy-storchaka commented on Apr 7, 2020

      @serhiy-storchaka
      Member

      It would be inconvenient to require adding Py_VISIT(Py_TYPE(self)) in all tp_visit implementations of heap allocated types (including third-party extensions). Since tp_visit is GC specific thing, I think it wold be more convenient make a special case for object types.

    6. pablogsal commented on Apr 7, 2020

      @pablogsal
      Member

      >> Since tp_visit is GC specific thing, I think it wold be more convenient make a special case for object types.

      I don't think that follows: The gc defers the logic of what and what should not be visited to the tp_traverse implementation of every object. The GC must be agnostic of the types it receives and heap types here are not special.

      --

      Also, if we make an exception in the GC for this special case, all tp_traverse of all those functions will be incorrect. Someone reading those tp_traverse can say "Oh, why these functions do not visit the type if they own s reference to it, this looks incorrect" and then we need to explain that that is an exception in the GC just because we were lazy to implement them correctly.

    7. serhiy-storchaka commented on Apr 7, 2020

      @serhiy-storchaka
      Member

      Interesting, this issue may be related to bpo-24379. The problem with the proposed implementation of subscript was that it created a reference loop, and not all links in this loop were visible by GC.

    8. pablogsal commented on Apr 7, 2020

      @pablogsal
      Member

      Also, as I mentioned, you don't need to modify all objects tp_traverse, only it's type.tp_traverse slot. For instance, all python objects know how to traverse stuff because they share the same tp_traverse:

      https://github.com/python/cpython/blob/master/Objects/typeobject.c#L1082

      So unless I am missing something, if you want to affect all heap types you just need to modify one tp_traverse in one place: the superclass.

    9. pablogsal commented on Apr 7, 2020

      @pablogsal
      Member

      Hummm, I think we may just be missing a Py_VISIT(Py_TYPE(self))here:

      https://github.com/python/cpython/blob/master/Objects/typeobject.c#L3562

      A class owns references to its type (type) or another metaclass

      Tim, could you confirm that that is the case?

    10. pablogsal commented on Apr 7, 2020

      @pablogsal
      Member

      Hummm, I think we may just be missing a Py_VISIT(Py_TYPE(self))here:

      Checking it more closely, that is incorrect, so we are not missing that visitation :(

    11. serhiy-storchaka commented on Apr 7, 2020

      @serhiy-storchaka
      Member

      Recently many static allocated types were converted to heap allocated types (using PyType_FromSpec). Now you need to add Py_VISIT(Py_TYPE(self)) in all corresponding tp_visit implementations.

      And since even official example for heap allocated types do not contain it (and it was not needed before 3.9), I am sure that all existing code do not contain it.

      We cannot change all user code, so we should change the interpreter code so that it will work correctly with existing user code.

    12. 31 remaining items

    13. encukou commented on May 22, 2020

      @encukou
      Member

      Ha! I think I found the issue in PySide now. It's different, but it's still a CPython issue. It's actually a variant of the "defining class" problem from PEP-573.

      It's legal to get the value of a slot, using PyType_GetSlot.
      It's reasonable to assume that what you put in the tp_traverse slot is what you'll get out using PyType_GetSlot.
      If a type's tp_traverse tries to call its superclass's tp_traverse, you can get an infinite loop.
      See the situation below or run attached reproducer (build the module, import it and exit
      the interpreter).

      Getting one's superclass is a bit tricky when dealing entirely with FromSpec
      types, but it is definitely *possible*. There's a lot of assumptions the code
      can reasonably make. In my reproducer, I stored the superclass in a C static
      variable (which is perfectly valid if the module cleanly refuses to work with
      subinterpreters or interpreter reload).

      Base:

      • real tp_traverse is PyType_FromSpec_tp_traverse
      • user_provided_tp_traverse is the base's original

      Subclass:

      • real tp_traverse is PyType_FromSpec_tp_traverse
      • user_provided_tp_traverse is the subclass' original

      So when the subclass is traversed:

      • subclass real tp_traverse calls the subclass user_provided_tp_traverse
      • subclass user_provided_tp_traverse calls PyType_GetSlot(base, tp_traverse)
        • that is, the base's real tp_traverse
      • the base's real tp_traverse calls the **SUBCLASS** user_provided_tp_traverse,
        since that's what's recorded in type(self)

      Another issue is that PyType_FromSpec_Allocated types lie about their size:
      tp->tp_basicsize + Py_SIZE(self) * tp->itemsize is not the actual
      allocated amount. This could technically be solved by redefining __sizeof__,
      but I'm more worried that it's another edge case of a hack, and There will
      probably be other edge cases. Since this is API we want to build on, I'd sleep
      easier if it's kept clean.

      What would be the downsides of reverting and documenting that tp_traverse
      must visit Py_TYPE(self)?
      It seems that you visit Py_TYPE(self), it could just end up with unbreakable
      reference cycles. We're dealing with modules and classes, which are usually
      effectively immortal (and if not, the author probably knows what they're doing).
      I don't think it would hurt that much.

    14. pablogsal commented on May 22, 2020

      @pablogsal
      Member

      What would be the downsides of reverting and documenting that tp_traverse must visit Py_TYPE(self)?

      Not much IMHO, I actually would prefer this solution over any automatic "hacks". The reason is that any hack will be technically violating the contract: if I read the tp_traverse of any of these classes I would say "wait a minute....this is wrong: the class owns a strong reference to its parent so...why is not visiting it?". But the answer would be that we are hacking because we didn't want to force users to migrate once we added the strong reference.

      Notice that the root of this problem was bpo-35810. In that issue we added a guide to the docs: https://docs.python.org/3/whatsnew/3.8.html#changes-in-the-c-api. In there it says that you should decref now the parent on tp_dealloc:

      static void
      foo_dealloc(foo_struct *instance) {
          PyObject *type = Py_TYPE(instance);
          PyObject_GC_Del(instance);
      #if PY_VERSION_HEX >= 0x03080000
          // This was not needed before Python 3.8 (Python issue 35810)
          Py_DECREF(type);
      #endif
      }

      but it forgot about that you should also visit the parent in tp_traverse.

      So, I propose to do the following:

      • Revert the hack.
      • Fix the tp_traverse of all the relevant classes in the stdlib to correctly visit its parent.
      • Modify the documentation of master, 3.9 and 3.8 to add the missing information: You MUST visit the parent in tp_traverse:

      What do you think?

    15. pablogsal commented on May 22, 2020

      @pablogsal
      Member

      I have been playing with the reproducer and I am a bit confused: The reproducer crashes in the same way even after reverting PR 19414 so it does not seem related to it. This is what I get:

      >>> import reproducer
      >>> reproducer.Modules/gcmodule.c:114: gc_decref: Assertion "gc_get_refs(g) > 0" failed: refcount is too small
      Enable tracemalloc to get the memory block allocation traceback

      object address : 0x55cf429e4080
      object refcount : 4
      object type : 0x55cf418e9c60
      object type name: type
      object repr : <class 'reproducer.Base'>

      Fatal Python error: _PyObject_AssertFailed: _PyObject_AssertFailed
      Python runtime state: initialized

      Current thread 0x00007fc65d2f8740 (most recent call first):
      File "<frozen importlib._bootstrap>", line 917 in _find_spec
      File "<frozen importlib._bootstrap>", line 982 in _find_and_load_unlocked
      File "<frozen importlib._bootstrap>", line 1007 in _find_and_load
      File "/home/pablogsal/github/python/master/Lib/re.py", line 124 in <module>
      File "<frozen importlib._bootstrap>", line 228 in _call_with_frames_removed
      File "<frozen importlib._bootstrap_external>", line 790 in exec_module
      File "<frozen importlib._bootstrap>", line 680 in _load_unlocked
      File "<frozen importlib._bootstrap>", line 986 in _find_and_load_unlocked
      File "<frozen importlib._bootstrap>", line 1007 in _find_and_load
      File "/home/pablogsal/github/python/master/Lib/rlcompleter.py", line 142 in attr_matches
      File "/home/pablogsal/github/python/master/Lib/rlcompleter.py", line 89 in complete
      [1] 162397 abort (core dumped) ./python

      This seems to indicate that that reproducer is already implementing incorrectly tp_traverse.

    16. pablogsal commented on May 22, 2020

      @pablogsal
      Member

      Ok, I found the problem. The problem is that the reproduced does not correctly work the reference count of base_class because when construction get tuple of bases:

      PyObject *bases = PyTuple_New(1);
      result = PyTuple_SetItem(bases, 0, base_class);
      if (result) return -1;
      
      PyObject *subclass = PyType_FromModuleAndSpec(m, &subclass_spec, bases);

      "PyTuple_SetItem" steals a reference to base_class but "PyModule_AddObject" also does the same, and the refcount is incorrect.

      If you add a Py_INCREF before, the crash disappears.

    17. pablogsal commented on May 23, 2020

      @pablogsal
      Member

      If you add a Py_INCREF before, the crash disappears.

      To be clear: the other crash is still in the reproduced: the one that Petr describes in his comment. In PR 20264 I have prepared the changes that I proposed previously (including the revert of the hack). Petr, do you mind reviewing it?

    18. encukou commented on May 23, 2020

      @encukou
      Member

      Thank you for responding so quickly!

      Petr, do you mind reviewing it?

      Yes, but I'll only get to it next week.

    19. miss-islington commented on May 27, 2020

      @miss-islington
      Contributor

      New changeset 1cf15af by Pablo Galindo in branch 'master':
      bpo-40217: Ensure Py_VISIT(Py_TYPE(self)) is always called for PyType_FromSpec types (reverts #63613) (GH-20264)
      1cf15af

    20. miss-islington commented on May 28, 2020

      @miss-islington
      Contributor

      New changeset bcbe5c5 by Miss Islington (bot) in branch '3.9':
      bpo-40217: Ensure Py_VISIT(Py_TYPE(self)) is always called for PyType_FromSpec types (reverts #63613) (GH-20264)
      bcbe5c5

    21. transferred this issue fromon Apr 10, 2022
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    No one assigned

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions