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.
This commit is contained in:
Elvis Tran 2026-03-22 00:00:10 +10:30
parent 6ff81579d9
commit b1b07bf55b
2 changed files with 20 additions and 2 deletions

View file

@ -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

View file

@ -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<CTFont>.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