From 30dcd7a7ca70deb405a40a9d025c38d7f4b6d99e Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:09:35 +0200 Subject: [PATCH] fix(hyprland/workspaces): own debounce timer on main thread, fix UAF MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The debounce timer added for flicker prevention was armed from the IPC listener thread via Glib::signal_timeout().connect(), while its timeout lambda and the m_updatePending flag ran on the GTK main thread — an unsynchronized cross-thread data race on GLib timer/main-loop state. Additionally ~Workspaces() never disconnected the timer, so a pending timeout could fire on a freed 'this' (use-after-free). Restore the pre-refactor threading model: onEvent now only mutates state under m_mutex on the IPC thread and calls dp.emit() (Glib::Dispatcher is thread-safe). The debounce timer is owned entirely by the main-thread update() path, which arms/re-arms it on each dispatch and coalesces bursts into a single refresh. ~Workspaces() disconnects the timer (guarded) so none outlives the object. Debounce behavior is preserved. --- include/modules/hyprland/workspaces.hpp | 3 +- src/modules/hyprland/workspaces.cpp | 43 ++++++++++++++----------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 4be7fd44..1237ef24 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -227,8 +227,9 @@ class Workspaces : public AModule, public EventHandler { sigc::connection m_scrollEventConnection_; IPC& m_ipc; + // Coalesces bursts of Hyprland events into a single UI refresh. Armed and + // disconnected only on the GTK main thread (see Workspaces::update). sigc::connection m_debounceTimer; - bool m_updatePending = false; }; } // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 4cd0d2ae..202b892b 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -37,6 +37,11 @@ Workspaces::~Workspaces() { if (m_scrollEventConnection_.connected()) { m_scrollEventConnection_.disconnect(); } + // Cancel any pending debounce timeout so it cannot fire on a freed `this`. + // Runs on the main thread, same as where the timer is armed. + if (m_debounceTimer.connected()) { + m_debounceTimer.disconnect(); + } m_ipc.unregisterForIPC(this); // wait for possible event handler to finish std::lock_guard lg(m_mutex); @@ -332,23 +337,11 @@ void Workspaces::onEvent(const std::string& ev) { } } - if (m_debounceTimer.connected()) { - m_debounceTimer.disconnect(); - m_updatePending = false; - } - - m_updatePending = true; - m_debounceTimer = Glib::signal_timeout().connect( - [this]() { - if (!m_updatePending) return false; - std::lock_guard lock(m_mutex); - if (m_updatePending) { - dp.emit(); - m_updatePending = false; - } - return false; - }, - 7); + // Notify the main thread. dp (Glib::Dispatcher) is the only thread-safe way to + // hand off to the GTK main loop; GLib timer state must never be touched from the + // IPC listener thread. The debounce timer is owned entirely by the main-thread + // update() path (see Workspaces::update). + dp.emit(); } void Workspaces::onWorkspaceActivated(std::string const& payload) { @@ -1041,8 +1034,20 @@ void Workspaces::setUrgentWorkspace(std::string const& windowaddress) { } auto Workspaces::update() -> void { - doUpdate(); - AModule::update(); + // Debounce rapid events (e.g. out-of-order create/destroy workspace events from + // Hyprland) to prevent workspace button flicker. This runs on the GTK main thread + // (invoked via the dp dispatcher), so arming/disconnecting the GLib timer here is + // thread-safe. Each event re-arms the timer, coalescing bursts into one refresh. + if (m_debounceTimer.connected()) { + m_debounceTimer.disconnect(); + } + m_debounceTimer = Glib::signal_timeout().connect( + [this]() { + doUpdate(); + AModule::update(); + return false; + }, + 7); } void Workspaces::updateWindowCount() {