Fix use-after-free in ghostty_surface_refresh after sleep/wake (#432) (#619)

Add nil guard in forceRefresh() to prevent dereferencing freed surface
pointer. Split else-if chains in Workspace.swift so
requestBackgroundSurfaceStartIfNeeded() runs if surface is freed during
the refresh call. Add regression test exercising the crash path.
This commit is contained in:
Lawrence Chen 2026-02-27 01:44:02 -08:00 committed by GitHub
parent 9ae737026d
commit dca8992901
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 80 additions and 16 deletions

View file

@ -6761,6 +6761,49 @@ final class GhosttySurfaceOverlayTests: XCTestCase {
XCTAssertFalse(hostedView.debugHasSearchOverlay())
}
func testForceRefreshNoopsAfterSurfaceReleaseDuringGeometryReconcile() throws {
#if DEBUG
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 420, height: 280),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
defer { window.orderOut(nil) }
guard let contentView = window.contentView else {
XCTFail("Expected content view")
return
}
let surface = TerminalSurface(
tabId: UUID(),
context: GHOSTTY_SURFACE_CONTEXT_SPLIT,
configTemplate: nil,
workingDirectory: nil
)
let hostedView = surface.hostedView
hostedView.frame = contentView.bounds
hostedView.autoresizingMask = [.width, .height]
contentView.addSubview(hostedView)
window.makeKeyAndOrderFront(nil)
window.displayIfNeeded()
contentView.layoutSubtreeIfNeeded()
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
hostedView.reconcileGeometryNow()
surface.releaseSurfaceForTesting()
XCTAssertNil(surface.surface, "Surface should be nil after test release helper")
hostedView.reconcileGeometryNow()
surface.forceRefresh()
XCTAssertNil(surface.surface, "Force refresh should no-op when runtime surface is nil")
#else
throw XCTSkip("Debug-only regression test")
#endif
}
func testSearchOverlayMountDoesNotRetainTerminalSurface() {
weak var weakSurface: TerminalSurface?