From f469e86b9cfee2b1b20dfdb162b121dab6ae90a4 Mon Sep 17 00:00:00 2001 From: Ken Sanislo Date: Thu, 16 Apr 2026 23:36:47 -0700 Subject: [PATCH] Guard Centurion feature lookups against HID++ 2.0 enum collisions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IntEnum members with the same int value hash equal and compare equal, so a dict lookup for SupportedFeature.DEVICE_NAME (0x0005) finds an entry stored as CenturionCoreFeature.MULTI_HOST_CONTROL (same 0x0005). The index is right for the Centurion feature but wrong for the HID++ 2.0 feature the caller intended. Concrete impact: `solaar show` on wired G522 called get_kind() -> feature_request(DEVICE_NAME, 0x20), which resolved to MULTI_HOST_CONTROL.function_2 via the collision, and the device returned OUT_OF_RANGE -> FeatureCallError crashed `solaar show`. Fix: in Device.feature_request, after resolving the index, compare the type of the stored inverse entry against the type of the requested feature. Mismatched types mean the device actually has the Centurion variant, not the HID++ 2.0 feature — return None instead of issuing a mis-targeted request. --- lib/logitech_receiver/device.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/logitech_receiver/device.py b/lib/logitech_receiver/device.py index e3336455..c2aa08f7 100644 --- a/lib/logitech_receiver/device.py +++ b/lib/logitech_receiver/device.py @@ -647,6 +647,18 @@ class Device: # Ensure sub-device features are discovered before routing decision if self.features is not None: self.features._check() + # Guard against Centurion/HID++ 2.0 feature ID collisions. IntEnum + # members with the same int value hash equal, so a dict lookup for + # SupportedFeature.DEVICE_NAME (0x0005) succeeds even when the + # device actually has CenturionCoreFeature.MULTI_HOST_CONTROL at + # that slot. If the type of the stored enum differs from what the + # caller asked for, treat the feature as unsupported. + if self.features is not None: + idx = self.features.get(feature) + if idx is not None: + stored = self.features.inverse.get(idx) + if stored is not None and type(stored) is not type(feature): + return None if feature in getattr(self, "_centurion_sub_features", ()): sub_idx = self.features.get(feature) if sub_idx is not None: