Skip to content

fix: remove lock-held file I/O and nested lock from userdata registry - #263

Merged
arzafran merged 1 commit into
mainfrom
fix/userdata-registry-lock-hygiene
Aug 5, 2026
Merged

fix: remove lock-held file I/O and nested lock from userdata registry#263
arzafran merged 1 commit into
mainfrom
fix/userdata-registry-lock-hygiene

Conversation

@arzafran

@arzafran arzafran commented Aug 5, 2026

Copy link
Copy Markdown
Member

What this does

#262 (just merged) stopped ghostty's callback threads from dereferencing a
freed Swift object — that crash-loop is gone (0 minidumps on the re-land
branch, down from 6/run). This PR removes two rough edges in how it did
that: the stale-pointer debug log used to run while holding the global
registry lock (synchronous file I/O, on the frequent path during rapid
surface teardown), and resolving a live pointer nested the object's own
lock inside that registry lock. Neither is a proven cause of anything yet,
but a socket-integration test and a focus test that had never failed
before both started timing out ("Socket closed" / hang) in the same test
run that added #262 — this is a plausible explanation, not a confirmed
fix, and the re-land gate will tell us if it holds.

Summary

  • GhosttySurfaceUserdataRegistry now owns the callback data directly: its
    storage changed from Set<UnsafeMutableRawPointer> to
    [UnsafeMutableRawPointer: GhosttySurfaceCallbackSnapshot]. resolve(from:)
    is now a pure dictionary lookup under one lock — it never calls
    Unmanaged.fromOpaque(_:).takeUnretainedValue() and never touches the
    GhosttySurfaceCallbackContext instance.
  • The DEBUG stale-pointer dlog(...) now fires after the lock is released,
    not while held.
  • GhosttySurfaceCallbackContext no longer stores tabId — its
    tabIdLock/_tabId/tabId getter/updateTabId were only ever read by
    the old resolve(from:), which no longer dereferences the class at all.
    The class is now just an identity/lifetime token for
    Unmanaged retain/release bookkeeping.
  • TerminalSurface.updateWorkspaceId now propagates a tabId reassignment
    into the registry via
    GhosttySurfaceUserdataRegistry.updateTabId(pointer:tabId:) — the same
    snapshot callbacks resolve — instead of only updating the (no-longer-read)
    context instance.
  • Grepped Sources/ for any other dlog( call made while a lock is held on
    a callback-frequency path introduced by fix: reject stale ghostty surface userdata pointers before ARC retain #262: none found. The lock-held
    dlog fixed here was the only instance.

Test Plan

  • xcodebuild -project GhosttyTabs.xcodeproj -scheme programa -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/programa-registry build → BUILD SUCCEEDED
  • -only-testing:programaTests/WorkspaceSplitWorkingDirectoryTests → 5/5 passed, including the existing testStaleSurfaceUserdataResolvesToNilInsteadOfCrashing (unmodified assertions, comment updated) and the new testSurfaceUserdataResolvesUpdatedTabIdAfterWorkspaceReassignment

…g pointer

GhosttySurfaceUserdataRegistry (added in #262 to stop ghostty callbacks
resolving freed GhosttySurfaceCallbackContext pointers) held its lock
across a DEBUG dlog() call on the stale-pointer path, and resolve(from:)
nested the context's own tabIdLock inside the registry lock while
dereferencing the pointer. Both are gone: the registry now stores the
(surfaceId, tabId) snapshot directly in a dictionary keyed by pointer, so
resolve(from:) is a pure lookup under one lock, and the stale dlog fires
after the lock is released.

- GhosttySurfaceCallbackContext no longer stores tabId; it's now just an
  identity/lifetime token for Unmanaged retain/release bookkeeping (its
  tabIdLock/_tabId/tabId getter/updateTabId were only read by the old
  resolve(from:), which no longer dereferences the class at all).
- TerminalSurface.updateWorkspaceId now propagates a tabId reassignment
  into the registry via GhosttySurfaceUserdataRegistry.updateTabId(pointer:tabId:),
  the same snapshot callbacks resolve.
- Regression test testStaleSurfaceUserdataResolvesToNilInsteadOfCrashing
  still passes unmodified (comment updated to note the mechanism change);
  added testSurfaceUserdataResolvesUpdatedTabIdAfterWorkspaceReassignment
  to pin the new tabId-propagation behavior.
@arzafran
arzafran merged commit 6224f6e into main Aug 5, 2026
10 checks passed
@arzafran
arzafran deleted the fix/userdata-registry-lock-hygiene branch August 5, 2026 19:04
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.

1 participant