Skip to content

Fix Cmd+N crash from workspace creation config snapshots - #2178

Merged
austinywang merged 2 commits into
mainfrom
issue-2157-cmd-n-snapshot-crash
Mar 26, 2026
Merged

austinywang merged 2 commits into
mainfrom
issue-2157-cmd-n-snapshot-crash

Conversation

@austinywang

@austinywang austinywang commented Mar 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add a regression test that injects pointer-backed inherited terminal config into workspace creation and asserts only sanitized values are forwarded
  • stop WorkspaceCreationSnapshot from storing the full ghostty_surface_config_s; snapshot only inherited font points and rebuild a clean config template during workspace creation
  • keep the regression-test commit separate from the fix commit so CI can show the failure before the sanitization lands

Context

PR #2133 stopped snapshotting live Workspace references, but WorkspaceCreationSnapshot still retained a full ghostty_surface_config_s. That C struct can include raw pointers owned by the source terminal surface. If Cmd+N captures the snapshot and the source workspace or surface tears down before the new workspace is created, the snapshot can carry dangling pointers and crash in the Cmd+N path described in #2157.

Closes #2157

Validation

  • ./scripts/reload.sh --tag cmd-n-fix --launch
  • Local tests not run per repo policy

Summary by CodeRabbit

  • Bug Fixes
    • Improved terminal configuration inheritance when creating new workspaces—font size now properly carries over from the parent workspace while other settings are appropriately sanitized.

@vercel

vercel Bot commented Mar 26, 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 26, 2026 0:59am

@coderabbitai

coderabbitai Bot commented Mar 26, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

TabManager's workspace-creation field replaces a pointer-backed terminal config snapshot with a simple inherited font-size Float value. New methods extract font size from inherited config and construct sanitized config templates for new workspaces. Tests verify that the sanitized template preserves font size while stripping other inherited values.

Changes

Cohort / File(s) Summary
TabManager refactoring
Sources/TabManager.swift
Replaced inheritedTerminalConfig snapshot field with inheritedTerminalFontPoints; introduced inheritedTerminalFontPointsForNewWorkspace() and workspaceCreationConfigTemplate() to extract font size and construct fresh sanitized configs; changed inheritedTerminalConfigForNewWorkspace() from private to internal.
Workspace creation sanitization tests
cmuxTests/WorkspaceUnitTests.swift
Added WorkspaceCreationConfigSanitizationTests test class with custom TabManager subclass that injects inherited config state and captures config templates passed during workspace creation; testAddWorkspacePassesSanitizedInheritedConfigTemplate() verifies font size is preserved while working_directory, command, and env_vars are stripped.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~35 minutes

Possibly related PRs

  • PR #2127: Modifies makeWorkspaceForCreation and addWorkspace, the workspace-creation entry point that now receives the new sanitized configTemplate parameter.
  • PR #1391: Introduces the snapshot mechanism from workspaceCreationSnapshot() that these changes replace with inherited-font-points approach.
  • PR #2101: Previously modified inheritedTerminalConfigForNewWorkspace, the method now extracted to be internal and paired with the new font-points inheritance pattern.

Poem

🐰 A snapshot once lived in the swift, so fast,
But pointers don't age well—they fade in the past!
Now fonts float freely, so clean and so bright,
Fresh configs bloom forth, sanitized right.
hoppy refactor ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 accurately describes the primary change: fixing a Cmd+N crash by addressing how workspace creation config snapshots are handled.
Description check ✅ Passed The PR description covers the main changes, context, and rationale. However, it lacks information on how the change was tested beyond a script tag reference.
Linked Issues check ✅ Passed The PR addresses the core objectives from #2157 by preventing snapshots from retaining raw pointers and sanitizing inherited config values during workspace creation.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the Cmd+N crash: test infrastructure, snapshot mechanism refactoring, and config template reconstruction.

✏️ 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-2157-cmd-n-snapshot-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.

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

No issues found across 2 files

@greptile-apps

