Repository: modin-project/modin

Issue:
issubclass() hard-crashes for remote objects
### Describe the problem
When making calls like `issubclass(remote_obj, LocalClass)` Python interpreter crashes with memory corruption.

Further investigation shows that Python C part, when walking parent classes, is calling a decref on tuple of base classes _before_ using its first element if there is only one element (typical case of object A being descendent from only object B).
This doesn't cause problems for regular Python because class itself keeps references to its base classes, but in remote objects `__bases__` is a regular attribute accessed over network which is destroyed as soon as a decref is called on it.

### Source code / logs
Sample traceback (as printed when running tests on simulated cloud):
```
modin\pandas\test\test_io.py Windows fatal exception: code 0xc0000374

Current thread 0x0000bb5c (most recent call first):
  File "c:\miniconda3\envs\foo\lib\site-packages\rpyc\core\protocol.py", line 306 in _netref_factory
  File "c:\source\modin\modin\experimental\cloud\rpyc_proxy.py", line 189 in _netref_factory
  File "c:\miniconda3\envs\foo\lib\site-packages\rpyc\core\protocol.py", line 294 in _unbox
  File "c:\miniconda3\envs\foo\lib\site-packages\rpyc\core\protocol.py", line 285 in <genexpr>
  File "c:\miniconda3\envs\foo\lib\site-packages\rpyc\core\protocol.py", line 285 in _unbox
  File "c:\miniconda3\envs\foo\lib\site-packages\rpyc\core\protocol.py", line 360 in _dispatch
  File "c:\miniconda3\envs\foo\lib\site-packages\rpyc\core\protocol.py", line 392 in serve
  File "c:\miniconda3\envs\foo\lib\site-packages\rpyc\core\async_.py", line 47 in wait
  File "c:\miniconda3\envs\foo\lib\site-packages\rpyc\core\async_.py", line 100 in value
  File "c:\miniconda3\envs\foo\lib\site-packages\rpyc\core\protocol.py", line 469 in sync_request
  File "c:\source\modin\modin\experimental\cloud\rpyc_proxy.py", line 156 in sync_request
  File "c:\miniconda3\envs\foo\lib\site-packages\rpyc\core\netref.py", line 76 in syncreq
  File "c:\miniconda3\envs\foo\lib\site-packages\rpyc\core\netref.py", line 161 in __getattribute__
  File "c:\miniconda3\envs\foo\lib\site-packages\pandas\core\dtypes\common.py", line 208 in <lambda>
  File "c:\miniconda3\envs\foo\lib\site-packages\pandas\core\dtypes\common.py", line 1750 in _is_dtype_type
  File "c:\miniconda3\envs\foo\lib\site-packages\pandas\core\dtypes\common.py", line 1144 in is_datetime_or_timedelta_dtype
  File "c:\miniconda3\envs\foo\lib\site-packages\pandas\core\dtypes\common.py", line 1320 in needs_i8_conversion
  File "c:\miniconda3\envs\foo\lib\site-packages\pandas\_testing.py", line 1155 in assert_series_equal
  File "c:\miniconda3\envs\foo\lib\site-packages\pandas\_testing.py", line 1382 in assert_frame_equal
  File "c:\source\modin\modin\pandas\test\utils.py", line 506 in df_equals
  File "c:\source\modin\modin\pandas\test\test_io.py", line 431 in test_from_parquet
  File "c:\miniconda3\envs\foo\lib\site-packages\_pytest\python.py", line 182 in pytest_pyfunc_call
  File "c:\miniconda3\envs\foo\lib\site-packages\pluggy\callers.py", line 187 in _multicall
  File "c:\miniconda3\envs\foo\lib\site-packages\pluggy\manager.py", line 87 in <lambda>
  File "c:\miniconda3\envs\foo\lib\site-packages\pluggy\manager.py", line 93 in _hookexec
  File "c:\miniconda3\envs\foo\lib\site-packages\pluggy\hooks.py", line 286 in __call__
  File "c:\miniconda3\envs\foo\lib\site-packages\_pytest\python.py", line 1477 in runtest
  File "c:\miniconda3\envs\foo\lib\site-packages\_pytest\runner.py", line 135 in pytest_runtest_call
  File "c:\miniconda3\envs\foo\lib\site-packages\pluggy\callers.py", line 187 in _multicall
  File "c:\miniconda3\envs\foo\lib\site-packages\pluggy\manager.py", line 87 in <lambda>
  File "c:\miniconda3\envs\foo\lib\site-packages\pluggy\manager.py", line 93 in _hookexec
  File "c:\miniconda3\envs\foo\lib\site-packages\pluggy\hooks.py", line 286 in __call__
  File "c:\miniconda3\envs\foo\lib\site-packages\_pytest\runner.py", line 217 in <lambda>
  File "c:\miniconda3\envs\foo\lib\site-packages\_pytest\runner.py", line 244 in from_call
  File "c:\miniconda3\envs\foo\lib\site-packages\_pytest\runner.py", line 217 in call_runtest_hook
  File "c:\miniconda3\envs\foo\lib\site-packages\_pytest\runner.py", line 186 in call_and_report
  File "c:\miniconda3\envs\foo\lib\site-packages\_pytest\runner.py", line 100 in runtestprotocol
  File "c:\miniconda3\envs\foo\lib\site-packages\_pytest\runner.py", line 85 in pytest_runtest_protocol
  File "c:\miniconda3\envs\foo\lib\site-packages\pluggy\callers.py", line 187 in _multicall
  File "c:\miniconda3\envs\foo\lib\site-packages\pluggy\manager.py", line 87 in <lambda>
  File "c:\miniconda3\envs\foo\lib\site-packages\pluggy\manager.py", line 93 in _hookexec
  File "c:\miniconda3\envs\foo\lib\site-packages\pluggy\hooks.py", line 286 in __call__
  File "c:\miniconda3\envs\foo\lib\site-packages\_pytest\main.py", line 272 in pytest_runtestloop
  File "c:\miniconda3\envs\foo\lib\site-packages\pluggy\callers.py", line 187 in _multicall
  File "c:\miniconda3\envs\foo\lib\site-packages\pluggy\manager.py", line 87 in <lambda>
  File "c:\miniconda3\envs\foo\lib\site-packages\pluggy\manager.py", line 93 in _hookexec
  File "c:\miniconda3\envs\foo\lib\site-packages\pluggy\hooks.py", line 286 in __call__
  File "c:\miniconda3\envs\foo\lib\site-packages\_pytest\main.py", line 247 in _main
  File "c:\miniconda3\envs\foo\lib\site-packages\_pytest\main.py", line 191 in wrap_session
  File "c:\miniconda3\envs\foo\lib\site-packages\_pytest\main.py", line 240 in pytest_cmdline_main
  File "c:\miniconda3\envs\foo\lib\site-packages\pluggy\callers.py", line 187 in _multicall
  File "c:\miniconda3\envs\foo\lib\site-packages\pluggy\manager.py", line 87 in <lambda>
  File "c:\miniconda3\envs\foo\lib\site-packages\pluggy\manager.py", line 93 in _hookexec
  File "c:\miniconda3\envs\foo\lib\site-packages\pluggy\hooks.py", line 286 in __call__
  File "c:\miniconda3\envs\foo\lib\site-packages\_pytest\config\__init__.py", line 125 in main
  File "c:\miniconda3\envs\foo\lib\site-packages\pytest\__main__.py", line 7 in <module>
  File "c:\miniconda3\envs\foo\lib\runpy.py", line 85 in _run_code
  File "c:\miniconda3\envs\foo\lib\runpy.py", line 193 in _run_module_as_main
```


