Skip to content

fix: guard inherited terminal config against stale surfaces - #2101

Merged
austinywang merged 4 commits into
mainfrom
issue-2024-inherited-terminal-config-crash
Mar 25, 2026
Merged

austinywang merged 4 commits into
mainfrom
issue-2024-inherited-terminal-config-crash

Conversation

@austinywang

@austinywang austinywang commented Mar 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Fixes Crash (SIGSEGV) in inheritedTerminalConfig when splitting panes — Intel x86_64 #2024.
  • Add debug-only regression coverage for split creation and new terminal creation when the inheritance source wrapper still holds a freed native Ghostty surface pointer.
  • Track runtime surface ownership, quarantine stale or mis-owned pointers before inherited-config/font lookups, and route both workspace and new-workspace inheritance through the guarded access path.
  • Keep the regression in a two-commit shape: commit 1 adds the failing regression, commit 2 adds the production fix.

Testing

  • ./scripts/setup.sh
  • ./scripts/reload.sh --tag issue-2024-config-crash
  • Manually verified that the tagged debug app built and launched successfully.
  • Did not run unit/UI tests locally; repo policy says to run them in GitHub Actions / VM.

Demo Video

For UI or behavior changes, include a short demo video (GitHub upload, Loom, or other direct link).

  • Video URL or attachment: N/A

Review Trigger (Copy/Paste as PR comment)

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

Checklist

  • I tested the change locally
  • I added or updated tests for behavior changes
  • I updated docs/changelog if needed
  • I requested bot reviews after my latest commit (copy/paste block above or equivalent)
  • All code review bot comments are resolved
  • All human review comments are resolved

Summary by cubic

Fixes #2024 by guarding inherited terminal config against stale Ghostty surfaces and adding safe fallbacks. Prevents crashes when creating splits or new terminals by validating surface ownership/liveness and using recorded font size when the source is stale.

  • Bug Fixes
    • Track runtime surface ownership in TerminalSurfaceRegistry with pointer→owner UUID; register on create and unregister on teardown/free/deinit.
    • Add liveSurfaceForGhosttyAccess(reason:) to verify registry ownership and liveness; quarantine stale/mis-owned pointers, close the portal, and release callbacks.
    • Route all inheritance through the guarded path, including split/new-terminal, new workspace (TabManager), and font-size probes.
    • Add cmuxSurfacePointerAppearsLive/cmuxPointerAppearsLive checks for surfaces and unretained QuickLook font pointers; verify the surface before font probes.
    • When a source is quarantined, fall back to rooted or last-known font size; add debug-only tests that free the native pointer and confirm split/new-terminal creation succeeds while quarantining the stale wrapper.

Written for commit eaf4856. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Safer terminal surface lifecycle handling: stale or freed native surfaces are quarantined and skipped to prevent crashes during workspace operations.
    • Live-surface access now enforces validation before use, improving reliability of terminal actions and config inheritance.
  • Tests

    • Added debug-only regression tests that simulate freed surfaces to verify quarantine and fallback behavior.

@vercel

vercel Bot commented Mar 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Mar 25, 2026 11:01pm

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@coderabbitai

coderabbitai Bot commented Mar 25, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a runtime surface ownership registry and a MainActor guarded-access API that validates wrapper ownership and native-pointer liveness before exposing ghostty_surface_t; stale or freed native surfaces are quarantined and callbacks/ownership are cleared.

Changes