greptile-apps Bot commented Mar 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a use-after-free crash in the Cmd+N workspace creation path by stopping WorkspaceCreationSnapshot from retaining a full ghostty_surface_config_s C struct (which can carry raw pointers owned by the source terminal surface) and instead snapshotting only the inherited font_size as a safe scalar Float?, then reconstructing a clean config via ghostty_surface_config_new() at workspace creation time.

  • Core fix (Sources/TabManager.swift): WorkspaceCreationSnapshot.inheritedTerminalConfig: ghostty_surface_config_s? is replaced by inheritedTerminalFontPoints: Float?. Two new private helpers (inheritedTerminalFontPointsForNewWorkspace and workspaceCreationConfigTemplate) isolate the extraction and reconstruction logic. inheritedTerminalConfigForNewWorkspace(workspace:) is widened from private to internal to allow the test subclass to override it.
  • Regression test (cmuxTests/WorkspaceUnitTests.swift): WorkspaceCreationConfigSanitizationTests injects a fully pointer-backed ghostty_surface_config_s (with working_directory, command, env_vars) and asserts that only font_size is forwarded to makeWorkspaceForCreation, following the two-commit structure required by repo policy.
  • The fix is scoped correctly: font_size is the only field consumed downstream during workspace creation, so no other inherited state is lost.

Confidence Score: 5/5

  • Safe to merge — the fix is narrowly targeted, eliminates the dangling-pointer class entirely by design, and is backed by a behavioral regression test that exercises the exact failure mode.
  • The change is minimal and mechanically correct: a raw-pointer-carrying C struct in a snapshot is replaced by a plain scalar, and the config is rebuilt from a safe zero-initialized base. The regression test verifies the sanitization end-to-end. No other callsites of the snapshot field exist. The one non-critical observation (redundant > 0 guard in workspaceCreationConfigTemplate) is harmless defensive code and has no impact on correctness.
  • No files require special attention.

Important Files Changed

Filename Overview
Sources/TabManager.swift Fixes dangling-pointer crash: replaces full ghostty_surface_config_s snapshot with a scalar Float? for font size, then rebuilds a clean config at workspace creation time; also widens inheritedTerminalConfigForNewWorkspace from private to internal to enable test subclassing.
cmuxTests/WorkspaceUnitTests.swift Adds WorkspaceCreationConfigSanitizationTests: injects a pointer-backed ghostty_surface_config_s via a TabManager subclass and asserts that only font_size survives into the config template forwarded to makeWorkspaceForCreation; memory correctly cleaned up in deinit.

Sequence Diagram

sequenceDiagram
    participant User as User (Cmd+N)
    participant TM as TabManager
    participant Snap as WorkspaceCreationSnapshot
    participant Surface as ghostty surface (may teardown)
    participant Config as workspaceCreationConfigTemplate

    User->>TM: addWorkspace()
    TM->>TM: workspaceCreationSnapshot()
    TM->>Surface: inheritedTerminalConfigForNewWorkspace()
    Surface-->>TM: ghostty_surface_config_s (with raw pointers)
    TM->>TM: inheritedTerminalFontPointsForNewWorkspace()<br/>extracts font_size Float only
    TM->>Snap: store inheritedTerminalFontPoints: Float?<br/>(NO raw pointers retained)
    Note over Surface: Surface may teardown here —<br/>safe because no pointer was stored

    TM->>Config: workspaceCreationConfigTemplate(inheritedTerminalFontPoints:)
    Config->>Config: ghostty_surface_config_new()<br/>config.font_size = inheritedTerminalFontPoints
    Config-->>TM: clean ghostty_surface_config_s (pointer-free)
    TM->>TM: makeWorkspaceForCreation(configTemplate: clean config)
Loading

Reviews (1): Last reviewed commit: "Sanitize workspace creation config snaps..." | Re-trigger Greptile

