Skip to content

Fix workspace creation snapshot crash - #2176

Merged
austinywang merged 4 commits into
mainfrom
issue-2157-workspace-creation-snapshot-crash
Mar 26, 2026
Merged

austinywang merged 4 commits into
mainfrom
issue-2157-workspace-creation-snapshot-crash

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Mar 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add a regression test for workspace creation config snapshots
  • stop snapshotting the full ghostty_surface_config_s across workspace creation
  • carry only inherited font size forward and rebuild a fresh config template when needed

Why

This follows up on #2157 after earlier attempts still left workspace creation holding onto pointer-backed config data from an existing surface.

Testing

  • xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/cmux-issue-2157-build-for-testing-final build-for-testing
  • ./scripts/reload.sh --tag issue-2157-workspace-creation-snapshot-crash

Notes

  • Local cmux-unit runtime execution is currently blocked by an unrelated host app bootstrap crash on this machine, including for unchanged baseline tests.

Summary by CodeRabbit

  • Refactor

    • Improved internal handling of terminal configuration inheritance when creating new tabs and workspaces.
  • Tests

    • Added test coverage for configuration sanitization during workspace creation.

@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 10:27pm

@coderabbitai

coderabbitai Bot commented Mar 26, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Tab snapshots are refactored to capture only inheritedTerminalFontPoints: Float? instead of the full inherited config structure. Workspace creation now uses this font value to build a fresh config template via workspaceCreationConfigTemplate(...). The method inheritedTerminalConfigForNewWorkspace(...) visibility is changed to internal, and new helpers extract font points and construct the template.

Changes

Cohort / File(s) Summary
TabManager Configuration Refactoring
Sources/TabManager.swift
Changed tab creation snapshots to capture numeric font points instead of full config structs. Made inheritedTerminalConfigForNewWorkspace(...) non-private; added inheritedTerminalFontPointsForNewWorkspace(...) and workspaceCreationConfigTemplate(...) helpers to build fresh configs from captured font sizes.
Workspace Config Sanitization Tests
cmuxTests/WorkspaceUnitTests.swift
Added WorkspaceCreationConfigSanitizationTests suite validating that config templates preserve font_size while clearing pointer-backed fields (working_directory, command, env_vars) and resetting env_var_count to 0.

Possibly related PRs

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A snapshot once held configs whole and grand,
But now just font points fit snugly in its hand—
Where templates craft fresh configs with care,
No pointers dangling in template air,
A cleaner inheritance, sanitized and fair! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix workspace creation snapshot crash' directly relates to the main change: resolving a crash in workspace creation by sanitizing config snapshots to prevent pointer-backed data retention.
Description check ✅ Passed The description covers required sections: a clear summary explaining what changed and why, testing instructions provided, and a checklist partially completed with relevant items.

✏️ 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-workspace-creation-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 crash during workspace creation by stopping the full ghostty_surface_config_s struct — which carries raw C pointers owned by the source surface — from being snapshotted across workspace creation. Instead, only the font size (Float?) is extracted at snapshot time, and a fresh, pointer-safe config is rebuilt from scratch when the workspace is actually created. A well-structured regression test (using the existing didCaptureWorkspaceCreationSnapshot seam) validates that the config template reaching makeWorkspaceForCreation contains no dangling pointer fields even after the original backing buffers are freed.

  • WorkspaceCreationSnapshot.inheritedTerminalConfig: ghostty_surface_config_s? replaced with inheritedTerminalFontPoints: Float? — eliminates the root cause of the use-after-free crash.
  • New workspaceCreationConfigTemplate(inheritedTerminalFontPoints:) builds a clean ghostty_surface_config_new() struct at workspace-creation time, setting only font_size.
  • inheritedTerminalConfigForNewWorkspace(workspace:) changed from private to internal to enable test-target overriding via @testable import.
  • Two new helper functions (inheritedTerminalFontPointsForNewWorkspace, workspaceCreationConfigTemplate) are declared internal but only inheritedTerminalConfigForNewWorkspace is actually overridden in tests — the other two could be private func.
  • The two-commit structure (failing test first, fix second) prescribed by the regression test policy is correctly followed in this PR.

Confidence Score: 5/5

  • Safe to merge — the fix correctly eliminates the dangling-pointer crash and the regression test validates the sanitization end-to-end.
  • The root cause (snapshotting a pointer-backed C struct across an async boundary) is precisely addressed. The regression test is well-designed using an existing test seam and would fail without the fix. The two-commit CI-provable structure is followed. The only outstanding item is a minor style preference (two helpers are internal where private would suffice), which has no correctness impact.
  • No files require special attention.

Important Files Changed

Filename Overview
Sources/TabManager.swift Replaces the pointer-unsafe full ghostty_surface_config_s snapshot field with a plain Float? font size; introduces clean workspaceCreationConfigTemplate to reconstruct a fresh struct at use-site. Core fix is correct. Two new helpers are inadvertently internal where private suffices.
cmuxTests/WorkspaceUnitTests.swift Adds WorkspaceCreationConfigSanitizationTests that injects a pointer-backed config, invalidates its backing buffers via the didCaptureWorkspaceCreationSnapshot seam, and asserts the resulting config template has no dangling pointer fields. Well-structured regression test.

