|  | 
|  | 1 | +from __future__ import annotations | 
|  | 2 | + | 
|  | 3 | +import gc | 
|  | 4 | +import types | 
|  | 5 | +import weakref | 
|  | 6 | + | 
|  | 7 | +import pytest | 
|  | 8 | + | 
|  | 9 | +import env  # noqa: F401 | 
|  | 10 | +from pybind11_tests import class_cross_module_use_after_one_module_dealloc as m | 
|  | 11 | + | 
|  | 12 | + | 
|  | 13 | +def delattr_and_ensure_destroyed(*specs): | 
|  | 14 | +    wrs = [] | 
|  | 15 | +    for mod, name in specs: | 
|  | 16 | +        wrs.append(weakref.ref(getattr(mod, name))) | 
|  | 17 | +        delattr(mod, name) | 
|  | 18 | + | 
|  | 19 | +    for _ in range(5): | 
|  | 20 | +        gc.collect() | 
|  | 21 | +        if all(wr() is None for wr in wrs): | 
|  | 22 | +            break | 
|  | 23 | +    else: | 
|  | 24 | +        pytest.fail( | 
|  | 25 | +            f"Could not delete bindings such as {next(wr for wr in wrs if wr() is not None)!r}" | 
|  | 26 | +        ) | 
|  | 27 | + | 
|  | 28 | + | 
|  | 29 | +@pytest.mark.skipif("env.PYPY or env.GRAALPY") | 
|  | 30 | +def test_cross_module_use_after_one_module_dealloc(): | 
|  | 31 | +    # This is a regression test for a bug that occurred during development of | 
|  | 32 | +    # internals::registered_types_cpp_fast (see #5842). registered_types_cpp_fast maps | 
|  | 33 | +    # &typeid(T) to a raw non-owning pointer to a Python metaclass. If two DSOs both | 
|  | 34 | +    # look up the same global type, they will create two separate entries in | 
|  | 35 | +    # registered_types_cpp_fast, which will look like: | 
|  | 36 | +    # +=======================================+ | 
|  | 37 | +    # |&typeid(T) from DSO 1|metaclass pointer| | 
|  | 38 | +    # |&typeid(T) from DSO 2|metaclass pointer| | 
|  | 39 | +    # +=======================================+ | 
|  | 40 | +    # | 
|  | 41 | +    # Then, if the metaclass is destroyed and we don't take extra steps to clean up the | 
|  | 42 | +    # table thoroughly, the first row of the table will be cleaned up but the second one | 
|  | 43 | +    # will contain a dangling pointer to the old metaclass instance. Further lookups | 
|  | 44 | +    # from DSO 2 will then return that dangling pointer, which will cause use-after-frees. | 
|  | 45 | + | 
|  | 46 | +    import pybind11_cross_module_tests as cm | 
|  | 47 | + | 
|  | 48 | +    module_scope = types.ModuleType("module_scope") | 
|  | 49 | +    instance = m.register_and_instantiate_cross_dso_class(module_scope) | 
|  | 50 | +    cm.consume_cross_dso_class(instance) | 
|  | 51 | + | 
|  | 52 | +    del instance | 
|  | 53 | +    delattr_and_ensure_destroyed((module_scope, "CrossDSOClass")) | 
|  | 54 | + | 
|  | 55 | +    # Make sure that CrossDSOClass gets allocated at a different address. | 
|  | 56 | +    m.register_unrelated_class(module_scope) | 
|  | 57 | + | 
|  | 58 | +    instance = m.register_and_instantiate_cross_dso_class(module_scope) | 
|  | 59 | +    cm.consume_cross_dso_class(instance) | 
0 commit comments