From 5d4610d9dda45eae8ebc165ffd953fd1d26f6c9d Mon Sep 17 00:00:00 2001 From: Sjoerd Siebinga Date: Tue, 17 Feb 2026 20:56:17 +0100 Subject: [PATCH] core: fix use-after-free segfault in async resource widget callbacks Properly lock AWP weak pointers before calling onAssetUpdate() to prevent use-after-free when widgets are destroyed during shutdown or output removal. Guard timer callback against null g_asyncResourceManager. Fix destruction order to join threads before resetting globals. --- src/core/hyprlock.cpp | 20 ++++++++++++-------- src/renderer/AsyncResourceManager.cpp | 24 +++++++++++++++++------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/core/hyprlock.cpp b/src/core/hyprlock.cpp index a92fa3f3..14f9cda2 100644 --- a/src/core/hyprlock.cpp +++ b/src/core/hyprlock.cpp @@ -452,8 +452,20 @@ void CHyprlock::run() { const auto DPY = m_sWaylandState.display; + // Wake threads so they observe m_bTerminate and exit; pending timers are dropped m_sLoopState.timerEvent = true; m_sLoopState.timerCV.notify_all(); + + pthread_kill(pollThr.native_handle(), SIGRTMIN); + + g_pAuth->terminate(); + + // Wait for threads to exit before destroying globals to prevent + // use-after-free in timer callbacks referencing g_asyncResourceManager + pollThr.join(); + timersThr.join(); + + // Now safe to destroy globals — no more timer callbacks can fire m_sWaylandState = {}; dma = {}; @@ -465,14 +477,6 @@ void CHyprlock::run() { wl_display_disconnect(DPY); - pthread_kill(pollThr.native_handle(), SIGRTMIN); - - g_pAuth->terminate(); - - // wait for threads to exit cleanly to avoid a coredump - pollThr.join(); - timersThr.join(); - Debug::log(LOG, "Reached the end, exiting"); } diff --git a/src/renderer/AsyncResourceManager.cpp b/src/renderer/AsyncResourceManager.cpp index 717cf524..a1ef99fd 100644 --- a/src/renderer/AsyncResourceManager.cpp +++ b/src/renderer/AsyncResourceManager.cpp @@ -248,11 +248,12 @@ bool CAsyncResourceManager::request(ResourceID id, const AWP& widget) { if (m_assets[id].texture) { // Asset already present. Dispatch the asset callback function. const auto& TEXTURE = m_assets[id].texture; - if (widget) - widget->onAssetUpdate(id, TEXTURE); + if (auto w = widget.lock()) { + w->onAssetUpdate(id, TEXTURE); - // TODO: add a centalized mechanism to render in one place in the event loop to avoid duplicate render calls - g_pHyprlock->addTimer(std::chrono::milliseconds(0), [](auto, auto) { g_pHyprlock->renderAllOutputs(); }, nullptr); + // TODO: add a centalized mechanism to render in one place in the event loop to avoid duplicate render calls + g_pHyprlock->addTimer(std::chrono::milliseconds(0), [](auto, auto) { g_pHyprlock->renderAllOutputs(); }, nullptr); + } } else if (widget) { // Asset currently in-flight. Add the widget reference to in order for the callback to get dispatched later. m_resourcesMutex.lock(); @@ -279,7 +280,16 @@ void CAsyncResourceManager::enqueue(ResourceID resourceID, const ASPm_events.finished.listenStatic([resourceID]() { - g_pHyprlock->addTimer(std::chrono::milliseconds(0), [](auto, void* resourceID) { g_asyncResourceManager->onResourceFinished((size_t)resourceID); }, (void*)resourceID); + if (!g_pHyprlock) + return; + + g_pHyprlock->addTimer( + std::chrono::milliseconds(0), + [](auto, void* resourceID) { + if (g_asyncResourceManager) + g_asyncResourceManager->onResourceFinished((size_t)resourceID); + }, + (void*)resourceID); }); } @@ -331,8 +341,8 @@ void CAsyncResourceManager::onResourceFinished(ResourceID id) { m_assets[id].texture = texture; for (const auto& widget : WIDGETS) { - if (widget) - widget->onAssetUpdate(id, texture); + if (auto w = widget.lock()) + w->onAssetUpdate(id, texture); } g_pHyprlock->renderAllOutputs();