Sequence Diagram

sequenceDiagram
    participant AM as addWorkspace()
    participant S as workspaceCreationSnapshot()
    participant H as inheritedTerminalFontPointsForNewWorkspace()
    participant IC as inheritedTerminalConfigForNewWorkspace()
    participant Seam as didCaptureWorkspaceCreationSnapshot()
    participant T as workspaceCreationConfigTemplate()
    participant MWC as makeWorkspaceForCreation()

    AM->>S: capture snapshot
    S->>H: get font size
    H->>IC: get full config (live surface or fallback)
    IC-->>H: ghostty_surface_config_s (may have C pointers)
    H-->>S: font_size: Float? (only scalar extracted)
    S-->>AM: snapshot { inheritedTerminalFontPoints: Float? }
    AM->>Seam: (test seam — source surface buffers may now be freed)
    AM->>T: workspaceCreationConfigTemplate(fontPoints)
    T-->>AM: ghostty_surface_config_new() + font_size only (no C pointers)
    AM->>MWC: configTemplate (clean, pointer-safe)
Loading

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

Comment thread Sources/TabManager.swift Outdated
Comment on lines +2276 to +2298
func inheritedTerminalFontPointsForNewWorkspace(
workspace: Workspace?
) -> Float? {
guard let inheritedConfig = inheritedTerminalConfigForNewWorkspace(workspace: workspace),
inheritedConfig.font_size > 0 else {
return nil
}
return inheritedConfig.font_size
}

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
Contributor

Choose a reason for hiding this comment

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

P2 Helper functions unnecessarily exposed as internal

inheritedTerminalFontPointsForNewWorkspace and workspaceCreationConfigTemplate are both declared func (internal visibility) but neither is overridden anywhere — the test subclass only overrides inheritedTerminalConfigForNewWorkspace (which does need to be internal for the @testable import override). These two helpers could be private func to keep the internal surface of TabManager minimal.

Suggested change
func inheritedTerminalFontPointsForNewWorkspace(
workspace: Workspace?
) -> Float? {
guard let inheritedConfig = inheritedTerminalConfigForNewWorkspace(workspace: workspace),
inheritedConfig.font_size > 0 else {
return nil
}
return inheritedConfig.font_size
}
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
}
private func inheritedTerminalFontPointsForNewWorkspace(
workspace: Workspace?
) -> Float? {
guard let inheritedConfig = inheritedTerminalConfigForNewWorkspace(workspace: workspace),
inheritedConfig.font_size > 0 else {
return nil
}
return inheritedConfig.font_size
}
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
}

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.

🧹 Nitpick comments (3)
Sources/TabManager.swift (1)

2286-2298: Consider explicit initialization for defensive clarity, though the code is currently safe.

The test testAddWorkspacePassesSanitizedInheritedConfigTemplateAfterSourceBuffersAreReleased confirms that ghostty_surface_config_new() properly initializes pointer-backed fields (working_directory, command, env_vars, env_var_count) to nil/0. Additionally, context is always set at GhosttyTerminalView.swift:3501 before ghostty_surface_new() is called, so relying on downstream assignment is safe. The initial_input field is not used anywhere in the codebase.

That said, explicitly nil-ing these fields in the template would be more defensive and clarify intent, especially if Ghostty's initialization contract ever changes. If you add these assignments, extend the regression test to cover initial_input and context for consistency (the latter should be GHOSTTY_SURFACE_CONTEXT_UNINITIALIZED or equivalent before downstream assignment).

🤖 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
workspaceCreationConfigTemplate currently returns a config from
ghostty_surface_config_new() but should explicitly clear pointer-backed fields
for defensive clarity: after creating var config = ghostty_surface_config_new()
set config.working_directory = nil, config.command = nil, config.env_vars = nil,
config.env_var_count = 0 and also explicitly clear config.initial_input and set
config.context = GHOSTTY_SURFACE_CONTEXT_UNINITIALIZED (or the equivalent
sentinel) before setting config.font_size and returning; then extend the test
testAddWorkspacePassesSanitizedInheritedConfigTemplateAfterSourceBuffersAreReleased
to assert that initial_input and context are sanitized in addition to the other
pointer-backed fields.
cmuxTests/WorkspaceUnitTests.swift (2)

559-577: Run the captured template through the real creation path.

Line 574 replaces configTemplate with nil, so this regression never executes the workspace-creation code that used to consume the inherited snapshot. Capturing the argument is useful, but I’d still forward the same template into super.makeWorkspaceForCreation(...) so a stale-pointer regression fails on the actual runtime path, not only in the intercepted copy.

