Skip to content

fix(stability): close crash paths found in the 2026-08 crash audit - #910

Merged
romchornyi merged 1 commit into
swift-sdk-integrationfrom
fix/crash-audit
Aug 3, 2026
Merged

fix(stability): close crash paths found in the 2026-08 crash audit#910
romchornyi merged 1 commit into
swift-sdk-integrationfrom
fix/crash-audit

Conversation

@romchornyi

@romchornyi romchornyi commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Issue being fixed or feature implemented

Users reported frequent crashes on 9.0.0 (13). A static audit of DashWallet/Sources (684 Swift + 189 ObjC files) was cross-referenced against device diagnostics from two users:

Device A Device B
OS iOS 26.5.2 iOS 27.0
network testnet mainnet
launches 11 in 85 min 28 over 14 days, incl. an 18 s relaunch

Device A's crashed run left one Starting full transaction reload with no matching Full reload complete (17 vs 16 over the file; the clean run is balanced 9/9), while the Rust/tokio side kept logging for a further 12 s — i.e. the app's Swift side stopped on its own inside HomeViewModel.reloadTxDataSource().

Device B died while idle with the Rust side stopping simultaneously, so it is a second, resource-shaped signature rather than the same bug.

What was done?

Nil / force-unwrap on ordinary paths

  • stopNetworkMonitoring() force-unwrapped reachabilityObserver, only ever assigned by startNetworkMonitoring(). Six adopters pair start in viewDidLoad with stop in deinit, so a controller released before its view loaded crashed.
  • Untyped catch blocks force-cast to Coinbase.Error / CrowdNode.Error — any URLError / CancellationError from a flaky connection crashed the Coinbase transfer and CrowdNode flows.
  • fatalError on decoded Coinbase amounts, exchange rates, account icon URLs and Uphold amounts (all server- or user-supplied), and on an unbound SDK wallet in CBAccount.send / UpholdAmountModel — reachable during network and wallet switches.
  • Gift-card amounts went through UInt64(Double), which traps on negative or non-finite input.

