mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-04-11 03:27:08 +02:00
This commit fixes a cross-thread use-after-free crash with exception code 0xc000041d (FATAL_USER_CALLBACK_EXCEPTION), identified via WinDbg analysis of a minidump where rax=0xfeeefeeefeeefeee at the crash site in d2d1!HwndPresenter::Present - the Windows heap freed-memory fill pattern confirming Direct2D dereferenced a previously freed object. The root cause was a data race between the border manager thread and the border's own message loop thread. Border::create() spawns a dedicated thread (Thread B) for the HWND message loop and sends Box<Border> back to the border manager thread (Thread A) via a channel. After this point both threads accessed Border::render_target concurrently without any synchronisation: Thread A called update_brushes() directly on the Box<Border>, replacing render_target with a new ID2D1HwndRenderTarget and dropping the old one. Dropping the old RenderTarget decremented the COM refcount to zero, causing D2D to free its internal HwndPresenter. Thread B was concurrently mid-render in a WM_PAINT or EVENT_OBJECT_LOCATIONCHANGE handler, holding a reference to that same old render target obtained via the GWLP_USERDATA raw pointer. Calling EndDraw() after HwndPresenter was freed produced the crash. The process uptime of two seconds in the dump confirmed this happened during startup workspace initialisation, when a ForceUpdate notification triggered update_brushes() while the newly-shown border window was processing its first WM_PAINT. The fix routes all brush update requests through the border's own message loop by posting a custom WM_UPDATE_BRUSHES (WM_USER + 1) message instead of calling update_brushes() cross-thread. The three call sites in the border manager that previously called border.update_brushes()? directly now call border.request_brush_update(), which posts the message via PostMessageW. The WndProc handler for WM_UPDATE_BRUSHES calls update_brushes() and invalidate() entirely on Thread B, eliminating the race. A secondary bug in destroy() was also fixed: it was clearing GWLP_USERDATA before posting WM_CLOSE, which caused WM_DESTROY's null- pointer guard to skip the render_target = None cleanup. This left the ID2D1HwndRenderTarget alive past HWND destruction, and D2D freed its HwndPresenter during WM_NCDESTROY while the COM wrapper still held a reference - a second path to the same crash for any message queued between WM_NCDESTROY and WM_QUIT. The premature GWLP_USERDATA clear has been removed; WM_DESTROY already handles it correctly after releasing the render target.