Cohort / File(s) Summary
Core ownership tracking & guarded access
Sources/GhosttyTerminalView.swift
Added TerminalSurfaceRegistry runtime ownership map and APIs (registerRuntimeSurface, unregisterRuntimeSurface, runtimeSurfaceOwnerId). Added @MainActor func liveSurfaceForGhosttyAccess(reason:) on TerminalSurface to validate ownership + pointer liveness and quarantine stale wrappers. Wired register/unregister into surface lifecycle; added DEBUG-only stale-pointer simulation helpers.
Pointer liveness helpers & integration
Sources/Workspace.swift
Added cmuxPointerAppearsLive(_:) and exported cmuxSurfacePointerAppearsLive(_:). Used these to gate cmuxCurrentSurfaceFontSizePoints, adjust quicklook font pointer validation, and integrate into inheritance/fallback logic (stale-rooted font fallback).
Consumers: safer inheritance usage
Sources/TabManager.swift
Replaced hasLiveSurface + direct unwrap with panel.surface.liveSurfaceForGhosttyAccess(reason:) when choosing an inheritance source.
Tests: simulate/quarantine freed pointers
cmuxTests/WorkspaceUnitTests.swift
Added main-runloop polling and window-hosting helpers. Added DEBUG-only tests that use replaceSurfaceWithFreedPointerForTesting() to force and assert quarantine behavior during newTerminalSplit and newTerminalSurface flows. Non-DEBUG builds skip these tests.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as Caller (e.g., newTerminalSplit)
    participant TSurface as TerminalSurface
    participant Registry as TerminalSurfaceRegistry
    participant Malloc as malloc_zone/_size
    participant Ghostty as Ghostty C API

    Caller->>TSurface: liveSurfaceForGhosttyAccess(reason)
    TSurface->>Registry: runtimeSurfaceOwnerId(ptr)?
    Registry-->>TSurface: ownerId matches? / ownership info

    alt ownership missing or mismatch
        TSurface->>TSurface: Quarantine (clear callbacks, unregister, nil surface, seal lifecycle)
        TSurface-->>Caller: nil (access denied)
    else ownership matches
        TSurface->>Malloc: cmuxSurfacePointerAppearsLive(ptr)?
        Malloc-->>TSurface: appears live?
        alt pointer appears freed
            TSurface->>TSurface: Quarantine (clear callbacks, unregister, nil surface, seal lifecycle)
            TSurface-->>Caller: nil (quarantined)
        else pointer live
            TSurface->>Ghostty: safe Ghostty C API call with ptr
            Ghostty-->>TSurface: result
            TSurface-->>Caller: ghostty_surface_t
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇
I hopped the registry, counted each ghostly plate,
I fenced the freed crumbs, and guarded the gate.
When surfaces vanish, I hush the alarm—
No crash, just a hop and a safe, new charm. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: guarding inherited terminal config against stale surfaces to fix the SIGSEGV crash.
Description check ✅ Passed The description covers all required template sections with sufficient detail: summary of changes, testing approach, and a complete checklist with most items marked.
Linked Issues check ✅ Passed The PR fully addresses issue #2024: tracks surface ownership, adds guarded access via liveSurfaceForGhosttyAccess(), routes all inheritance through guarded paths, validates pointer liveness, and includes regression tests.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing the use-after-free crash: TerminalSurfaceRegistry for ownership tracking, liveSurfaceForGhosttyAccess() for guarded access, pointer liveness checks, and regression tests—no unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-2024-inherited-terminal-config-crash

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a crash (#2024) where stale ghostty_surface_t pointers — Swift wrappers that remained non-nil after the backing native Ghostty surface was already freed — were passed to ghostty_surface_inherited_config during split creation and new-workspace creation, causing memory-safety violations.

Key changes:

  • TerminalSurfaceRegistry gains a runtimeSurfaceOwners: [UInt: UUID] dictionary that tracks which TerminalSurface instance owns each live native surface pointer. Entries are written on ghostty_surface_new success and cleared on every teardown path (teardownSurface, deinit, and the test helper).
  • A new liveSurfaceForGhosttyAccess(reason:) method on TerminalSurface replaces the previous hasLiveSurface + surface two-step. It cross-checks ownership in the registry and runs the existing malloc_zone heuristic; on mismatch it quarantines the wrapper in place (nil out surface, close the portal lifecycle) before returning nil to the caller.
  • Three call sites — Workspace.inheritedTerminalConfig, Workspace.rememberTerminalConfigInheritanceSource, and TabManager.inheritedTerminalConfigForNewWorkspace — are updated to go through liveSurfaceForGhosttyAccess.
  • Two DEBUG-only regression tests (testNewTerminalSplitSkipsFreedInheritedSurfacePointer, testNewTerminalSurfaceSkipsFreedInheritedSurfacePointer) simulate the out-of-band free via replaceSurfaceWithFreedPointerForTesting and assert that split/new-terminal creation completes without a crash and that the stale pointer is quarantined.

Minor issues found:

  • TerminalSurfaceRegistry.ownsRuntimeSurface is defined but never called — dead code.
  • registeredOwnerToken is computed unconditionally but is only referenced inside a #if DEBUG block; this will produce an "unused variable" warning in Release builds.
  • The hostTerminalPanelInWindow helper in the tests uses a fixed 50 ms RunLoop drain to wait for the native surface to materialise, which may be flaky on slow CI agents.

Confidence Score: 4/5

  • Safe to merge after addressing two minor Release-build warnings; crash fix and ownership tracking are correct.
  • The ownership-registry approach is sound and the three guard-call-site updates are consistent. The two unused-symbol issues (ownsRuntimeSurface dead code, registeredOwnerToken Release-build warning) are cosmetic and do not affect runtime correctness. The 50 ms RunLoop drain in the new test helper is a minor flakiness risk. No data-loss or security concerns.
  • Sources/GhosttyTerminalView.swift — dead ownsRuntimeSurface method and Release-build unused-variable warning in liveSurfaceForGhosttyAccess.

Important Files Changed

Filename Overview
Sources/GhosttyTerminalView.swift Adds runtimeSurfaceOwners dictionary to TerminalSurfaceRegistry and the liveSurfaceForGhosttyAccess(reason:) guard method. ownsRuntimeSurface is defined but never called (dead code). registeredOwnerToken computed outside #if DEBUG will generate an unused-variable warning in Release builds.
Sources/Workspace.swift Replaces two hasLiveSurface + direct surface accesses with liveSurfaceForGhosttyAccess(reason:). Also adds cmuxSurfacePointerAppearsLive / cmuxPointerAppearsLive helpers (elevating the existing malloc-zone heuristic to the surface level). Changes look correct.
Sources/TabManager.swift Routes inheritedTerminalConfigForNewWorkspace through liveSurfaceForGhosttyAccess. Minimal, correct change.
cmuxTests/WorkspaceUnitTests.swift Adds hostTerminalPanelInWindow helper and two DEBUG-only regression tests. The 50 ms RunLoop drain in the helper is a potential flakiness source on slow CI agents; surface creation is not guaranteed within that window.

Sequence Diagram

sequenceDiagram
    participant Caller as Caller (Workspace / TabManager)
    participant TS as TerminalSurface
    participant Reg as TerminalSurfaceRegistry
    participant Ghostty as Ghostty C API

    Note over TS,Reg: Surface creation (createSurface)
    TS->>Ghostty: ghostty_surface_new(...)
    Ghostty-->>TS: ghostty_surface_t (createdSurface)
    TS->>Reg: registerRuntimeSurface(createdSurface, ownerId: id)

    Note over Caller,Ghostty: Config inheritance (normal path)
    Caller->>TS: liveSurfaceForGhosttyAccess(reason:)
    TS->>TS: guard hasLiveSurface
    TS->>Reg: runtimeSurfaceOwnerId(surface)
    Reg-->>TS: ownerId (== self.id)
    TS->>Ghostty: cmuxSurfacePointerAppearsLive(surface)
    Ghostty-->>TS: true
    TS-->>Caller: ghostty_surface_t ✓
    Caller->>Ghostty: ghostty_surface_inherited_config(...)

    Note over Caller,Ghostty: Config inheritance (stale pointer path)
    Caller->>TS: liveSurfaceForGhosttyAccess(reason:)
    TS->>TS: guard hasLiveSurface
    TS->>Reg: runtimeSurfaceOwnerId(surface)
    Reg-->>TS: nil (already unregistered / reused)
    TS->>Reg: unregisterRuntimeSurface(surface)
    TS->>TS: self.surface = nil (quarantine)
    TS->>TS: markPortalLifecycleClosed
    TS-->>Caller: nil — skip inherited config

    Note over TS,Reg: Surface teardown
    TS->>Reg: unregisterRuntimeSurface(surface)
    TS->>Ghostty: ghostty_surface_free(surface)
Loading

Reviews (1): Last reviewed commit: "fix: guard inherited terminal config aga..." | Re-trigger Greptile

Comment thread Sources/GhosttyTerminalView.swift Outdated
Comment on lines +2671 to +2675
func ownsRuntimeSurface(_ surface: ghostty_surface_t, ownerId: UUID) -> Bool {
lock.lock()
defer { lock.unlock() }
return runtimeSurfaceOwners[UInt(bitPattern: surface)] == ownerId
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 ownsRuntimeSurface is dead code

ownsRuntimeSurface is defined here but never called anywhere in the codebase. It adds unnecessary public API surface to TerminalSurfaceRegistry and will confuse future readers about whether it is actually used.

Suggested change
func ownsRuntimeSurface(_ surface: ghostty_surface_t, ownerId: UUID) -> Bool {
lock.lock()
defer { lock.unlock() }
return runtimeSurfaceOwners[UInt(bitPattern: surface)] == ownerId
}

Comment thread cmuxTests/WorkspaceUnitTests.swift Outdated
window.makeKeyAndOrderFront(nil)
window.displayIfNeeded()
contentView.layoutSubtreeIfNeeded()
RunLoop.current.run(until: Date().addingTimeInterval(0.05))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 50 ms RunLoop drain may be flaky in slow CI

RunLoop.current.run(until: Date().addingTimeInterval(0.05)) is a timed wait that relies on the Ghostty runtime surface being created within 50 ms. Surface creation can be slower on loaded CI machines or under Rosetta 2, making the subsequent XCTAssertNotNil(sourcePanel.surface.surface, ...) assertion spuriously fail and turn both new regression tests into false negatives.

Consider polling with a tight retry loop (e.g. XCTAssertTrue(waitForCondition({ sourcePanel.surface.surface != nil }, timeout: 2))) or using XCTestExpectation / fulfillment(of:timeout:) to wait for the surface to materialise, rather than a fixed sleep.

Comment thread Sources/GhosttyTerminalView.swift Outdated
guard hasLiveSurface, let surface else { return nil }
let registry = TerminalSurfaceRegistry.shared
let registeredOwnerId = registry.runtimeSurfaceOwnerId(surface)
let registeredOwnerToken = registeredOwnerId.map { String($0.uuidString.prefix(5)) } ?? "nil"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 registeredOwnerToken unused in Release builds

registeredOwnerToken is computed unconditionally on every call path, but it is only consumed inside the #if DEBUG block a few lines below. In Release builds Swift will emit an "immutable value 'registeredOwnerToken' was never used" warning because the #if DEBUG section is entirely excluded from compilation.

Move the let registeredOwnerToken = ... declaration to inside the #if DEBUG block (just before the dlog(...) call) so it is only compiled when it is actually referenced.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 2665-2669: The unregisterRuntimeSurface currently removes the
entry by pointer only; change its signature to accept the expected ownerId (e.g.
func unregisterRuntimeSurface(_ surface: ghostty_surface_t, ownerId: UInt) ) and
implement a compare-and-remove: compute key = UInt(bitPattern: surface), look up
runtimeSurfaceOwners[key], and only remove if the stored ownerId equals the
passed ownerId; otherwise do nothing. Update all teardown/quarantine callers to
pass the wrapper's id when calling unregisterRuntimeSurface so we never erase an
entry belonging to a new owner.
- Around line 2958-2984: The code currently exposes the raw surface pointer and
bypasses quarantine; change direct uses of the surface property so all C calls
go through a guarded accessor that invokes liveSurfaceForGhosttyAccess(reason:),
make the raw surface storage private (e.g. rename surface to _surface) and add
two explicit APIs: a safeSurfaceForGhosttyAccess(reason:) that returns
ghostty_surface_t? (calls liveSurfaceForGhosttyAccess) for all attachToView,
updateSize, forceRefresh, setFocus, performBindingAction, and
GhosttyNSView.surface call sites, and a separate identityOnlySurfaceToken() (or
runtimeSurfaceOwnerId()) that returns an identity token used only for
comparisons and tests (used by replaceSurfaceWithFreedPointerForTesting). Update
callers to use the safe accessor for C interactions and the identity-only API
for equality checks so tests can simulate freed-pointer wrappers without risking
raw pointer dereference.

In `@Sources/Workspace.swift`:
- Around line 6970-6972: Before calling
terminalPanel.surface.liveSurfaceForGhosttyAccess, read and stash the
candidate’s rooted font from
terminalInheritanceFontPointsByPanelId[terminalPanel.id] into a local variable;
if the guard fails (surface is quarantined) use that stashed value as the
fallback override for zoom/font inheritance instead of simply continuing and
dropping the map entry. In practice: capture the existing rooted font before the
guard, and when liveSurfaceForGhosttyAccess returns nil, propagate or reassign
that captured value as the fallback donor for downstream inheritance logic
rather than losing terminalInheritanceFontPointsByPanelId[terminalPanel.id].

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fdc5d343-e611-4359-854d-8458f704a45d

📥 Commits

Reviewing files that changed from the base of the PR and between 960006e and ca204f4.

📒 Files selected for processing (4)
  • Sources/GhosttyTerminalView.swift
  • Sources/TabManager.swift
  • Sources/Workspace.swift
  • cmuxTests/WorkspaceUnitTests.swift

Comment thread Sources/GhosttyTerminalView.swift Outdated
Comment thread Sources/Workspace.swift Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 4 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/GhosttyTerminalView.swift">

<violation number="1" location="Sources/GhosttyTerminalView.swift:2665">
P1: `unregisterRuntimeSurface` unconditionally removes the pointer from the registry, which will corrupt tracking if a stale wrapper unregisters a pointer that has been reallocated to a new surface. It must require an `ownerId` and verify it before removal.</violation>
</file>

<file name="cmuxTests/WorkspaceUnitTests.swift">

<violation number="1" location="cmuxTests/WorkspaceUnitTests.swift:813">
P3: Replace the fixed 50ms RunLoop sleep with a condition-based wait (polling or XCTest expectation) to avoid flaky failures when surface creation takes longer under CI load.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/GhosttyTerminalView.swift Outdated
Comment thread cmuxTests/WorkspaceUnitTests.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
Sources/GhosttyTerminalView.swift (1)

2847-2857: ⚠️ Potential issue | 🟠 Major

Guarded access is still opt-in.

Line 2847 still exposes surface module-wide, so this contract is easy to bypass. The file still has raw Ghostty call paths (attachToView, updateSize, forceRefresh, setFocus, performBindingAction, GhosttyNSView.surface), and the provided Sources/AppDelegate.swift snippets still gate readiness off terminalPanel.surface.surface != nil. That means a stale wrapper can still hit freed memory on redraw/input/sendText even though inherited-config callers now use the guarded accessor.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 2847 - 2857, The surface
property is still externally accessible (private(set) var surface) so callers
can bypass the hasLiveSurface contract and dereference freed pointers; make the
stored surface fully private and force all external code paths to go through the
guarded accessor pattern (liveSurfaceForGhosttyAccess(reason:)) or a new safe
helper (e.g., withLiveSurface(_ closure: (ghostty_surface_t) -> Void) that
returns false/throws if no live surface) and update all call sites that
currently reference GhosttyTerminalView.surface or GhosttyNSView.surface —
including attachToView, updateSize, forceRefresh, setFocus, performBindingAction
and the AppDelegate readiness checks — to use the guarded API so raw Ghostty C
calls never see a possibly-freed pointer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 3100-3126: The quarantined/closed TerminalSurface can be
immediately resurrected because liveSurfaceForGhosttyAccess clears surface but
attachSurface(_:)/TerminalSurface.attachToView(_:) only checks surface == nil
before calling createSurface(for:), allowing re-registration into a permanently
closed wrapper; update the attach/create flow to either (A) prevent re-creation
when the wrapper is in a quarantined/closed state by checking hasLiveSurface or
a new isQuarantined flag before calling createSurface(for:) (symbols:
GhosttyNSView.attachSurface(_:), TerminalSurface.attachToView(_:),
createSurface(for:), hasLiveSurface, canAcceptPortalBinding, surface) or (B)
explicitly reopen the portal lifecycle before re-registering the new native
surface by clearing the quarantined state and calling the lifecycle open path
(symbols: markPortalLifecycleClosed(reason:), recordTeardownRequest(reason:),
surfaceCallbackContext) so that re-registration is only allowed on a
legitimately open TerminalSurface; choose one approach and apply it at the
attach/create entry to eliminate the rogue re-registration.

---

Duplicate comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 2847-2857: The surface property is still externally accessible
(private(set) var surface) so callers can bypass the hasLiveSurface contract and
dereference freed pointers; make the stored surface fully private and force all
external code paths to go through the guarded accessor pattern
(liveSurfaceForGhosttyAccess(reason:)) or a new safe helper (e.g.,
withLiveSurface(_ closure: (ghostty_surface_t) -> Void) that returns
false/throws if no live surface) and update all call sites that currently
reference GhosttyTerminalView.surface or GhosttyNSView.surface — including
attachToView, updateSize, forceRefresh, setFocus, performBindingAction and the
AppDelegate readiness checks — to use the guarded API so raw Ghostty C calls
never see a possibly-freed pointer.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 28b468cd-ec51-4c2b-90f0-854f99bcd202

📥 Commits

Reviewing files that changed from the base of the PR and between 2cdb41f and eaf4856.

📒 Files selected for processing (4)
  • Sources/GhosttyTerminalView.swift
  • Sources/TabManager.swift
  • Sources/Workspace.swift
  • cmuxTests/WorkspaceUnitTests.swift
✅ Files skipped from review due to trivial changes (1)
  • cmuxTests/WorkspaceUnitTests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/Workspace.swift

Comment on lines +3100 to +3126
@MainActor
func liveSurfaceForGhosttyAccess(reason: String) -> ghostty_surface_t? {
guard hasLiveSurface, let surface else { return nil }
let registry = TerminalSurfaceRegistry.shared
let registeredOwnerId = registry.runtimeSurfaceOwnerId(surface)
guard registeredOwnerId == id,
cmuxSurfacePointerAppearsLive(surface) else {
let callbackContext = surfaceCallbackContext
surfaceCallbackContext = nil
registry.unregisterRuntimeSurface(surface, ownerId: id)
self.surface = nil
activePortalHostLease = nil
recordTeardownRequest(reason: reason)
markPortalLifecycleClosed(reason: reason)
#if DEBUG
let registeredOwnerToken = registeredOwnerId.map { String($0.uuidString.prefix(5)) } ?? "nil"
dlog(
"surface.lifecycle.stale surface=\(id.uuidString.prefix(5)) " +
"workspace=\(tabId.uuidString.prefix(5)) reason=\(reason) " +
"registryOwner=\(registeredOwnerToken)"
)
#endif
callbackContext?.release()
return nil
}
return surface
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Quarantined wrappers can immediately resurrect a new native surface.

This path seals the wrapper as .closed and clears surface, but the existing attach flow only checks surface == nil. On the next GhosttyNSView.attachSurface(_:) / TerminalSurface.attachToView(_:), createSurface(for:) can run again and Line 3714 re-registers a fresh pointer on a permanently closed TerminalSurface. After that, hasLiveSurface / canAcceptPortalBinding stay false while raw callers can still drive the new surface. Either block recreation after quarantine or explicitly reopen the lifecycle before re-registering.

Also applies to: 3714-3715

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 3100 - 3126, The
quarantined/closed TerminalSurface can be immediately resurrected because
liveSurfaceForGhosttyAccess clears surface but
attachSurface(_:)/TerminalSurface.attachToView(_:) only checks surface == nil
before calling createSurface(for:), allowing re-registration into a permanently
closed wrapper; update the attach/create flow to either (A) prevent re-creation
when the wrapper is in a quarantined/closed state by checking hasLiveSurface or
a new isQuarantined flag before calling createSurface(for:) (symbols:
GhosttyNSView.attachSurface(_:), TerminalSurface.attachToView(_:),
createSurface(for:), hasLiveSurface, canAcceptPortalBinding, surface) or (B)
explicitly reopen the portal lifecycle before re-registering the new native
surface by clearing the quarantined state and calling the lifecycle open path
(symbols: markPortalLifecycleClosed(reason:), recordTeardownRequest(reason:),
surfaceCallbackContext) so that re-registration is only allowed on a
legitimately open TerminalSurface; choose one approach and apply it at the
attach/create entry to eliminate the rogue re-registration.

@austinywang
austinywang merged commit 9f2adce into main Mar 25, 2026
16 checks passed
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
…-ai#2101)

* test: add stale inherited surface regression

* fix: guard inherited terminal config against stale surfaces

* fix: address stale surface review feedback

This branch was successfully deployed

1 active deployment
Preview — eaf48562 Deployed Mar 25, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Crash (SIGSEGV) in inheritedTerminalConfig when splitting panes — Intel x86_64

1 participant