From 8a8548de607e0488fd4b8ab05373749545a29342 Mon Sep 17 00:00:00 2001
From: Mark Johnston <markj@FreeBSD.org>
Date: Thu, 24 Aug 2023 17:43:36 -0400
Subject: [PATCH] python bindings: Load all modules with RTLD_GLOBAL

libdnf's python bindings are implemented by a set of C++ shared objects
generated by swig.  Some generated code is duplicated between modules,
in particular the SwigPyIterator class templates, which use exceptions
of type swig::stop_iteration to signal an end-of-iteration condition.
The modules do not depend on each other and thus belong to different
DAGs from the perspective of the runtime linker.

It turns out that this stop_iteration exception can be thrown between
modules.  This happens at least during dnf startup with python 3.9:

cli.py(935):         subst.update_from_etc(from_root, varsdir=conf._get_value('varsdir'))
 --- modulename: config, funcname: _get_value
config.py(102):         method = getattr(self._config, name, None)
config.py(103):         if method is None:
config.py(105):         return method().getValue()
 --- modulename: conf, funcname: varsdir
conf.py(1183):         return _conf.ConfigMain_varsdir(self)
 --- modulename: conf, funcname: getValue
conf.py(512):         return _conf.OptionStringList_getValue(self)
 --- modulename: substitutions, funcname: update_from_etc
substitutions.py(47):         for vars_path in varsdir:
 --- modulename: module, funcname: __iter__
module.py(557):         return self.iterator()
 --- modulename: module, funcname: iterator
module.py(555):         return _module.VectorString_iterator(self)
 --- modulename: transaction, funcname: __next__
transaction.py(94):         return _transaction.SwigPyIterator___next__(self)

In particular, the module and transaction modules are somehow both
involved: module returns the iterator, and transaction advances the
iterator.  Both modules contain the same iterator code, so I'm not sure
why it works this way.  The behaviour is sensitive to import order; for
example, if transaction is imported before module, then the code above
ends up using module's implementation of SwigPyItreator___next__.

In any case, the use of swig::stop_iteration is broken in the above
scenario since the exception is thrown by module with module.so's copy
of the swig::stop_iteration type info, and caught by transaction.so
using transaction.so's copy of the type info, resulting in an uncaught
exception.

Work around the problem by loading all modules with RTLD_GLOBAL to
ensure that RTTI is unique.  This is required when throwing exceptions
across DSO boundaries, see https://gcc.gnu.org/faq.html#dso for example.

Conflict:NA
Reference:https://github.com/rpm-software-management/libdnf/commit/8a8548de607e0488fd4b8ab05373749545a29342
---
 bindings/python/__init__.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/bindings/python/__init__.py b/bindings/python/__init__.py
index 3bfd3a9441..07cf1959d5 100644
--- a/bindings/python/__init__.py
+++ b/bindings/python/__init__.py
@@ -6,11 +6,14 @@
 import sys, os
 sys.setdlopenflags(os.RTLD_NOW | os.RTLD_GLOBAL)
 from . import error
-sys.setdlopenflags(os.RTLD_NOW)
 
+# Other modules also need to be loaded with RTLD_GLOBAL to preserve uniqueness
+# of RTTI.  There are code paths where an exception thrown in one module is
+# supposed to be caught in another.
 from . import common_types
 from . import conf
 from . import module
 from . import repo
 from . import transaction
 from . import utils
+sys.setdlopenflags(os.RTLD_NOW)
