Fix use-after-free in ghostty_surface_refresh after sleep/wake - #619
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis pull request fixes a use-after-free crash in Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Sources/GhosttyTerminalView.swift (1)
2020-2031:⚠️ Potential issue | 🟡 MinorDEBUG logging should use
dlog()instead of direct file writes.The coding guidelines specify that all debug events must go to a unified log using the
dlog()free function. This refresh debug logging writes directly to/tmp/cmux-refresh-debug.loginstead of usingdlog(). As per coding guidelines, alldlog()call sites must be wrapped in#if DEBUG/#endifconditional compilation blocks.♻️ Refactor to use dlog()
`#if` DEBUG - let ts = ISO8601DateFormatter().string(from: Date()) - let line = "[\(ts)] forceRefresh: \(id) \(viewState)\n" - let logPath = "/tmp/cmux-refresh-debug.log" - if let handle = FileHandle(forWritingAtPath: logPath) { - handle.seekToEndOfFile() - handle.write(line.data(using: .utf8)!) - handle.closeFile() - } else { - FileManager.default.createFile(atPath: logPath, contents: line.data(using: .utf8)) - } + dlog("surface.forceRefresh surface=\(id.uuidString.prefix(5)) \(viewState)") `#endif`As per coding guidelines: "All debug events (keys, mouse, focus, splits, tabs) must go to a unified log in DEBUG builds using the
dlog()free function. Alldlog()call sites must be wrapped in#if DEBUG/#endifconditional compilation blocks."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/GhosttyTerminalView.swift` around lines 2020 - 2031, Replace the manual file-write debug block with a call to the dlog() free function inside the existing `#if` DEBUG/#endif: construct the same log string (including ISO8601 timestamp, id and viewState) and call dlog("[\(ts)] forceRefresh: \(id) \(viewState)"); remove the FileHandle/FileManager create/write/close code; ensure the timestamp generation (ISO8601DateFormatter) and message formatting remain the same so the output matches the previous content.
🧹 Nitpick comments (1)
Sources/Workspace.swift (1)
3142-3147: Consider matching restart gating with the main reconcile path.Non-blocking: to avoid unnecessary restart attempts during transient detach/reparent, consider gating this restart with attachment/usable-bounds checks (same pattern as Line 3087).
Proposed consistency patch
panel.hostedView.reconcileGeometryNow() if panel.surface.surface != nil { panel.surface.forceRefresh() } - if panel.surface.surface == nil { + let isAttached = panel.hostedView.window != nil && panel.hostedView.superview != nil + let hasUsableBounds = panel.hostedView.bounds.width > 1 && panel.hostedView.bounds.height > 1 + if panel.surface.surface == nil, isAttached, hasUsableBounds { panel.surface.requestBackgroundSurfaceStartIfNeeded() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Workspace.swift` around lines 3142 - 3147, The restart attempt (panel.surface.requestBackgroundSurfaceStartIfNeeded()) should be gated the same way the main reconcile path gates restarts to avoid transient detach/reparent churn: wrap the requestBackgroundSurfaceStartIfNeeded() call in the same attachment and usable-bounds condition used in the reconcile block (i.e., copy the attachment/usableBounds check from the reconcile code path) so you only call requestBackgroundSurfaceStartIfNeeded() when the panel is attached and has usable bounds; leave panel.surface.forceRefresh() behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 2020-2031: Replace the manual file-write debug block with a call
to the dlog() free function inside the existing `#if` DEBUG/#endif: construct the
same log string (including ISO8601 timestamp, id and viewState) and call
dlog("[\(ts)] forceRefresh: \(id) \(viewState)"); remove the
FileHandle/FileManager create/write/close code; ensure the timestamp generation
(ISO8601DateFormatter) and message formatting remain the same so the output
matches the previous content.
---
Nitpick comments:
In `@Sources/Workspace.swift`:
- Around line 3142-3147: The restart attempt
(panel.surface.requestBackgroundSurfaceStartIfNeeded()) should be gated the same
way the main reconcile path gates restarts to avoid transient detach/reparent
churn: wrap the requestBackgroundSurfaceStartIfNeeded() call in the same
attachment and usable-bounds condition used in the reconcile block (i.e., copy
the attachment/usableBounds check from the reconcile code path) so you only call
requestBackgroundSurfaceStartIfNeeded() when the panel is attached and has
usable bounds; leave panel.surface.forceRefresh() behavior unchanged.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
Sources/GhosttyTerminalView.swiftSources/Workspace.swiftcmuxTests/CmuxWebViewKeyEquivalentTests.swift
Greptile SummaryFixes a use-after-free crash during sleep/wake by adding a nil guard in
Confidence Score: 5/5
Important Files Changed
Last reviewed commit: d448b2a |
…low-ai#432) (manaflow-ai#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.
Summary
forceRefresh()to prevent dereferencing freed surface pointer afterforceRefreshSurface()triggers layout that frees the surfaceelse ifchains inWorkspace.swiftinto separateifstatements sorequestBackgroundSurfaceStartIfNeeded()runs when the surface is freed during refresh (recovery path)releaseSurfaceForTesting()helper (#if DEBUG) and regression testCloses #432
Test plan
xcodebuild test -only-testing:cmuxTestsSummary by CodeRabbit
Bug Fixes
Tests