Skip to content

fix: resolve CheckedContinuation deadlock in NetworkTransport on actor dealloc (Swift 6.3)#213

Draft
doozMen wants to merge 3 commits intomodelcontextprotocol:mainfrom
doozMen:fix/atomic-continuation-guards
Draft

fix: resolve CheckedContinuation deadlock in NetworkTransport on actor dealloc (Swift 6.3)#213
doozMen wants to merge 3 commits intomodelcontextprotocol:mainfrom
doozMen:fix/atomic-continuation-guards

Conversation

@doozMen
Copy link

@doozMen doozMen commented Mar 18, 2026

Summary

Fixes #214CheckedContinuation never resumed in NetworkTransport send()/sendHeartbeat()/receiveData() when the actor deallocates during an in-flight NWConnection operation. This causes a permanent deadlock.

Discovered via Swift 6.3 strict concurrency checking, but the bug exists in all Swift versions using this transport.

Changes

NetworkTransport.swift

  • send(): Changed completion closure capture from [weak self] to [weak self, continuation]. Added continuation.resume(throwing:) in the guard let self else branch.
  • sendHeartbeat(): Same fix — completion closure now captures continuation directly and resumes with error on dealloc.
  • receiveData(): Changed from implicit strong self capture to explicit [weak self, continuation] for consistency. All four branches already resumed the continuation; this hardens the capture semantics.

Client.swift

  • withBatch(): Fixed early-return path to return result instead of implicit void (was broken for non-Void generic T).

ClientTests.swift

  • Added testBatchRequestEmptyNonVoid() — verifies withBatch returns the closure result when no requests are added (non-Void path).

Housekeeping

  • Removed accidentally committed .claude/settings.local.json and .swift-version
  • Added both to .gitignore

The Pattern

// BEFORE (deadlock on dealloc):
completion: .contentProcessed { [weak self] error in
    guard let self else { return }  // ← continuation leaked!

// AFTER (safe):
completion: .contentProcessed { [weak self, continuation] error in
    guard let self else {
        continuation.resume(throwing: MCPError.internalError("Transport deallocated"))
        return
    }

CheckedContinuation is Sendable — safe to capture directly alongside [weak self]. This decouples the continuation's lifetime from the actor, guaranteeing resumption even on dealloc.

Testing

  • swift build — clean
  • swift test --filter testBatchRequestEmpty — 2 tests pass
  • Audited sendHeartbeat() — already correct in outer guard, fixed inner completion
  • Verified Logger is nonisolated let + Sendable — safe to access from completion closures

@doozMen doozMen force-pushed the fix/atomic-continuation-guards branch 2 times, most recently from ec383c2 to 843a4ab Compare March 18, 2026 11:05
@doozMen doozMen marked this pull request as draft March 18, 2026 12:31
@doozMen doozMen changed the title fix: atomic continuation guards for safe single-resumption fix: resolve CheckedContinuation deadlock in NetworkTransport on actor dealloc (Swift 6.3) Mar 18, 2026
@doozMen doozMen force-pushed the fix/atomic-continuation-guards branch from 843a4ab to 54c1ac4 Compare March 22, 2026 10:53
doozMen and others added 2 commits March 22, 2026 11:54
)

Replace nonisolated(unsafe) on non-Sendable CheckedContinuation
properties with a thread-safe LockedValue wrapper using NSLock.

This fixes Swift 6.3-dev build failures without bumping platform
minimums (preserves macOS 13+/iOS 16+).

Fixes modelcontextprotocol#211

Co-authored-by: Stijn Willems <stijn@promptping.ai>
@doozMen doozMen force-pushed the fix/atomic-continuation-guards branch from 54c1ac4 to 70ea8de Compare March 22, 2026 10:55
@movetz
Copy link
Contributor

movetz commented Mar 24, 2026

Fixed in #203 and available in the latest v0.12 release. Please check.

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.

CheckedContinuation never resumed in NetworkTransport send/receive on actor dealloc (Swift 6.3)

2 participants