Several functions use the return value of PyObject_RichCompareBool or PyObject_IsTrue in a boolean if(...) context. Both APIs return -1 on error, which is truthy in C/C++ — comparison errors are silently treated as "true" matches.
1. _Disconnector_call — wrong slot disconnected (signaling.cpp:444-450)
slot.richcmp(other, Py_EQ) returns -1 on error, treated as truthy → the first slot in the list matches on comparison error, causing the wrong slot to be disconnected.
Reproducer:
import gc; gc.disable()
import os
from enaml.signaling import Signal
class BadSlot:
def __eq__(self, other):
raise TypeError("comparison bomb")
def __hash__(self):
return id(self)
def __call__(self): pass
class Host: pass
host = Host()
sig = Signal()
bound = sig.__get__(host, type(host))
bound.connect(lambda: None)
bound.connect(BadSlot())
try:
bound.disconnect(BadSlot())
except SystemError as e:
print(f"SystemError (BUG - error-as-truthy): {e}")
except TypeError as e:
print(f"TypeError propagated (correct): {e}")
os._exit(0)
# SystemError: <enaml.signaling._Disconnector object ...> returned a result with an exception set
2. BoundSignal_richcompare (signaling.cpp:622)
Same pattern — error during __eq__ treated as truthy.
3. CallableRef_richcompare (callableref.cpp:123,132)
Same pattern in both Py_EQ and Py_NE branches.
4. PyObject_IsTrue in SubscriptionObserver_call (subscription_observer.cpp:122)
PyObject_IsTrue(self->ref) returns -1 on error. Used in if(...) where -1 is truthy → proceeds with active exception.
Fix for all sites — check for error before branching:
int cmp = slot.richcmp( other, Py_EQ );
if( cmp < 0 )
return 0; // propagate exception
if( cmp ) { ... }
Found by cext-review-toolkit.
Several functions use the return value of
PyObject_RichCompareBoolorPyObject_IsTruein a booleanif(...)context. Both APIs return -1 on error, which is truthy in C/C++ — comparison errors are silently treated as "true" matches.1.
_Disconnector_call— wrong slot disconnected (signaling.cpp:444-450)slot.richcmp(other, Py_EQ)returns -1 on error, treated as truthy → the first slot in the list matches on comparison error, causing the wrong slot to be disconnected.Reproducer:
2.
BoundSignal_richcompare(signaling.cpp:622)Same pattern — error during
__eq__treated as truthy.3.
CallableRef_richcompare(callableref.cpp:123,132)Same pattern in both
Py_EQandPy_NEbranches.4.
PyObject_IsTrueinSubscriptionObserver_call(subscription_observer.cpp:122)PyObject_IsTrue(self->ref)returns -1 on error. Used inif(...)where -1 is truthy → proceeds with active exception.Fix for all sites — check for error before branching:
Found by cext-review-toolkit.