From 0955e91f2ad79f56ceb36ff77e89a6cba4fb0762 Mon Sep 17 00:00:00 2001 From: Rustam Abdullaev Date: Mon, 19 Oct 2026 11:53:43 +0200 Subject: [PATCH 1/2] client/input: remove dead devices on read failure The switch to libudev made removal notifications reliable in the common case, but the client still relies on them exclusively. The kernel hangs up the evdev descriptor as soon as the device is unregistered, while the udev remove event only arrives after udevd has processed it. Until then (or forever, if the event is lost to a monitor socket overflow) the descriptor stays POLLERR | POLLHUP, libevdev returns -ENODEV, and poll() ignores it, pinning a core and starving event relay for other devices. - poll(): treat -ENODEV as device removal - evdevDeviceAdded(): drop any device still held for the path, since a new node there means the remove event for the old one was missed - tryAddEvdevDevice(): never hold two devices for one path, which the retry queue could otherwise cause - evdevDeviceRemoved(): also drop the path from the retry queue Reproducible by pausing udevd (udevadm control --stop-exec-queue) and disconnecting a grabbed device. Fixes InputActions/discussions#492 --- src/client/input/StandaloneInputBackend.cpp | 51 ++++++++++++++++----- src/client/input/StandaloneInputBackend.h | 9 +++- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/client/input/StandaloneInputBackend.cpp b/src/client/input/StandaloneInputBackend.cpp index 56cbd8f..f34ce48 100644 --- a/src/client/input/StandaloneInputBackend.cpp +++ b/src/client/input/StandaloneInputBackend.cpp @@ -18,6 +18,7 @@ #include "StandaloneInputBackend.h" #include "StandaloneInputDevice.h" +#include #include #include #include @@ -26,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -94,14 +96,9 @@ void StandaloneInputBackend::reset() m_virtualKeyboard.reset(); m_virtualMouse.reset(); - for (const auto &device : m_devices) { - if (device->properties().grab()) { - // The compositor will take long enough to detect device removal that it will start generating key repeat events, reset the device to prevent that - device->resetVirtualDeviceState(); - } - removeDevice(device.get()); + while (!m_devices.empty()) { + removeDeviceAt(m_devices.begin()); } - m_devices.clear(); m_udevMonitor.reset(); m_deviceInitializationRetryTimer.stop(); LibinputInputBackend::reset(); @@ -113,6 +110,10 @@ void StandaloneInputBackend::evdevDeviceAdded(const QString &path) return; } + // A node can only be created at a path after the previous one was removed, so any device still held for this path refers to a dead node (the remove + // event may have been missed, e.g. if the udev monitor socket overflowed) + evdevDeviceRemoved(path); + if (!tryAddEvdevDevice(path)) { m_deviceInitializationQueue[path] = 0; } @@ -121,7 +122,8 @@ void StandaloneInputBackend::evdevDeviceAdded(const QString &path) bool StandaloneInputBackend::tryAddEvdevDevice(const QString &path) { for (const auto &device : m_devices) { - if (path == m_virtualKeyboard->path() || path == m_virtualMouse->path() || device->isDeviceOwnedByThisDevice(path)) { + // Never hold two devices for one path + if (path == m_virtualKeyboard->path() || path == m_virtualMouse->path() || device->path() == path || device->isDeviceOwnedByThisDevice(path)) { return true; } } @@ -153,13 +155,25 @@ bool StandaloneInputBackend::tryAddEvdevDevice(const QString &path) void StandaloneInputBackend::evdevDeviceRemoved(const QString &path) { + m_deviceInitializationQueue.erase(path); + auto it = std::ranges::find_if(m_devices, [&path](const auto &device) { return device->path() == path; }); if (it != m_devices.end()) { - removeDevice(it->get()); - m_devices.erase(it); + removeDeviceAt(it); + } +} + +StandaloneInputBackend::DeviceList::iterator StandaloneInputBackend::removeDeviceAt(DeviceList::iterator it) +{ + const auto &device = *it; + if (device->properties().grab()) { + // The compositor will take long enough to detect device removal that it will start generating key repeat events, reset the device to prevent that + device->resetVirtualDeviceState(); } + removeDevice(device.get()); + return m_devices.erase(it); } void StandaloneInputBackend::deviceInitializationRetryTimerTick() @@ -312,18 +326,26 @@ LibinputEventsProcessingResult StandaloneInputBackend::handleLibinputEvents(Stan void StandaloneInputBackend::poll() { std::vector frame; - for (const auto &device : m_devices) { + for (auto it = m_devices.begin(); it != m_devices.end();) { + const auto &device = *it; if (!device->properties().grab()) { handleLibinputEvents(device.get(), device->libinput()); + ++it; continue; } int status{}; + bool removed{}; while (true) { input_event evdevEvent{}; auto flags = status == LIBEVDEV_READ_STATUS_SYNC ? LIBEVDEV_READ_FLAG_SYNC : LIBEVDEV_READ_FLAG_NORMAL; status = device->libevdev()->nextEvent(flags, evdevEvent); if (status != LIBEVDEV_READ_STATUS_SUCCESS && status != LIBEVDEV_READ_STATUS_SYNC) { + if (status == -ENODEV) { + qWarning(INPUTACTIONS, "Device \"%s\" is gone, removing it", device->path().toStdString().c_str()); + removed = true; + break; + } // Handle events generated after a delay, e.g. pointer button after tapping handleLibinputEvents(device.get(), device->libinput()); break; @@ -373,10 +395,17 @@ void StandaloneInputBackend::poll() frame.clear(); } + if (removed) { + frame.clear(); + it = removeDeviceAt(it); + continue; + } + if (device->type() == InputDeviceType::Touchpad && device->physicalState().validTouchPoints().empty()) { device->setTouchpadNeutral(true); device->setTouchpadBlocked(false); } + ++it; } } diff --git a/src/client/input/StandaloneInputBackend.h b/src/client/input/StandaloneInputBackend.h index 2bbe244..36df4b6 100644 --- a/src/client/input/StandaloneInputBackend.h +++ b/src/client/input/StandaloneInputBackend.h @@ -62,6 +62,8 @@ private slots: void deviceInitializationRetryTimerTick(); private: + using DeviceList = std::vector>; + void poll(); void evdevDeviceAdded(const QString &path); @@ -71,6 +73,11 @@ private slots: */ bool tryAddEvdevDevice(const QString &path); void evdevDeviceRemoved(const QString &path); + /** + * Removes the device and erases it from m_devices. + * @return Iterator following the removed device. + */ + DeviceList::iterator removeDeviceAt(DeviceList::iterator it); bool handleEvent(StandaloneInputDevice *sender, const libinput::Event &event); LibinputEventsProcessingResult handleLibinputEvents(StandaloneInputDevice *device, libinput::PathContext *libinputContext); @@ -85,7 +92,7 @@ private slots: std::map m_deviceInitializationQueue; QTimer m_deviceInitializationRetryTimer; - std::vector> m_devices; + DeviceList m_devices; std::optional m_virtualKeyboard; std::optional m_virtualMouse; }; From 1215e12bdca246c892cb72d87fdb361d87310729 Mon Sep 17 00:00:00 2001 From: Rustam Abdullaev Date: Mon, 19 Oct 2026 11:58:33 +0200 Subject: [PATCH 2/2] client/input: unregister devices dropped by the retry timer When libinput keeps rejecting a device's event injection device, the retry timer gave up by erasing the device from m_devices without calling removeDevice(). InputBackend kept the freed pointer in its device list and event handler chain, so anything iterating devices afterwards (e.g. `inputactions devices list`, keyboard modifier queries) read freed memory. Remove the device through removeDeviceAt() like every other path, and log why it was dropped. --- src/client/input/StandaloneInputBackend.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/input/StandaloneInputBackend.cpp b/src/client/input/StandaloneInputBackend.cpp index f34ce48..abbd03d 100644 --- a/src/client/input/StandaloneInputBackend.cpp +++ b/src/client/input/StandaloneInputBackend.cpp @@ -194,7 +194,8 @@ void StandaloneInputBackend::deviceInitializationRetryTimerTick() const auto &device = it->get(); if (!device->isLibinputEventInjectionDeviceInitialized() && device->tryInitializeLibinputEventInjectionDevice() == MAX_INITIALIZATION_ATTEMPTS) { - it = m_devices.erase(it); + qWarning(INPUTACTIONS, "Failed to add device \"%s\" to libinput, removing it", device->path().toStdString().c_str()); + it = removeDeviceAt(it); continue; }