♻️ Suggested change
         override func makeWorkspaceForCreation(
             title: String,
             workingDirectory: String?,
             portOrdinal: Int,
             configTemplate: ghostty_surface_config_s?,
             initialTerminalCommand: String?,
             initialTerminalEnvironment: [String: String]
         ) -> Workspace {
             capturedConfigTemplate = configTemplate
             // The assertion is on the captured template; avoid dereferencing any injected
             // pointer-backed fields here so the test can safely detect unsanitized state.
             return super.makeWorkspaceForCreation(
                 title: title,
                 workingDirectory: workingDirectory,
                 portOrdinal: portOrdinal,
-                configTemplate: nil,
+                configTemplate: configTemplate,
                 initialTerminalCommand: initialTerminalCommand,
                 initialTerminalEnvironment: initialTerminalEnvironment
             )
         }

Based on learnings, "Do not add tests that only verify source code text, method signatures, AST fragments, or grep-style patterns. Tests must verify observable runtime behavior through executable paths (unit/integration/e2e/CLI), not implementation shape."

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

In `@cmuxTests/WorkspaceUnitTests.swift` around lines 559 - 577, The override of
makeWorkspaceForCreation captures configTemplate into capturedConfigTemplate but
then passes nil to super, preventing the real creation path from exercising the
original template; modify the override (makeWorkspaceForCreation) to forward the
captured configTemplate (the configTemplate parameter) into
super.makeWorkspaceForCreation instead of nil so the inherited
workspace-creation logic runs with the real snapshot while still preserving
capturedConfigTemplate for assertions.

516-520: Add a fallback cleanup for the malloc-backed fixture.

If this helper exits before didCaptureWorkspaceCreationSnapshot() runs, the strdup/allocate buffers never get released. A small deinit guard makes the test fixture safe for early-failure paths too.

🧹 Suggested cleanup
     private final class UnsafeConfigSnapshotTabManager: TabManager {
+        deinit {
+            invalidateInjectedConfig()
+        }
+
         private var retainedCStringPointers: [UnsafeMutablePointer<CChar>] = []
         private var retainedEnvVars: UnsafeMutablePointer<ghostty_env_var_s>?
         private var injectedConfig: ghostty_surface_config_s?
         var capturedConfigTemplate: ghostty_surface_config_s?

Also applies to: 580-591

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

In `@cmuxTests/WorkspaceUnitTests.swift` around lines 516 - 520,
UnsafeConfigSnapshotTabManager currently retains malloc/strdup-backed buffers
(retainedCStringPointers, retainedEnvVars, and possibly members of
injectedConfig) that are only freed when didCaptureWorkspaceCreationSnapshot()
runs; add a deinit to safely free these on early exit: iterate
retainedCStringPointers and free() each pointer and clear the array, deallocate
retainedEnvVars if non-nil, and release any malloced fields inside
injectedConfig (e.g., C string pointers) before dropping it so the fixture
doesn't leak if the helper exits early.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cmuxTests/WorkspaceUnitTests.swift`:
- Around line 559-577: The override of makeWorkspaceForCreation captures
configTemplate into capturedConfigTemplate but then passes nil to super,
preventing the real creation path from exercising the original template; modify
the override (makeWorkspaceForCreation) to forward the captured configTemplate
(the configTemplate parameter) into super.makeWorkspaceForCreation instead of
nil so the inherited workspace-creation logic runs with the real snapshot while
still preserving capturedConfigTemplate for assertions.
- Around line 516-520: UnsafeConfigSnapshotTabManager currently retains
malloc/strdup-backed buffers (retainedCStringPointers, retainedEnvVars, and
possibly members of injectedConfig) that are only freed when
didCaptureWorkspaceCreationSnapshot() runs; add a deinit to safely free these on
early exit: iterate retainedCStringPointers and free() each pointer and clear
the array, deallocate retainedEnvVars if non-nil, and release any malloced
fields inside injectedConfig (e.g., C string pointers) before dropping it so the
fixture doesn't leak if the helper exits early.

In `@Sources/TabManager.swift`:
- Around line 2286-2298: The workspaceCreationConfigTemplate currently returns a
config from ghostty_surface_config_new() but should explicitly clear
pointer-backed fields for defensive clarity: after creating var config =
ghostty_surface_config_new() set config.working_directory = nil, config.command
= nil, config.env_vars = nil, config.env_var_count = 0 and also explicitly clear
config.initial_input and set config.context =
GHOSTTY_SURFACE_CONTEXT_UNINITIALIZED (or the equivalent sentinel) before
setting config.font_size and returning; then extend the test
testAddWorkspacePassesSanitizedInheritedConfigTemplateAfterSourceBuffersAreReleased
to assert that initial_input and context are sanitized in addition to the other
pointer-backed fields.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6eab8a26-4451-4c3f-8629-f2ab3f5cb094

📥 Commits

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

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

austinywang and others added 2 commits March 26, 2026 15:23
…creation-snapshot-crash

# Conflicts:
#	Sources/TabManager.swift
Main already has this test class; the branch's version was a duplicate
that would cause a compilation error after merge.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@austinywang
austinywang merged commit bc9e45c into main Mar 26, 2026
14 checks passed
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
* Add workspace creation config regression test

* Sanitize workspace creation config snapshot

* Remove duplicate WorkspaceCreationConfigSanitizationTests class

Main already has this test class; the branch's version was a duplicate
that would cause a compilation error after merge.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
Co-authored-by: austinpower1258 <austinwang115@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 558e6641 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.

2 participants