Skip to content

Fix Release-only Cmd+N workspace snapshot UAF - #2181

Merged
austinywang merged 3 commits into
mainfrom
issue-2180-cmd-n-retain-crash
Mar 26, 2026
Merged

austinywang merged 3 commits into
mainfrom
issue-2180-cmd-n-retain-crash

Conversation

@austinywang

@austinywang austinywang commented Mar 26, 2026 •

Copy link
Copy Markdown
Contributor

Closes #2180

Summary

  • add a regression test that closes a workspace after the Cmd+N snapshot is captured and verifies the captured workspace stays alive until creation finishes
  • keep the pre-creation tabs array alive for the full addWorkspace() path so Release ARC cannot drop captured Workspace references before insertion
  • build the workspace-creation snapshot from the captured tabs and selected tab id instead of re-reading live Combine-backed state mid-creation

Testing

  • not run locally per repo policy
  • built Debug app with ./scripts/reload.sh --tag cmd-n-retain-fix --launch
  • built isolated Release app with ./scripts/reloads.sh --tag cmd-n-retain-fix

Summary by CodeRabbit

  • Bug Fixes

    • Ensures a captured workspace remains alive through the entire creation flow so placement and selection decisions use the pre-captured state, preventing premature deallocation and incorrect insertion behavior.
  • Tests

    • Added a test validating that a captured workspace persists until creation completes, is inserted after the current workspace, becomes selected, and is released afterward.

@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 2:05am

@coderabbitai

coderabbitai Bot commented Mar 26, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d8622bd2-f07e-4d1a-affd-03fc5f0f3fae

📥 Commits

Reviewing files that changed from the base of the PR and between 09872b6 and 8085719.

📒 Files selected for processing (1)
  • cmuxTests/WorkspaceUnitTests.swift
✅ Files skipped from review due to trivial changes (1)
  • cmuxTests/WorkspaceUnitTests.swift

📝 Walkthrough

Walkthrough

Capture tabs and selectedTabId early in addWorkspace, compute the creation snapshot from those captured values, and wrap the workspace-creation flow in withExtendedLifetime so captured workspaces remain alive through insertion. A unit test verifies the captured workspace stays alive until creation finishes.

Changes

Cohort / File(s) Summary
Lifetime-managed snapshot capture
Sources/TabManager.swift
addWorkspace(...) now captures tabs and selectedTabId, uses withExtendedLifetime(capturedTabs) around workspace creation, returns newWorkspace from inside the closure, and adds workspaceCreationSnapshot(currentTabs:currentSelectedTabId:) with the original workspaceCreationSnapshot() delegating to it. Adjusted control flow so snapshot/insert decisions use pre-captured state.
Workspace lifetime test
cmuxTests/WorkspaceUnitTests.swift
Added testAddWorkspaceKeepsCapturedWorkspaceAliveUntilCreationFinishes to exercise capturing/mutation/closure during snapshotting; uses a weak reference to assert the captured workspace is alive during creation and deallocated after insertion completes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I nudged the tabs, I held them tight,
Captured their names through the busy night.
With lifetimes long and snapshots clear,
Cmd+N births without the fear —
A tiny hop, a stable light. ✨

🚥 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 'Fix Release-only Cmd+N workspace snapshot UAF' clearly and specifically describes the main change—fixing a use-after-free bug in the Cmd+N workspace snapshot flow that only occurs in Release builds.
Description check ✅ Passed The description covers the summary (what changed and why), testing methodology, and is properly structured, though some optional sections like demo video and checklist are missing but not critical for this bug fix.
Linked Issues check ✅ Passed The code changes directly address all primary objectives from issue #2180: capturing tabs/selectedTabId to prevent premature ARC deallocation, using withExtendedLifetime to extend object lifetime through the full creation path, computing snapshot from captured state instead of live Combine state, and adding a regression test.
Out of Scope Changes check ✅ Passed All changes in TabManager.swift and WorkspaceUnitTests.swift are directly scoped to fixing the Cmd+N workspace snapshot use-after-free bug identified in issue #2180; no unrelated changes are present.

✏️ 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-2180-cmd-n-retain-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

@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

