fix(evdev): require a real keyboard key in /proc keyboard discovery (#918)

* fix(evdev): require a real keyboard key in /proc keyboard discovery

Any nonzero B: KEY bit qualified a device as a keyboard, so power/sleep
buttons, the ACPI video bus, vendor hotkey blocks and pointer-button
interfaces were all grabbed and given (vocalinux) uinput clones, leaving
duplicate logical devices that confuse compositor input settings.

Require a modifier or KEY_A in the KEY bitmap — the same rule the evdev
fallback path already applies — and update the test fixtures to a real
keyboard bitmap so they exercise the intended gate.

Fixes #915

* fix(evdev): read /proc bitmaps at the kernel's word width

Capability bitmap words are unsigned longs — 32 bits on 32-bit kernels
(armv7l, i686), not 64. Parse the whole bitmap through _bitmap_to_int()
sized by platform.machine() so high key codes like KEY_RIGHTMETA land
correctly on both widths. Give pointer-test mocks real keyboard keys so
the pointer check — not the keyboard-key gate — drops them.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
Co-authored-by: Jatin K Malik <jk.malik@yahoo.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot]
2026-10-07 18:03:50 -07:00
committed by GitHub
co-authored by Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Jatin K Malik Jatin K Malik Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent ecd4ae8388
commit 06f475cf95
2 changed files with 126 additions and 26 deletions
@@ -290,12 +290,29 @@ def find_keyboard_devices() -> list[str]:
# which showed up as periodic cursor stutter (#900).
_POINTER_AXIS_BITS = 0x3
# Key codes only real keyboards report: the modifiers plus KEY_A — the
# same set the evdev fallback requires. Button-only devices (power/sleep
# buttons, the ACPI video bus, vendor hotkey blocks) never have one, so
# they stop being grabbed and cloned too (#915).
_KEYBOARD_KEY_CODES = frozenset(
{
KEY_LEFTCTRL,
KEY_RIGHTCTRL,
KEY_LEFTALT,
KEY_RIGHTALT,
KEY_LEFTSHIFT,
KEY_RIGHTSHIFT,
KEY_LEFTMETA,
KEY_RIGHTMETA,
30, # KEY_A
}
)
# Bits 53 and 54 of an ABS bitmap: ABS_MT_POSITION_X|ABS_MT_POSITION_Y.
# Multitouch-only touchpads and touchscreens (e.g. the Goodix GXTP5100 in
# #914) can report the MT pair without the single-touch ABS_X/Y pair.
_POINTER_MT_AXIS_BITS = 0x3 << 53
# Machines whose kernels print bitmap words as 32-bit unsigned longs;
# everything else (x86_64, aarch64, ppc64*, riscv64, s390x, ...) is 64-bit.
# On a 32-bit kernel bit N lives in a different word than on 64-bit, which
@@ -338,6 +355,11 @@ def _bitmap_has_any_bit(hex_bitmap: str) -> bool:
return _bitmap_to_int(hex_bitmap) != 0
def _bitmap_has_code(hex_bitmap: str, code: int) -> bool:
"""True when bit ``code`` is set in a /proc capability bitmap."""
return bool(_bitmap_to_int(hex_bitmap) & (1 << code))
def _bitmap_low_bits(hex_bitmap: str) -> int:
"""Bits 0-63 of a /proc capability bitmap, or 0.
@@ -357,13 +379,25 @@ def _proc_block_has_pointer_axes(device: dict[str, Any]) -> bool:
)
def _proc_block_has_keyboard_keys(device: dict[str, Any]) -> bool:
"""True when the block's KEY bitmap has a real keyboard key.
Any nonzero KEY bit used to qualify a block as a keyboard — but
touchpad buttons, power/sleep keys and ACPI hotkey blocks all report
EV_KEY codes too, so every one of them was grabbed and cloned (#915).
"""
key_bitmap = device.get("key", "")
return any(_bitmap_has_code(key_bitmap, code) for code in _KEYBOARD_KEY_CODES)
def _parse_keyboard_devices_from_proc(proc_file: TextIO) -> list[str]:
"""Parse an open /proc/bus/input/devices stream for event KEY devices.
Devices that emit pointer motion (REL_X/Y, ABS_X/Y or
ABS_MT_POSITION_X/Y) are skipped: mice and tablets report EV_KEY bits
for their buttons, but grabbing them proxies every cursor movement
through this reader.
Only blocks carrying a real keyboard key (a modifier or KEY_A, the
same rule the evdev fallback applies) qualify. Devices that emit
pointer motion (REL_X/Y, ABS_X/Y or ABS_MT_POSITION_X/Y) are
skipped: mice and tablets report EV_KEY bits for their buttons, but
grabbing them proxies every cursor movement through this reader.
"""
keyboard_devices: list[str] = []
device: Optional[dict[str, Any]] = None
@@ -372,18 +406,23 @@ def _parse_keyboard_devices_from_proc(proc_file: TextIO) -> list[str]:
nonlocal device
if device is None:
return
if _bitmap_has_any_bit(device.get("key", "")):
if _proc_block_has_pointer_axes(device):
if not _proc_block_has_keyboard_keys(device):
if _bitmap_has_any_bit(device.get("key", "")):
logger.debug(
"Skipping pointer device during keyboard discovery: "
"Skipping non-keyboard device during keyboard discovery: "
f"{device.get('name', '?')} ({device.get('handlers', [])})"
)
else:
for handler in device.get("handlers", []):
if handler.startswith("event"):
device_path = f"/dev/input/{handler}"
if os.path.exists(device_path):
keyboard_devices.append(device_path)
elif _proc_block_has_pointer_axes(device):
logger.debug(
"Skipping pointer device during keyboard discovery: "
f"{device.get('name', '?')} ({device.get('handlers', [])})"
)
else:
for handler in device.get("handlers", []):
if handler.startswith("event"):
device_path = f"/dev/input/{handler}"
if os.path.exists(device_path):
keyboard_devices.append(device_path)
device = None
with proc_file as f:
+73 -12
View File
@@ -54,7 +54,7 @@ H: Handlers=
mock_proc_content = """I: Bus=0011 Vendor=0001 Product=0001 Version=ab83
N: Name="AT Translated Set 2 keyboard"
H: Handlers=sysrq kbd event0
B: KEY=10000 7ff 202100 3953b001 68ffe0 1 20000 2000000000000 0
B: KEY=402000000 3803078f800d001 feffffdfffefffff fffffffffffffffe
"""
with patch("builtins.open", mock_open(read_data=mock_proc_content)):
with patch("os.path.exists", return_value=True):
@@ -66,7 +66,7 @@ B: KEY=10000 7ff 202100 3953b001 68ffe0 1 20000 2000000000000 0
mock_proc_content = """I: Bus=0011 Vendor=0001 Product=0001 Version=ab83
N: Name="AT Translated Set 2 keyboard"
H: Handlers=sysrq kbd event0
B: KEY=10000 7ff 202100 3953b001 68ffe0 1 20000 2000000000000 0
B: KEY=402000000 3803078f800d001 feffffdfffefffff fffffffffffffffe
I: Bus=0018 Vendor=04f3 Product=0033 Version=0500
N: Name="Elan Touchpad"
H: Handlers=mouse0 event1
@@ -126,7 +126,7 @@ B: KEY=0
mock_proc_content = """I: Bus=0011 Vendor=0001 Product=0001 Version=ab83
N: Name="Device with no event"
H: Handlers=kbd
B: KEY=10000 7ff 202100 3953b001 68ffe0 1 20000 2000000000000 0
B: KEY=402000000 3803078f800d001 feffffdfffefffff fffffffffffffffe
"""
with patch("builtins.open", mock_open(read_data=mock_proc_content)):
result = find_keyboard_devices()
@@ -138,7 +138,7 @@ B: KEY=10000 7ff 202100 3953b001 68ffe0 1 20000 2000000000000 0
mock_proc_content = """I: Bus=0011 Vendor=0001 Product=0001 Version=ab83
N: Name="AT Translated Set 2 keyboard"
H: Handlers=sysrq kbd event0
B: KEY=10000 7ff 202100 3953b001 68ffe0 1 20000 2000000000000 0
B: KEY=402000000 3803078f800d001 feffffdfffefffff fffffffffffffffe
"""
with patch("builtins.open", mock_open(read_data=mock_proc_content)):
with patch("os.path.exists", return_value=False):
@@ -150,14 +150,16 @@ B: KEY=10000 7ff 202100 3953b001 68ffe0 1 20000 2000000000000 0
mock_proc_content = """I: Bus=0011 Vendor=0001 Product=0001 Version=ab83
N: Name="AT Translated Set 2 keyboard"
H: Handlers=sysrq kbd event0
B: KEY=10000 7ff 202100 3953b001 68ffe0 1 20000 2000000000000 0
B: KEY=402000000 3803078f800d001 feffffdfffefffff fffffffffffffffe
I: Bus=0003 Vendor=046d Product=c08b Version=0110
N: Name="Logitech G502 HERO Gaming Mouse"
H: Handlers=mouse0 event5
B: KEY=1f0000 0 0 0 0
B: KEY=1f0000 0 0 0 38000000
B: REL=143
B: MSC=10
"""
# KEY carries real keyboard keys (bits 29-31) so only the pointer
# check can drop this device.
with patch("builtins.open", mock_open(read_data=mock_proc_content)):
with patch("os.path.exists", return_value=True):
result = find_keyboard_devices()
@@ -168,9 +170,11 @@ B: MSC=10
mock_proc_content = """I: Bus=0018 Vendor=04f3 Product=0033 Version=0500
N: Name="Elan Touchpad"
H: Handlers=mouse0 event1
B: KEY=ff000000000000 0 0 0
B: KEY=ff000000000000 0 0 38000000
B: ABS=273000000000003
"""
# KEY carries real keyboard keys (bits 29-31) so only the pointer
# check can drop this device.
with patch("builtins.open", mock_open(read_data=mock_proc_content)):
with patch("os.path.exists", return_value=True):
result = find_keyboard_devices()
@@ -181,7 +185,7 @@ B: ABS=273000000000003
mock_proc_content = """I: Bus=0005 Vendor=046d Product=b331 Version=0030
N: Name="Wireless Keyboard"
H: Handlers=kbd event3
B: KEY=10000 7ff 202100 3953b001 68ffe0 1 20000 2000000000000 0
B: KEY=402000000 3803078f800d001 feffffdfffefffff fffffffffffffffe
B: REL=100
"""
with patch("builtins.open", mock_open(read_data=mock_proc_content)):
@@ -194,14 +198,71 @@ B: REL=100
mock_proc_content = """I: Bus=0003 Vendor=046d Product=c52b Version=1201
N: Name="Logitech USB Receiver"
H: Handlers=mouse0 event4
B: KEY=ffff0000 0 0 0 0
B: KEY=ffff0000 0 0 0 38000000
B: REL=0 0 143
"""
# KEY carries real keyboard keys (bits 29-31) so only the pointer
# check can drop this device.
with patch("builtins.open", mock_open(read_data=mock_proc_content)):
with patch("os.path.exists", return_value=True):
result = find_keyboard_devices()
assert result == []
def test_find_keyboard_devices_skips_button_only_devices(self) -> None:
"""Devices whose KEY bitmap has no real keyboard key are not keyboards (#915)."""
mock_proc_content = """I: Bus=0019 Vendor=0000 Product=0001 Version=0000
N: Name="Power Button"
H: Handlers=kbd event4
B: KEY=1000000000000 0
I: Bus=0019 Vendor=0000 Product=0003 Version=0000
N: Name="Sleep Button"
H: Handlers=kbd event5
B: KEY=4000 0 0
I: Bus=0019 Vendor=0000 Product=0006 Version=0000
N: Name="Video Bus"
H: Handlers=kbd event6
B: KEY=3f000300000000 0 0 0 0
I: Bus=0019 Vendor=17aa Product=5054 Version=4101
N: Name="ThinkPad Extra Buttons"
H: Handlers=kbd event7 rfkill
B: KEY=40040002000000 c00017 21004000 300600 400028000000 0
I: Bus=0011 Vendor=0001 Product=0001 Version=ab83
N: Name="AT Translated Set 2 keyboard"
H: Handlers=sysrq kbd event0
B: KEY=402000000 3803078f800d001 feffffdfffefffff fffffffffffffffe
I: Bus=0003 Vendor=046d Product=c08b Version=0110
N: Name="Logitech G502 HERO Gaming Mouse Keyboard"
H: Handlers=sysrq kbd event8
B: KEY=ff 0 0 0 38000000
I: Bus=0006 Vendor=0000 Product=0000 Version=0001
N: Name="ydotoold virtual device"
H: Handlers=kbd mouse0 event9
B: KEY=ffff0000 0 0 38000000
B: REL=143
"""
with patch("builtins.open", mock_open(read_data=mock_proc_content)):
with patch("os.path.exists", return_value=True):
result = find_keyboard_devices()
assert result == ["/dev/input/event0", "/dev/input/event8"]
def test_find_keyboard_devices_32bit_words(self) -> None:
"""On a 32-bit kernel each bitmap word holds 32 bits, not 64.
KEY_RIGHTCTRL (code 97) sits in the fourth 32-bit word — the first
word printed. A 64-bit reading would look for it in word 1 and
miss the keyboard entirely.
"""
mock_proc_content = """I: Bus=0019 Vendor=0000 Product=0001 Version=0000
N: Name="GPIO keyboard"
H: Handlers=kbd event2
B: KEY=2 0 0 0
"""
with patch("vocalinux.ui.keyboard_backends.evdev_backend._WORD_BITS", 32):
with patch("builtins.open", mock_open(read_data=mock_proc_content)):
with patch("os.path.exists", return_value=True):
result = find_keyboard_devices()
assert result == ["/dev/input/event2"]
def test_find_keyboard_devices_skips_multitouch_only_touchpad(self) -> None:
"""A touchpad reporting only MT axes (no ABS_X/Y) is a pointer (#914)."""
mock_proc_content = """I: Bus=0018 Vendor=27c6 Product=01e9 Version=0100
@@ -209,7 +270,7 @@ N: Name="GXTP5100:00 27C6:01E9"
H: Handlers=mouse0 event2
B: PROP=85
B: EV=1b
B: KEY=10000 0 0 0 0
B: KEY=10000 0 0 0 38000000
B: ABS=60000000000000
B: MSC=10
"""
@@ -223,7 +284,7 @@ B: MSC=10
mock_proc_content = """I: Bus=0018 Vendor=27c6 Product=01e9 Version=0100
N: Name="GXTP5100:00 27C6:01E9"
H: Handlers=mouse0 event2
B: KEY=10000 0 0 0 0
B: KEY=10000 0 0 0 38000000
B: ABS=60000000000003
"""
with patch("builtins.open", mock_open(read_data=mock_proc_content)):
@@ -236,7 +297,7 @@ B: ABS=60000000000003
mock_proc_content = """I: Bus=0018 Vendor=27c6 Product=01e9 Version=0100
N: Name="GXTP5100:00 27C6:01E9"
H: Handlers=mouse0 event2
B: KEY=10000 0 0 0 0
B: KEY=10000 0 0 0 38000000
B: ABS=600000 0
"""
with patch("vocalinux.ui.keyboard_backends.evdev_backend._WORD_BITS", 32):