Repository navigation
Conversation
gRPC-Swift 1.8.0, the newest release on CocoaPods, holds a lock while it registers a task cancellation handler in PassthroughMessageSource.consumeNextElement(). If the consuming task is already cancelled, the handler runs immediately and takes the same lock again, which traps (or deadlocks in release builds). Stopping a synchronizer cancels the block download task mid-stream, so iOS crashed in LightWalletGRPCService.blockStream. update-sources now routes every response stream in LightWalletGRPCService through cancellationSafeResponses(): an uncancelled task drains the gRPC stream, and a cancelled consumer cancels the RPC instead of reaching gRPC's cancellation handler. The script fails if an SDK update changes the call shape it rewrites.
ios-tests is a SwiftPM package pinned to the gRPC-Swift and SwiftNIO versions the CocoaPods build links. CancelRepro reproduces the crash (the same lock.swift precondition as the field report) by reading gRPC's message stream from a cancelled task, and the tests check that the guarded stream throws CancellationError there instead, survives repeated cancellation of the SDK's blockStream shape, passes elements and errors through, and cancels the RPC when its reader is cancelled or dropped. Run with swift test from ios-tests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CHANGELOG
Does this branch warrant an entry to the CHANGELOG?
Dependencies
none
Description
Asana task
Fixes the iOS crash in
LightWalletGRPCService.blockStreamwhen a ZEC wallet's sync is stopped (pause, resync, logout) while blocks are downloading.Root cause. gRPC-Swift 1.8.0 (the newest version on CocoaPods) holds
_lockinPassthroughMessageSource.consumeNextElement()while it registers a task cancellation handler. If the reading task is already cancelled, Swift runs that handler immediately on the same thread, and the handler takes_lockagain. NIO's lock is error-checking in debug builds: the relock returnsEDEADLKand the precondition atlock.swift:74traps. Release builds can deadlock or trap. The crash report's faulting thread shows this chain (consumeNextElement->swift_task_addCancellationHandler-> theonCancelclosure ->Lock.withLock). A channel or call freed under the iterator would not produce that stack. Upstream replaced this code in 1.11.0 (grpc/grpc-swift#1477), which ships only through SwiftPM.Fix.
ios/CancellationSafeStream.swiftaddscancellationSafeResponses()onGRPCAsyncServerStreamingCall. An uncancelled task drains gRPC's stream into anAsyncThrowingStream. When the consumer is cancelled or drops the stream, the RPC is cancelled instead, and gRPC's cancellation handler never runs. A consumer cancelled mid-stream still getsCancellationError, which keeps the SDK's existing "sync cancelled" handling.scripts/updateSources.tsrewrites all six server-streaming call sites inLightWalletGRPCService.swiftand fails if an SDK update changes the shape it matches, so a bump cannot drop the fix unnoticed.Tests.
ios-tests/is a SwiftPM package pinned to the gRPC-Swift and SwiftNIO versions the app links. It is outside the npm package and not run in CI; runswift testfromios-tests.CancelRepro rawreproduces the field crash (same precondition, SIGTRAP). The guarded variant ends withCancellationError.blockStreamshape with random cancellation crashed 5 of 5 runs unguarded and ran 20,000 rounds clean guarded.In-app (iOS sim, edge-react-gui). Stop loops drove a syncing ZEC wallet through resync and pause/unpause while blocks were downloading. The SDK logs confirm the stops landed mid-download (
Sync failed with error: CancellationError()).Task.isCancelledjust before the vulnerable call, so the in-app window is narrow.consumeNextElement(), before it registers the cancellation handler. Both builds used the same edited Pods source.EXC_BREAKPOINT(SIGTRAP). The faulting stack matches the field crash:Lock.withLock<-PassthroughMessageSource.consumeNextElement()onCancel closure <-swift_task_addCancellationHandler<-GRPCAsyncResponseStream.Iterator.next<-LightWalletGRPCService.blockStream.react-native-piratechain vendors an SDK with the same gRPC stream pattern and likely has the same exposure. It is not changed here.
Note
Medium Risk
Changes iOS sync cancellation and all gRPC streaming RPC consumption paths; incorrect wrapping could affect stop/resync behavior, though tests target the failure mode.
Overview
Fixes an iOS crash when stopping wallet sync (pause, resync, logout) while block download is in progress. gRPC-Swift 1.8.0 (CocoaPods) can SIGTRAP/deadlock if the task reading a response stream is cancelled while
PassthroughMessageSourceholds its lock during cancellation handler registration.Adds
CancellationSafeStreamandcancellationSafeResponses()onGRPCAsyncServerStreamingCall: an uncancelled task drains the gRPC stream; when the consumer cancels or drops the stream, the RPC is cancelled instead so gRPC never hits the bad path. Cancelled consumers still seeCancellationError.updateSources.tsrewrites all server-streaming sites in vendoredLightWalletGRPCService.swiftto use the wrapper and throws if an SDK bump changes the expected stream shape. The helper is included in the podspec.ios-tests/(SwiftPM, not in CI) reproduces the crash and tests the guarded behavior.Reviewed by Cursor Bugbot for commit 9858cd9. Bugbot is set up for automated code reviews on this repo. Configure here.
Test evidence
9858cd9Test the gRPC stream cancellation guard
2026-09-25
🪓 added a 50 ms wait in gRPC-Swift PassthroughMessageSource.consumeNextElement before cancellation-handler registration to widen the race window
1. zec wallet menu pause resync mid sync
2. zec wallet alive after 48 stop cycles
3. 🪓 zec wallet alive after forced race cycles