fix(evdev): strip SW_RFKILL_ALL from uinput clone capabilities (#947)
The evdev uinput clone copied EV_SW capabilities verbatim. "ThinkPad Extra Buttons" (thinkpad_acpi) declares SW_RFKILL_ALL, and a fresh uinput device reports every switch as 0. The kernel rfkill-input handler reads a 0 killswitch as radios-off, so every clone creation soft-blocked all radios at startup and again after every resume. Strip SW_RFKILL_ALL from the clone capability list, dropping EV_SW entirely when it was the only switch. Other switches stay declared so real switch events still forward; a forwarded SW_RFKILL_ALL write is dropped by the kernel for the undeclared code. Fixes #946
This commit is contained in:
@@ -1317,6 +1317,14 @@ class EvdevDeviceHub:
|
||||
EV_REP is filtered out so the clone never autorepeats: the physical
|
||||
device's own repeat events are forwarded verbatim, preserving its
|
||||
repeat rate without doubling repeats.
|
||||
|
||||
SW_RFKILL_ALL is stripped from EV_SW: a fresh uinput device reports
|
||||
every switch as 0, and the kernel's rfkill-input handler reads a 0
|
||||
killswitch as radios-off — cloning that bit soft-blocks all radios
|
||||
the moment the clone appears, on ThinkPads and any other device
|
||||
declaring it (#946). The clone keeps every other switch so real
|
||||
switch events still forward; a forwarded SW_RFKILL_ALL write is
|
||||
dropped by the kernel for the undeclared code.
|
||||
"""
|
||||
try:
|
||||
capabilities: dict[int, Sequence[int]] = {
|
||||
@@ -1324,6 +1332,13 @@ class EvdevDeviceHub:
|
||||
}
|
||||
for event_type in (ecodes.EV_SYN, ecodes.EV_FF, ecodes.EV_REP):
|
||||
capabilities.pop(event_type, None)
|
||||
switches = [
|
||||
code for code in capabilities.get(ecodes.EV_SW, ()) if code != ecodes.SW_RFKILL_ALL
|
||||
]
|
||||
if switches:
|
||||
capabilities[ecodes.EV_SW] = switches
|
||||
else:
|
||||
capabilities.pop(ecodes.EV_SW, None)
|
||||
name = _clone_device_name(device)
|
||||
try:
|
||||
return UInput(
|
||||
|
||||
@@ -1362,6 +1362,69 @@ class TestEvdevGrabAndForwarding:
|
||||
assert backend._forwarders[10] is forwarder
|
||||
assert device in backend.devices
|
||||
|
||||
@patch("vocalinux.ui.keyboard_backends.evdev_backend.UInput")
|
||||
@patch("vocalinux.ui.keyboard_backends.evdev_backend.InputDevice")
|
||||
def test_clone_drops_rfkill_switch_keeps_other_switches(
|
||||
self, mock_input_device: Mock, mock_uinput: Mock
|
||||
) -> None:
|
||||
"""The clone must not declare SW_RFKILL_ALL (#946).
|
||||
|
||||
"ThinkPad Extra Buttons" declares SW_RFKILL_ALL. A fresh uinput
|
||||
device reports every switch as 0, and the kernel's rfkill-input
|
||||
handler reads a 0 killswitch as radios-off — cloning the bit
|
||||
soft-blocked all radios on every startup and resume. Other
|
||||
switches stay declared so real switch events still forward.
|
||||
"""
|
||||
device = MagicMock()
|
||||
device.fileno.return_value = 10
|
||||
device.active_keys.return_value = []
|
||||
device.capabilities.return_value = {
|
||||
ecodes.EV_KEY: [30], # KEY_A
|
||||
ecodes.EV_SW: [ecodes.SW_RFKILL_ALL, ecodes.SW_TABLET_MODE],
|
||||
}
|
||||
mock_input_device.return_value = device
|
||||
forwarder = MagicMock()
|
||||
forwarder.device.path = "/dev/input/event20"
|
||||
mock_uinput.return_value = forwarder
|
||||
|
||||
backend = EvdevKeyboardBackend()
|
||||
opened = backend._open_keyboard_device("/dev/input/event0")
|
||||
|
||||
assert opened is True
|
||||
events = mock_uinput.call_args.kwargs["events"]
|
||||
assert ecodes.SW_RFKILL_ALL not in events[ecodes.EV_SW]
|
||||
assert ecodes.SW_TABLET_MODE in events[ecodes.EV_SW]
|
||||
|
||||
@patch("vocalinux.ui.keyboard_backends.evdev_backend.UInput")
|
||||
@patch("vocalinux.ui.keyboard_backends.evdev_backend.InputDevice")
|
||||
def test_clone_drops_ev_sw_when_only_switch_is_rfkill(
|
||||
self, mock_input_device: Mock, mock_uinput: Mock
|
||||
) -> None:
|
||||
"""A device declaring only SW_RFKILL_ALL gets no EV_SW at all (#946).
|
||||
|
||||
ThinkPad Extra Buttons reports SW=8: bit 3 alone. Declaring an
|
||||
empty EV_SW capability would still register a switch device, so
|
||||
the type is dropped entirely.
|
||||
"""
|
||||
device = MagicMock()
|
||||
device.fileno.return_value = 10
|
||||
device.active_keys.return_value = []
|
||||
device.capabilities.return_value = {
|
||||
ecodes.EV_KEY: [30], # KEY_A
|
||||
ecodes.EV_SW: [ecodes.SW_RFKILL_ALL],
|
||||
}
|
||||
mock_input_device.return_value = device
|
||||
forwarder = MagicMock()
|
||||
forwarder.device.path = "/dev/input/event20"
|
||||
mock_uinput.return_value = forwarder
|
||||
|
||||
backend = EvdevKeyboardBackend()
|
||||
opened = backend._open_keyboard_device("/dev/input/event0")
|
||||
|
||||
assert opened is True
|
||||
events = mock_uinput.call_args.kwargs["events"]
|
||||
assert ecodes.EV_SW not in events
|
||||
|
||||
@patch("vocalinux.ui.keyboard_backends.evdev_backend.UInput")
|
||||
@patch("vocalinux.ui.keyboard_backends.evdev_backend.InputDevice")
|
||||
@patch("vocalinux.ui.keyboard_backends.evdev_backend.os.path.exists")
|
||||
|
||||
Reference in New Issue
Block a user