Comment thread Sources/TabManager.swift
Comment on lines +2256 to 2258
func inheritedTerminalConfigForNewWorkspace(
workspace: Workspace?
) -> ghostty_surface_config_s? {

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 private widened to internal for test subclassing

inheritedTerminalConfigForNewWorkspace(workspace:) is now internal (no explicit access modifier) so UnsafeConfigSnapshotTabManager in the test target can override it. This is a pragmatic and functional approach given the test harness, but it permanently exposes this method to all callers within the module, not just the test.

An alternative that keeps the method private and avoids a permanent API surface increase is injecting the config source as a closure or a small protocol:

// In TabManager, keep private:
private var inheritedConfigProvider: (Workspace?) -> ghostty_surface_config_s? = { [weak self] in
    self?.inheritedTerminalConfigForNewWorkspace(workspace: $0)
}

The test then just swaps out manager.inheritedConfigProvider instead of subclassing. Not a blocker — the current approach is correct and testable — but worth considering if the codebase discourages widening private to internal solely for tests.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@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

🧹 Nitpick comments (1)
cmuxTests/WorkspaceUnitTests.swift (1)

522-553: Exercise the post-snapshot teardown window in this regression.

The injected C buffers stay alive until UnsafeConfigSnapshotTabManager deinit, so this only proves the final configTemplate is sanitized. It will still pass if WorkspaceCreationSnapshot starts retaining pointer-backed data again and only strips it later. Consider invalidating/freeing the injected config right after didCaptureWorkspaceCreationSnapshot() and then asserting addWorkspace() still survives that path.

Also applies to: 581-596

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

In `@cmuxTests/WorkspaceUnitTests.swift` around lines 522 - 553, The test
currently leaves the C-backed buffers live until UnsafeConfigSnapshotTabManager
deinit, so it only verifies final config sanitization; modify the test to
free/invalidate the injected config immediately after calling
didCaptureWorkspaceCreationSnapshot() for the WorkspaceCreationSnapshot path:
call the existing teardown logic (deallocate retainedEnvVars, free
retainedCStringPointers and null out injectedConfig/its pointers) right after
didCaptureWorkspaceCreationSnapshot() and then assert that addWorkspace() still
succeeds; focus changes around installInjectedConfig,
didCaptureWorkspaceCreationSnapshot(), and addWorkspace() to ensure the snapshot
consumer does not retain pointer-backed data beyond that call.
🤖 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/TabManager.swift`:
- Around line 2286-2298: The function workspaceCreationConfigTemplate creates a
ghostty_surface_config_s via ghostty_surface_config_new() but currently only
sets font_size, leaving other pointer fields uninitialized; before returning
from workspaceCreationConfigTemplate, explicitly zero or nil out all
pointer/reference members of the ghostty_surface_config_s (the fields that could
hold C pointers) so the struct contains no indeterminate pointer values when
later passed to ghostty_surface_new(); update workspaceCreationConfigTemplate to
set those pointer fields to nil/0 (or otherwise clear them) after calling
ghostty_surface_config_new() and before returning the config.

---

Nitpick comments:
In `@cmuxTests/WorkspaceUnitTests.swift`:
- Around line 522-553: The test currently leaves the C-backed buffers live until
UnsafeConfigSnapshotTabManager deinit, so it only verifies final config
sanitization; modify the test to free/invalidate the injected config immediately
after calling didCaptureWorkspaceCreationSnapshot() for the
WorkspaceCreationSnapshot path: call the existing teardown logic (deallocate
retainedEnvVars, free retainedCStringPointers and null out injectedConfig/its
pointers) right after didCaptureWorkspaceCreationSnapshot() and then assert that
addWorkspace() still succeeds; focus changes around installInjectedConfig,
didCaptureWorkspaceCreationSnapshot(), and addWorkspace() to ensure the snapshot
consumer does not retain pointer-backed data beyond that call.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bcf76334-8a37-4d99-b688-72875778d586

📥 Commits

Reviewing files that changed from the base of the PR and between 0a1d8c2 and a00e617.

📒 Files selected for processing (2)
  • Sources/TabManager.swift
  • cmuxTests/WorkspaceUnitTests.swift

Comment thread Sources/TabManager.swift
Comment on lines +2286 to +2298
private func workspaceCreationConfigTemplate(
inheritedTerminalFontPoints: Float?
) -> ghostty_surface_config_s? {
guard let inheritedTerminalFontPoints, inheritedTerminalFontPoints > 0 else {
return nil
}
// ghostty_surface_config_s can carry raw C pointers owned by the source surface.
// New workspace creation only needs the inherited zoom level, so rebuild a clean
// config instead of snapshotting pointer-backed fields across workspace creation.
var config = ghostty_surface_config_new()
config.font_size = inheritedTerminalFontPoints
return config
}

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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check if TabManager.swift exists and read the specified lines
if [ -f "Sources/TabManager.swift" ]; then
  wc -l Sources/TabManager.swift
  echo "---"
  sed -n '2276,2298p' Sources/TabManager.swift
else
  # Try to locate TabManager.swift
  find . -name "TabManager.swift" -type f
fi

Repository: manaflow-ai/cmux

Length of output: 1086


🏁 Script executed:

# Search for ghostty_surface_config definition and usage
rg "ghostty_surface_config" -A 3 -B 1 --max-count 20

Repository: manaflow-ai/cmux

Length of output: 9223


🏁 Script executed:

# Look for the Ghostty header file in the repo
find . -name "ghostty.h" -o -name "*ghostty*.h" | head -20

Repository: manaflow-ai/cmux

Length of output: 71


🏁 Script executed:

# Read the ghostty.h header to understand the struct and constructor
cat -n ghostty.h | grep -A 40 "ghostty_surface_config_s" | head -60

Repository: manaflow-ai/cmux

Length of output: 2266


🏁 Script executed:

# Look for the implementation or documentation of ghostty_surface_config_new()
# Check if there's a build directory or any implementation hints
rg "ghostty_surface_config_new" -B 3 -A 10

Repository: manaflow-ai/cmux

Length of output: 6034


🏁 Script executed:

# Check how the config is actually used across the codebase to understand the risk
rg "ghostty_surface_config_new\(\)" -A 5 | head -80

Repository: manaflow-ai/cmux

Length of output: 2722


🏁 Script executed:

# Search for any comments or documentation about field initialization
rg "zero.init|zero-init|zero.initialize" -i -C 2

Repository: manaflow-ai/cmux

Length of output: 1237


🏁 Script executed:

# Get the exact struct definition to see all pointer fields
sed -n '400,453p' ghostty.h | cat -n

Repository: manaflow-ai/cmux

Length of output: 1575


Explicitly clear uninitialized pointer fields before returning.

The Ghostty header doesn't document that ghostty_surface_config_new() zero-initializes all members, and C struct constructors across FFI boundaries don't guarantee initialization. Since this config is passed to ghostty_surface_new(), uninitialized pointer values could introduce undefined behavior. The test code (WorkspaceUnitTests.swift) explicitly sets all pointer fields after construction, establishing the expected pattern. Clear the fields you don't use.

Suggested hardening
     var config = ghostty_surface_config_new()
+    config.working_directory = nil
+    config.command = nil
+    config.env_vars = nil
+    config.env_var_count = 0
+    config.initial_input = nil
     config.font_size = inheritedTerminalFontPoints
     return config
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/TabManager.swift` around lines 2286 - 2298, The function
workspaceCreationConfigTemplate creates a ghostty_surface_config_s via
ghostty_surface_config_new() but currently only sets font_size, leaving other
pointer fields uninitialized; before returning from
workspaceCreationConfigTemplate, explicitly zero or nil out all
pointer/reference members of the ghostty_surface_config_s (the fields that could
hold C pointers) so the struct contains no indeterminate pointer values when
later passed to ghostty_surface_new(); update workspaceCreationConfigTemplate to
set those pointer fields to nil/0 (or otherwise clear them) after calling
ghostty_surface_config_new() and before returning the config.

@austinywang
austinywang merged commit b93be12 into main Mar 26, 2026
20 checks passed
Jesssullivan added a commit to Jesssullivan/cmux that referenced this pull request Mar 26, 2026
Ingests all upstream fixes since 2026-03-22 including:
- Fix Cmd+N crash: retain snapshot workspaces (manaflow-ai#2183, manaflow-ai#2181, manaflow-ai#2178, manaflow-ai#2173)
- Fix browser pane restore after reopen (manaflow-ai#2141)
- Fix Ghostty resize_split keybind (manaflow-ai#1899)
- Reduce shell integration prompt latency (manaflow-ai#2109)
- Fix command palette focus after terminal find (manaflow-ai#2089)
- Add Codex CLI hooks (manaflow-ai#2103)
- Add cmux.json custom commands (manaflow-ai#2011)
- Fix window position restore on relaunch (manaflow-ai#2129)

Conflict resolution:
- BrowserPanel.swift: accepted upstream configureWebViewConfiguration()
  refactor (already includes our forMainFrameOnly:true CAPTCHA fix from PR manaflow-ai#1877)

Fork-specific files preserved:
- Sources/Panels/WebAuthn{Coordinator,BridgeJavaScript}.swift
- Sources/FIDO2/module.modulemap
- vendor/ctap2 submodule
- cmux.entitlements (with camera/audio-input removed)
- cmux.embedded.entitlements
- .github/workflows/fork-{ci,release}.yml
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
…#2178)

* Add workspace config sanitization regression test

* Sanitize workspace creation config snapshots

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
Preview — a00e6175 Deployed Mar 26, 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 in workspaceCreationSnapshot on new workspace (post #2017 fix)

2 participants