From 28f4676b5d8964189ac3f6974cfbb212ef322bb4 Mon Sep 17 00:00:00 2001 From: Jesse Rosenstock Date: Sat, 25 Apr 2026 15:30:10 +0200 Subject: [PATCH] core: Acquire renderer state mutex before calling processLinks Holding the renderer state mutex is a documented precondition of `processLinks`, but `mouseButtonCallback` previously called the function without the mutex. This creates a race with the I/O thread's `processOutput`, which can prune scrollback pages while `processLinks` is reading them, resulting in a use-after-free segfault. See https://github.com/ghostty-org/ghostty/discussions/12409 (Linux: crash while selecting text). https://github.com/ghostty-org/ghostty/blob/57b5e1e2507cd65ab8197d39baa4ce2505185510/src/Surface.zig#L4354-L4355 https://github.com/ghostty-org/ghostty/blob/57b5e1e2507cd65ab8197d39baa4ce2505185510/src/Surface.zig#L3822-L3824 995e4e375 (os: open) changed the body of `processLinks` to be non-trivial and documented the precondition, but the lock was not held at the call site. --- src/Surface.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Surface.zig b/src/Surface.zig index dfc3a50ea..a040b1211 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3821,6 +3821,8 @@ pub fn mouseButtonCallback( // clicked link will swallow the event. if (self.mouse.over_link) { const pos = try self.rt_surface.getCursorPos(); + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); if (self.processLinks(pos)) |processed| { if (processed) return true; } else |err| {