🤖 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`:
- Line 1240: Replace the bare title literal used where the workspace/tab is
created (the title: "Terminal \(nextTabCount)" occurrence) with a localized
string lookup using String(localized: ...) and format the number, e.g. use
String(localized: "workspace.title.default", defaultValue: "Terminal
%d").formatted(nextTabCount) (referencing the existing nextTabCount and the
title: parameter), and add the key "workspace.title.default" with the English
default "Terminal %d" to your Localizable.xcstrings.
🪄 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: a107ebdd-63ba-487f-9d05-4692a3639c50

📥 Commits

Reviewing files that changed from the base of the PR and between 18531fd and 09872b6.

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

Comment thread Sources/TabManager.swift
let ordinal = Self.nextPortOrdinal
Self.nextPortOrdinal += 1
let newWorkspace = makeWorkspaceForCreation(
title: "Terminal \(nextTabCount)",

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

Localize the default workspace title at Line 1240.

"Terminal \(nextTabCount)" is user-visible and should use a localization key.

🌐 Proposed fix
-                title: "Terminal \(nextTabCount)",
+                title: String(
+                    localized: "workspace.title.default",
+                    defaultValue: "Terminal \(nextTabCount)"
+                ),

Also add workspace.title.default to Resources/Localizable.xcstrings.

As per coding guidelines: “All user-facing strings must be localized using String(localized: "key.name", defaultValue: "English text") … Never use bare string literals … or other UI elements”.

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

In `@Sources/TabManager.swift` at line 1240, Replace the bare title literal used
where the workspace/tab is created (the title: "Terminal \(nextTabCount)"
occurrence) with a localized string lookup using String(localized: ...) and
format the number, e.g. use String(localized: "workspace.title.default",
defaultValue: "Terminal %d").formatted(nextTabCount) (referencing the existing
nextTabCount and the title: parameter), and add the key
"workspace.title.default" with the English default "Terminal %d" to your
Localizable.xcstrings.

@greptile-apps

greptile-apps Bot commented Mar 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a Release-mode use-after-free crash triggered by the Cmd+N workspace creation path: the Swift ARC optimizer could release Workspace objects captured in the pre-creation tabs snapshot before addWorkspace() finished inserting the new workspace, causing crashes in swift_retain. The fix captures tabs and selectedTabId before the critical section and wraps the entire addWorkspace() body in withExtendedLifetime(capturedTabs) to prevent early release; it also threads the captured values directly into workspaceCreationSnapshot() so the snapshot is always built from consistent pre-creation state rather than live Combine-backed accessors.\n\nKey changes:\n- addWorkspace() now calls withExtendedLifetime(capturedTabs) { ... } around the full creation closure, guaranteeing Release-ARC cannot drop captured Workspace references until after insertion completes.\n- workspaceCreationSnapshot() gains a parameterized overload accepting pre-captured currentTabs / currentSelectedTabId; the original no-arg overload is preserved for the unrelated newTabInsertIndex call-site.\n- A two-commit regression test (testAddWorkspaceKeepsCapturedWorkspaceAliveUntilCreationFinishes) is added following the repo's required test-first-then-fix commit structure — but the test itself has a compile-blocking bug (closingWorkspace = nil after guard let closingWorkspace creates an immutable non-optional shadow) that must be fixed before the test target will build.

Confidence Score: 4/5

Production fix is correct and well-reasoned; the regression test has a compile error that blocks the test target build but does not affect app behavior.

The withExtendedLifetime approach is the standard Swift idiom for this exact problem, the comment accurately describes the Release-ARC hazard, and the refactor of workspaceCreationSnapshot is clean. The only blocker is a guard let shadow making closingWorkspace = nil a compile error in the new test, preventing the test target from building and the regression from being verified by CI. One targeted fix to the test is needed before merging.

cmuxTests/WorkspaceUnitTests.swift — the new regression test needs the guard let / nil assignment fixed before CI can confirm the test passes.

Important Files Changed

Filename Overview
Sources/TabManager.swift Wraps the full addWorkspace() body in withExtendedLifetime(capturedTabs) and threads pre-captured tabs/selectedTabId into workspaceCreationSnapshot; the no-arg overload is preserved for the unrelated newTabInsertIndex call-site. Logic is sound and the comment accurately describes the Release-ARC hazard being fixed.
cmuxTests/WorkspaceUnitTests.swift New regression test testAddWorkspaceKeepsCapturedWorkspaceAliveUntilCreationFinishes has a compile-blocking bug: closingWorkspace = nil is written after guard let closingWorkspace creates a non-optional let shadow, making the assignment a type/mutability error and preventing the test target from building.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant TabManager
    participant ARC as Swift ARC (Release)
    participant Workspace

    Caller->>TabManager: addWorkspace(placementOverride:)
    TabManager->>TabManager: capturedTabs = tabs (strong copy)
    TabManager->>TabManager: capturedSelectedTabId = selectedTabId
    Note over TabManager,ARC: withExtendedLifetime(capturedTabs) begins — ARC blocked from releasing array
    TabManager->>TabManager: workspaceCreationSnapshot(currentTabs: capturedTabs, ...)
    TabManager->>TabManager: didCaptureWorkspaceCreationSnapshot()
    Note over Workspace: (mid-creation: external close may remove workspace from tabs)
    ARC--xWorkspace: [blocked] early release of captured Workspace prevented
    TabManager->>TabManager: makeWorkspaceForCreation(...)
    TabManager->>TabManager: var updatedTabs = tabs (live, post-close)
    TabManager->>TabManager: updatedTabs.insert(newWorkspace, at: insertIndex)
    TabManager->>TabManager: tabs = updatedTabs
    Note over TabManager,ARC: withExtendedLifetime ends — capturedTabs released
    ARC->>Workspace: release captured Workspace (ref count may drop to 0)
    TabManager-->>Caller: return newWorkspace
Loading

Reviews (1): Last reviewed commit: "fix: retain snapshot workspaces through ..." | Re-trigger Greptile

Comment thread cmuxTests/WorkspaceUnitTests.swift Outdated
Comment on lines +462 to +470
guard let closingWorkspace else {
XCTFail("Expected secondary workspace")
return
}

let closingWorkspaceId = closingWorkspace.id
weak var weakClosingWorkspace = closingWorkspace
XCTAssertEqual(manager.tabs.map(\.id), [first.id, closingWorkspaceId, third.id])
closingWorkspace = 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.

P1 guard let shadow makes closingWorkspace = nil a compile error

After guard let closingWorkspace (SE-0345 shorthand) on line 462, the name closingWorkspace in the continuation scope refers to a new non-optional let constant (Workspace, not Workspace?), shadowing the original var Workspace? declared on line 458. closingWorkspace = nil on line 470 therefore fails to compile on two counts: the binding is immutable (let) and the type doesn't accept nil.

Because the assignment cannot compile, the test target would not build, meaning the regression test can never actually run — and the intended strong-reference drop never happens anyway, so weakClosingWorkspace would remain non-nil through the whole function regardless of withExtendedLifetime.

The simplest fix is to avoid the shorthand guard and nil-out the original var directly:

XCTAssertNotNil(closingWorkspace, "Expected secondary workspace")
guard closingWorkspace != nil else { return }

let closingWorkspaceId = closingWorkspace!.id
weak var weakClosingWorkspace = closingWorkspace
XCTAssertEqual(manager.tabs.map(\.id), [first.id, closingWorkspaceId, third.id])
closingWorkspace = nil  // drops the local strong ref; original var is still Workspace?

Or use a distinct holder name so both the unwrapped let and the nullable var coexist:

var closingWorkspaceHolder: Workspace? = manager.addWorkspace()
let third = manager.addWorkspace()
manager.selectWorkspace(third)

guard let closingWorkspace = closingWorkspaceHolder else {
    XCTFail("Expected secondary workspace")
    return
}

let closingWorkspaceId = closingWorkspace.id
weak var weakClosingWorkspace: Workspace? = closingWorkspace
XCTAssertEqual(manager.tabs.map(\.id), [first.id, closingWorkspaceId, third.id])
closingWorkspaceHolder = nil   // drop the holder's strong ref

@austinywang
austinywang merged commit 61e6a0e into main Mar 26, 2026
14 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
* test: reproduce Cmd+N snapshot workspace lifetime race

* fix: retain snapshot workspaces through Cmd+N creation

* fix: repair workspace lifetime regression test

This branch was successfully deployed

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

Cmd+N crash: swift_retain PAC failure in addWorkspace (Release-only, post #2178)

1 participant