From d28bc121a88def99cbc5ec8be1e18bc40f789325 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 5 Aug 2026 14:09:27 -0700 Subject: [PATCH] macos: synchronize cached value access Fixes #13276 Make CachedValue safe for concurrent terminal content reads and expiry. The expiry task could previously release cached Swift String storage while another thread retained it, aborting the process during otherwise normal terminal use. Protect cached values and task handles with an NSLock, and exercise concurrent reads across repeated expiration in a regression test. --- .../Surface View/SurfaceView_AppKit.swift | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index 4fb156317..0ce244873 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -2370,6 +2370,7 @@ extension Ghostty.SurfaceView { /// We use this to cache our surface content. This probably should be extracted some day /// to a more generic helper. class CachedValue { + private let lock = NSLock() private var value: T? private let fetch: () -> T private let duration: Duration @@ -2381,10 +2382,15 @@ class CachedValue { } deinit { + lock.lock() expiryTask?.cancel() + lock.unlock() } func get() -> T { + lock.lock() + defer { lock.unlock() } + if let value { return value } @@ -2399,8 +2405,7 @@ class CachedValue { expiryTask = Task { [weak self] in do { try await Task.sleep(until: expires) - self?.value = nil - self?.expiryTask = nil + self?.expire() } catch { // Task was cancelled, do nothing } @@ -2408,4 +2413,12 @@ class CachedValue { return result } + + private func expire() { + lock.lock() + defer { lock.unlock() } + + value = nil + expiryTask = nil + } }