macos: synchronize cached value access (#13646)

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.
This commit is contained in:
Mitchell Hashimoto
2026-08-05 15:19:03 -07:00
committed by GitHub

View File

@@ -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<T> {
private let lock = NSLock()
private var value: T?
private let fetch: () -> T
private let duration: Duration
@@ -2381,10 +2382,15 @@ class CachedValue<T> {
}
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<T> {
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<T> {
return result
}
private func expire() {
lock.lock()
defer { lock.unlock() }
value = nil
expiryTask = nil
}
}