-
Notifications
You must be signed in to change notification settings - Fork 245
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Class netrefs return wrong __class__ value #355
Comments
It looks like the source of the problem is in I was going to attempt to patch this myself, but the solution isn't immediately clear because when |
For brevity, I'm going to focus on ideal behavior compared to current behavior.
Of course, these definitions are specific to the context of RPyC Steps to reproduce with Python3:
Running with
The most intuitive behavior for the last line:
This is where transparency and secure default behavior collide. The special attribute So, an ideal solution might be:
tl;dr versions |
Thanks for the detailed response to this issue! However, the results from the examples you listed as "# looks good" are still problematic, regardless of whether it is netrefs or local objects that are returned. The fundamental problem is that in Python Also, for what it's worth, in our code base we are relying heavily on the builtins being represented as local classes rather than netrefs. For us the safety concerns you listed don't apply, so if that workflow stops being supported I'm afraid we'll be forced to fork and diverge from the officially supported rpyc going forward. |
No worries, the next release should support your use (the details that determine backwards compatibility are TBD). If a CVE is okay then upgrading to version I actually have nothing the behavior of If Since the implementation |
Note, this is my top priority issue. I'll see if I can get a fix within the week and thanks for reporting it 🥇 I apologize if my writing is ever brusque---I appreciate your feedback. Still working on the finer details for my mental model of RPyC (as a new-ish maintainer). I have a better understanding of your use case, a better understanding of my mistakes, and should have a fix for the bug that does not break other use cases. I'll keep you updated. |
Thanks so much! I appreciate all your help and your detailed responses. Will the fix work for Python 2.7 or will it be Python 3 only? |
I think master should have a fix for this issue for Python 2.7, 3.6, 3.7, and 3.8-dev. I did add a unit test under If you're curious about the exact changes (since I failed to mention this issue in all of the commits): diff --git a/rpyc/core/netref.py b/rpyc/core/netref.py
index 39fb905..5fdfae3 100644
--- a/rpyc/core/netref.py
+++ b/rpyc/core/netref.py
@@ -229,9 +229,15 @@ class BaseNetref(with_metaclass(NetrefMetaclass, object)):
elif other.____id_pack__[2] != 0:
return True
else:
+ # seems dubious if each netref proxies to a different address spaces
return syncreq(self, consts.HANDLE_INSTANCECHECK, other.____id_pack__)
else:
- return isinstance(other, self.__class__)
+ if self.____id_pack__[2] == 0:
+ # outside the context of `__instancecheck__`, `__class__` is expected to be type(self)
+ # within the context of `__instancecheck__`, `other` should be compared to the proxied class
+ return isinstance(other, type(self).__dict__['__class__'].instance)
+ else:
+ raise TypeError("isinstance() arg 2 must be a class, type, or tuple of classes and types")
def _make_method(name, doc):
@@ -271,6 +277,32 @@ def _make_method(name, doc):
return method
+class NetrefClass(object):
+ """a descriptor of the class being proxied
+
+ Future considerations:
+ + there may be a cleaner alternative but lib.compat.with_metaclass prevented using __new__
+ + consider using __slot__ for this class
+ + revisit the design choice to use properties here
+ """
+ def __init__(self, class_obj):
+ self._class_obj = class_obj
+
+ @property
+ def instance(self):
+ """accessor to class object for the instance being proxied"""
+ return self._class_obj
+
+ @property
+ def owner(self):
+ """accessor to the class object for the instance owner being proxied"""
+ return self._class_obj.__class__
+
+ def __get__(self, netref_instance, netref_owner):
+ """the value returned when accessing the netref class is dictated by whether or not an instance is proxied"""
+ return self.owner if netref_instance.____id_pack__[2] == 0 else self.instance
+
+
def class_factory(id_pack, methods):
"""Creates a netref class proxying the given class
@@ -281,30 +313,30 @@ def class_factory(id_pack, methods):
"""
ns = {"__slots__": (), "__class__": None}
name_pack = id_pack[0]
- if name_pack is not None: # attempt to resolve against builtins and sys.modules
- ns["__class__"] = _normalized_builtin_types.get(name_pack)
- if ns["__class__"] is None:
- _module = None
- didx = name_pack.rfind('.')
- if didx != -1:
- _module = sys.modules.get(name_pack[:didx])
- if _module is not None:
- _module = getattr(_module, name_pack[didx + 1:], None)
- else:
- _module = sys.modules.get(name_pack)
- else:
- _module = sys.modules.get(name_pack)
- if _module:
- if id_pack[2] == 0:
- ns["__class__"] = _module
- else:
- ns["__class__"] = getattr(_module, "__class__", None)
-
+ class_descriptor = None
+ if name_pack is not None:
+ # attempt to resolve __class__ using sys.modules (i.e. builtins and imported modules)
+ _module = None
+ cursor = len(name_pack)
+ while cursor != -1:
+ _module = sys.modules.get(name_pack[:cursor])
+ if _module is None:
+ cursor = name_pack.rfind('.')
+ continue
+ _class_name = name_pack[cursor + 1:]
+ _class = getattr(_module, _class_name, None)
+ if _class is not None and hasattr(_class, '__class__'):
+ class_descriptor = NetrefClass(_class)
+ break
+ ns['__class__'] = class_descriptor
+ netref_name = class_descriptor.owner.__name__ if class_descriptor is not None and id_pack[2] == 0 else name_pack
+ # create methods that must perform a syncreq
for name, doc in methods:
name = str(name) # IronPython issue #10
+ # only create methods that wont shadow BaseNetref during merge for mro
if name not in LOCAL_ATTRS: # i.e. `name != __class__`
ns[name] = _make_method(name, doc)
- return type(name_pack, (BaseNetref,), ns)
+ return type(netref_name, (BaseNetref,), ns)
for _builtin in _builtin_types:
diff --git a/rpyc/core/protocol.py b/rpyc/core/protocol.py
index 3c2a974..e4ed651 100644
--- a/rpyc/core/protocol.py
+++ b/rpyc/core/protocol.py
@@ -305,15 +305,19 @@ class Connection(object):
def _netref_factory(self, id_pack): # boxing
"""id_pack is for remote, so when class id fails to directly match """
- if id_pack[0] in netref.builtin_classes_cache:
+ cls = None
+ if id_pack[2] == 0 and id_pack in self._netref_classes_cache:
+ cls = self._netref_classes_cache[id_pack]
+ elif id_pack[0] in netref.builtin_classes_cache:
cls = netref.builtin_classes_cache[id_pack[0]]
- elif id_pack[1] in self._netref_classes_cache:
- cls = self._netref_classes_cache[id_pack[1]]
- else:
+ if cls is None:
# in the future, it could see if a sys.module cache/lookup hits first
cls_methods = self.sync_request(consts.HANDLE_INSPECT, id_pack)
cls = netref.class_factory(id_pack, cls_methods)
- self._netref_classes_cache[id_pack[1]] = cls
+ if id_pack[2] == 0:
+ # only use cached netrefs for classes
+ # ... instance caching after gc of a proxy will take some mental gymnastics
+ self._netref_classes_cache[id_pack] = cls
return cls(self, id_pack)
def _dispatch_request(self, seq, raw_args): # dispatch
diff --git a/rpyc/lib/__init__.py b/rpyc/lib/__init__.py
index 33513ac..6d7e967 100644
--- a/rpyc/lib/__init__.py
+++ b/rpyc/lib/__init__.py
@@ -151,12 +151,21 @@ def exp_backoff(collision):
def get_id_pack(obj):
- """introspects the given (local) object, returns id_pack as expected by BaseNetref"""
- if not inspect.isclass(obj):
+ """introspects the given "local" object, returns id_pack as expected by BaseNetref
+
+ The given object is "local" in the sense that it is from the local cache. Any object in the local cache exists
+ in the current address space or is a netref. A netref in the local cache could be from a chained-connection.
+ To handle type related behavior properly, the attribute `__class__` is a descriptor for netrefs.
+
+ So, check thy assumptions regarding the given object when creating `id_pack`.
+ """
+ if hasattr(obj, '____id_pack__'):
+ # netrefs are handled first since __class__ is a descriptor
+ return obj.____id_pack__
+ elif not inspect.isclass(obj):
+ # TODO: this is wrong for modules
name_pack = '{0}.{1}'.format(obj.__class__.__module__, obj.__class__.__name__)
return (name_pack, id(type(obj)), id(obj))
- elif hasattr(obj, '____id_pack__'):
- return obj.____id_pack__
else:
name_pack = '{0}.{1}'.format(obj.__module__, obj.__name__)
return (name_pack, id(obj), 0) |
Thanks for making the changes so quickly! I'm hoping to be able to spend some time tomorrow to confirm that it all works for our use cases. |
I've done the first bit of testing, and it looks like it's mostly working. The example that I provided in my original post is working as expected, which is fantastic! Many thanks!!! However I'm now seeing a couple cases that seem to be behaving strangely and I don't know if they're related: Case 1:
Case 2:
|
Just checking in on this since it's been a couple months since the last update. Will it be possible to address the two cases I mentioned in my last comment? |
So, 4.1.3 was released just before writing this. Case 1 is fixed. Case 2, from test_netref_hierachy.py (just noticed the typo...) the lines can be un-commented to show that the base cases switched in terms of passing.
Iirc, the test failures would keep switching. Dropping Python 2 would make it a bit easier to suss out the root cause of the issue and hopefully fix case 2 completely. The plan is to take another crack at it eventually. |
Sorry I've gone so long without giving a response since your last update! Unfortunately I've been stuck on other priorities and haven't been able to devote any time until now. I just tested out Case 1 above with v4.1.5 and I'm still seeing the same results as before:
It's really strange though that this case from the test suite works correctly:
|
Found this issue while investigating a (possibly related) problem. All tests are performed on RPyC 5.0.1. First of all, about the issue @jtobiska described in the last comment - I believe that's observed because the After I add
My second issue is with this discrepancy:
I'd like I hope I'll have time further on this week, I'll try to solve these 2 issues (re @jtobiska 's issue, I'm not sure what's the correct approach, I don't think that adding all missing types to |
I'll have to take a look into this. I think sometime ago I was fixing issues related to isinstance. Some more examples #390 |
This seems like it could be an issue in how RPyC handles caching and iirc it does not cache the entire inheritance hierarchy. More over, if it fails to find it already cache it should try to locate it by other means which probably fails as well. Last time I worked on this it would fix some test and break others. There is probably logic errors in more than one place. I'll try to take another crack at this in the near future. |
…ce logic for Foo/Bar. Still a lot of cleanup and thought required for changes being made to NetrefMetaclass and BaseNetref that impact type checking. Even so, the direction looks promising
…ce logic for Foo/Bar. Still a lot of cleanup and thought required for changes being made to NetrefMetaclass and BaseNetref that impact type checking. Even so, the direction looks promising - Run actions for develop branch too.
@Jongy, any chance you want to take a look at my develop branch. Despite
The tests don't fail in the same way locally. Or anyone else, a second set of eyes would be make the fix move faster. |
As of release 4.1, it seems that netrefs that represent classes are returning incorrect values for the
__class__
attribute.type()
calls are also returning similarly incorrect results.We encountered this change in behavior when we attempted to upgrade from 3.3.0 to 4.1.2 and it caused a number of problems in code that was depending on the type handling to work as expected.
To complicate matters, it looks like the fix for #339 included a change to the implementation of
BaseNetref.__instancecheck__
that is now relying on this incorrect behavior. The last line of__instancecheck__
is now callingisinstance(other, self.__class__)
rather thanisinstance(other, self)
.self
represents the correct class to check, butself.__class__
is working becauseself.__class__
is incorrectly returningself
(or the local value ofself
, depending on whether the class is a builtin).Environment
The text was updated successfully, but these errors were encountered: