From b1b07bf55b7725b056897b74fabe22dba690ce63 Mon Sep 17 00:00:00 2001 From: Elvis Tran <40386529+elvistranhere@users.noreply.github.com> Date: Sun, 22 Mar 2026 00:00:10 +1030 Subject: [PATCH] Fix #1870: prevent split crash on Intel Macs caused by stale font pointer ghostty_surface_quicklook_font returns an unretained CTFont pointer that can become stale on Intel Macs, leading to EXC_BAD_ACCESS (SIGSEGV) when creating a split. This is a follow-up to the same crash pattern fixed in #1496. Add malloc_size validation in cmuxCurrentSurfaceFontSizePoints to detect freed heap allocations before interpreting the pointer as a CTFont. Also add hasLiveSurface guards in inheritedTerminalConfig and rememberTerminalConfigInheritanceSource to skip surfaces whose native state is closing or closed. All callers already handle nil gracefully by falling back to inherited config values. --- Sources/GhosttyTerminalView.swift | 9 +++++++++ Sources/Workspace.swift | 13 +++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index 2ee395f0..5ff4359a 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -2677,6 +2677,15 @@ final class TerminalSurface: Identifiable, ObservableObject { private(set) var surface: ghostty_surface_t? private weak var attachedView: GhosttyNSView? + + /// Whether the runtime Ghostty surface exists and has not begun teardown. + /// + /// Use this before passing `surface` to Ghostty C APIs that dereference the + /// pointer (e.g. `ghostty_surface_inherited_config`, `ghostty_surface_quicklook_font`). + /// A non-nil `surface` alone is not sufficient — the underlying native state may + /// already be closing or closed. + var hasLiveSurface: Bool { surface != nil && portalLifecycleState == .live } + /// Whether the terminal surface view is currently attached to a window. /// /// Use the hosted view rather than the inner surface view, since the surface can be diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 3734b412..ed36d8fb 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -26,6 +26,13 @@ func cmuxCurrentSurfaceFontSizePoints(_ surface: ghostty_surface_t) -> Float? { return nil } + // Validate the font pointer is a live heap allocation before interpreting + // it as a CTFont. ghostty_surface_quicklook_font returns an unretained + // pointer that can become stale on Intel Macs (#1496, #1870). + guard malloc_size(quicklookFont) > 0 else { + return nil + } + let ctFont = Unmanaged.fromOpaque(quicklookFont).takeUnretainedValue() let points = Float(CTFontGetSize(ctFont)) guard points > 0 else { return nil } @@ -6797,7 +6804,8 @@ final class Workspace: Identifiable, ObservableObject { private func rememberTerminalConfigInheritanceSource(_ terminalPanel: TerminalPanel) { lastTerminalConfigInheritancePanelId = terminalPanel.id - if let sourceSurface = terminalPanel.surface.surface, + if terminalPanel.surface.hasLiveSurface, + let sourceSurface = terminalPanel.surface.surface, let runtimePoints = cmuxCurrentSurfaceFontSizePoints(sourceSurface) { let existing = terminalInheritanceFontPointsByPanelId[terminalPanel.id] if existing == nil || abs((existing ?? runtimePoints) - runtimePoints) > 0.05 { @@ -6895,7 +6903,8 @@ final class Workspace: Identifiable, ObservableObject { preferredPanelId: preferredPanelId, inPane: preferredPaneId ) { - guard let sourceSurface = terminalPanel.surface.surface else { continue } + guard terminalPanel.surface.hasLiveSurface, + let sourceSurface = terminalPanel.surface.surface else { continue } var config = cmuxInheritedSurfaceConfig( sourceSurface: sourceSurface, context: GHOSTTY_SURFACE_CONTEXT_SPLIT