Home transaction list (device A's crash)

  • txItems was read on the view model's worker queue while being written on main. Every access is now on main, and TransactionGroup became a struct so the array no longer runs ARC on shared class instances.
  • Group indexes captured off-main were applied a main-queue turn later without revalidation, trapping when a full reload replaced the array in between.

QR scanner teardown

  • DWCaptureSessionManager never tore down — across all six device logs "Preview has been stopped" appears 4×, "Tearing down…" 0×, because teardown was scheduled only when the session was already running. The capture session, its device input and three serial queues leaked for the process lifetime. Teardown is now unconditional, clears both output delegates, and runs on main so the session state is single-threaded; a nil capture device (simulator, or a device that will not vend the camera) is handled instead of raising NSInvalidArgumentException.

Other

  • NumberFormatter.inputString(from:and:) fell through assertionFailure to fatalError in Release when the currency symbol could not be located in the formatted string — locale-dependent, on the amount-keyboard hot path.
  • ExplorePointOfUseListViewController inserted rows from pager offsets rather than the data source, raising NSInternalInconsistencyException when a filter or search changed the model mid-request.
  • DatabaseConnection built its store URL with URL(string:) on a filesystem path and passed absoluteString to SQLite.
  • The three DispatchSemaphore bridges into Task { @MainActor } now refuse to run on the main thread rather than deadlocking.
  • Removed the CJTEST debug tags from shipping log lines (guardrail Fix bitcode support in bls-signatures-pod #6).

Dropped during rebase

The audit also found that [DWEnvironment sharedInstance] materialized DashSync and reconciled the whole legacy transaction set on main during didFinishLaunching — 1–4 s in the diagnostics (~15k DSAccount log lines per launch, 99.5 % of device B's log volume), against the launch watchdog budget. chore(dashsync): complete final unlink on the base branch removed that call entirely, so the fix is no longer needed and was dropped when rebasing onto it.

How Has This Been Tested?

Not built. The SwiftDashSDK package in ../platform is on feat/platform-wallet-dashconnect-ffi and fails with value of type 'EventHandlerCallbacks' has no member 'release_fn', so the app target was never reached. Needs a dashpay arm64-sim build against the v4.1-dev pin plus a testnet smoke of Home, Coinbase transfer, CrowdNode, the QR scanner and the amount keyboard before merge.

Verification so far is static: every changed call site was read against its callers, and the new enum cases (Coinbase.Error.unknownError, CrowdNode.Error.messageStatus, UpholdRequestTransferModelState.fail) were confirmed to exist. The rebase onto the post-unlink base was resolved by hand: SwiftDashSDKWalletWiper.deleteWalletsFromSDK was rewritten upstream to take [Network: Set<Data>], and the main-thread guard was reapplied to the new signature.

Breaking Changes

None. TransactionGroup changes from class to struct, but it is internal to the home list and no call site relied on reference semantics.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f8e70c81-55fb-4663-8455-bef795053f43

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

Static audit of DashWallet/Sources plus device diagnostics from two users
(testnet iOS 26.5.2 and mainnet iOS 27.0, both 9.0.0 build 13).

Nil / force-unwrap crashes on ordinary paths:
- `stopNetworkMonitoring()` force-unwrapped `reachabilityObserver`, which is
  only assigned by `startNetworkMonitoring()`. Six adopters pair start in
  `viewDidLoad` with stop in `deinit`, so a controller released before its
  view loaded crashed. Now guarded and nilled out.
- Untyped `catch` blocks force-cast to `Coinbase.Error` / `CrowdNode.Error`;
  any `URLError` / `CancellationError` from a flaky connection crashed the
  Coinbase transfer and CrowdNode flows. Now `as?` with a fallback case.
- `fatalError` on decoded Coinbase amounts, exchange rates, account icon URLs
  and Uphold amounts — all server- or user-supplied. They now return a
  placeholder or a typed error.
- `CBAccount.send` / `UpholdAmountModel` treated an unbound SDK wallet as
  unreachable; it is reachable during network and wallet switches.
- Gift-card amounts went through `UInt64(Double)`, which traps on negative or
  non-finite input. Now range-checked via `Decimal`.

Home transaction list:
- `txItems` was read on the view model's worker queue while being written on
  main. Every access is now on main, and `TransactionGroup` became a struct so
  the array no longer runs ARC on shared class instances.
- Group indexes captured off-main were applied a main-queue turn later without
  revalidation, trapping when a full reload replaced the array in between.

Launch and teardown:
- `[DWEnvironment sharedInstance]` materializes DashSync and reconciles the
  whole legacy transaction set on main — 1-4s in the diagnostics, against the
  launch watchdog budget. The DashSync-touching tail is deferred by one
  main-queue turn. Every chain consumer moved with it: deferring
  `DWEnvironment` alone let `DWPhoneWCSessionManager`'s background read win the
  race and hit DSChain's main-thread assertion.
- `DWCaptureSessionManager` never tore down: teardown was scheduled only when
  the session was already running, so the capture session, its device input
  and three serial queues leaked for the process lifetime. Teardown is now
  unconditional, clears both output delegates, and runs on main so the session
  state is single-threaded. A nil capture device (simulator, or a device that
  will not vend the camera) is handled instead of raising.

Other:
- `NumberFormatter.inputString(from:and:)` fell through `assertionFailure` to
  `fatalError` in Release when the currency symbol could not be located in the
  formatted string — locale-dependent, on the amount-keyboard hot path. It now
  returns nil, and the separator index is taken after the string it indexes is
  rebuilt.
- `ExplorePointOfUseListViewController` inserted rows from pager offsets rather
  than the data source, raising NSInternalInconsistencyException when a filter
  or search changed the model mid-request.
- `DatabaseConnection` built its store URL with `URL(string:)` on a filesystem
  path and passed `absoluteString` to SQLite; `migrateIfNeeded` now reports a
  failed open instead of unwrapping nil.
- The three `DispatchSemaphore` bridges into `Task { @mainactor }` refuse to
  run on the main thread rather than deadlocking.
- Removed the `CJTEST` debug tags from shipping log lines (guardrail #6).

Not verified by a build: the SwiftDashSDK package in ../platform is on a
feature branch and does not compile, so the app target was never reached.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@romchornyi
romchornyi merged commit b354267 into swift-sdk-integration Aug 3, 2026
1 of 3 checks passed
@romchornyi
romchornyi deleted the fix/crash-audit branch August 3, 2026 17:22
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