Discussion:

Resolving patch:
diff --git a/modin/experimental/cloud/rpyc_proxy.py b/modin/experimental/cloud/rpyc_proxy.py
--- a/modin/experimental/cloud/rpyc_proxy.py
+++ b/modin/experimental/cloud/rpyc_proxy.py
@@ -173,6 +173,37 @@ def async_request(self, handler, *args, **kw):
                 return res
         return super().async_request(handler, *args, **kw)
 
+    def __patched_netref(self, id_pack):
+        """
+        Default RPyC behaviour is to defer almost everything to be always obtained
+        from remote side. This is almost always correct except when Python behaves
+        strangely. For example, when checking for isinstance() or issubclass() it
+        gets obj.__bases__ tuple and uses its elements *after* calling a decref
+        on the __bases__, because Python assumes that the class type holds
+        a reference to __bases__, which isn't true for RPyC proxy classes, so in
+        RPyC case the element gets destroyed and undefined behaviour happens.
+
+        So we're patching RPyC netref __getattribute__ to keep a reference
+        for certain read-only properties to better emulate local objects.
+        """
+        result = super()._netref_factory(id_pack)
+        cls = type(result)
+        orig_getattribute = cls.__getattribute__
+        type.__setattr__(cls, "__readonly_cache__", {})
+
+        def __getattribute__(this, name):
+            if name in {"__bases__", "__base__", "__mro__"}:
+                cache = object.__getattribute__(this, "__readonly_cache__")
+                try:
+                    return cache[name]
+                except KeyError:
+                    res = cache[name] = orig_getattribute(this, name)
+                    return res
+            return orig_getattribute(this, name)
+
+        cls.__getattribute__ = __getattribute__
+        return result
+
     def _netref_factory(self, id_pack):
         id_name, cls_id, inst_id = id_pack
         id_name = str(id_name)
@@ -181,12 +212,12 @@ def _netref_factory(self, id_pack):
             try:
                 cached_cls = self._remote_cls_cache[(id_name, cls_id)]
             except KeyError:
-                result = super()._netref_factory(id_pack)
+                result = self.__patched_netref(id_pack)
                 self._remote_cls_cache[(id_name, cls_id)] = type(result)
             else:
                 result = cached_cls(self, id_pack)
         else:
-            result = super()._netref_factory(id_pack)
+            result = self.__patched_netref(id_pack)
         # try getting __real_cls__ from result.__class__ BUT make sure to
         # NOT get it from some parent class for result.__class__, otherwise
         # multiple wrappings happen


Test patch: