Skip to content

fix(platform-wallet): poll Sync Now FFI passes on big-stack threads to stop SIGBUS crash#4033

Merged
QuantumExplorer merged 2 commits into
v4.1-devfrom
claude/exciting-snyder-ea277b
Jul 7, 2026
Merged

fix(platform-wallet): poll Sync Now FFI passes on big-stack threads to stop SIGBUS crash#4033
QuantumExplorer merged 2 commits into
v4.1-devfrom
claude/exciting-snyder-ea277b

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Jul 7, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

TestFlight users report the app instantly crashes when tapping "Sync Now" under Shielded Sync Status on the latest build (splawik21, hilawe, JGCMiner77 in the iOS channel).

Reproduced on-simulator with a matching crash log: EXC_BAD_ACCESS / SIGBUS "Thread stack size exceeded" on a com.apple.root.user-initiated-qos.cooperative thread, faulting inside the shielded notes-sync future with platform_wallet_manager_shielded_sync_sync_nowRuntime::block_on at the base of the stack.

Root cause: the shielded (and platform-address / identity-token) sync_now FFI entry points poll their entire sync future via runtime().block_on(...) on the host's calling thread. Swift calls them from dispatch/concurrency threads with ~512 KB of stack, and the sync future's recursion (DAPI request stack, GroveDB proof verification) has grown past it — the exact failure mode already documented and fixed for DashPay sync in #3841 ("SIGBUS observed on-device 2026-06-12", fixed with block_on_worker). The background sync loop was unaffected because it polls on a dedicated 2 MB std::thread, which is why only the button crashed.

What was done?

  • platform_wallet_manager_shielded_sync_sync_now and ..._shielded_sync_wallet now use block_on_worker, which parks the calling thread and polls the pass on the runtime's 8 MB-stack workers — same fix dashpay_sync_sync_now already ships.
  • platform_wallet_manager_identity_sync_sync_now: same change (identical latent crash one tap away).
  • platform_wallet_manager_platform_address_sync_sync_now: its future trips rustc's implied-lifetime-bound limitation (Unexpected higher-ranked lifetime error in GAT usage rust-lang/rust#100013) against block_on_worker's Send + 'static bounds, so added a run_on_big_stack_thread escape hatch in runtime.rs that creates and polls the future entirely on a scoped 8 MB OS thread (no Send/'static proof needed).

No Swift-side changes; no behavior change beyond which thread polls the pass.

How Has This Been Tested?

  • cargo check -p platform-wallet-ffi --features shielded and --all-features --all-targets; clippy clean; cargo fmt.
  • Rebuilt the sim app (build_ios.sh --target sim) and verified on the simulator that previously crashed (wallets with 1,142 synced shielded notes): tapped shielded Sync Now 4×, DashPay Sync Now, and Platform Sync Now — all passes complete (shielded "Queries Since Launch: 5 syncs", last-sync stamps update), app stays alive, no new crash reports in DiagnosticReports. Before the fix the same tap on the same data aborted the app instantly.

Breaking Changes

None.

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 added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved reliability of wallet sync actions (identity, platform address, and shielded sync), especially for large or complex sync operations.
    • Reduced the chance of crashes or freezes by executing “sync now” work in a safer runtime context designed to avoid stack-related verification failures.
  • New Features
    • Added an internal “big-stack” execution helper to support sync tasks that can’t satisfy stricter runtime requirements.
  • Tests
    • Added coverage to validate the big-stack helper handles deep recursion and returns results correctly.

…o stop SIGBUS crash

The shielded, platform-address, and identity-token sync_now FFI entry
points polled their whole sync future via runtime().block_on on the
host's calling thread. iOS dispatch/concurrency threads have ~512 KB of
stack, and the notes-sync / proof-verification recursion now exceeds it,
so tapping "Sync Now" under Shielded Sync Status aborted the app with
SIGBUS "Thread stack size exceeded" (reported by TestFlight users on the
latest build; reproduced on-sim with a matching crash log).

Move the polling off the calling thread the same way dashpay_sync
already does: block_on_worker dispatches the pass onto the runtime's
8 MB-stack workers. The platform-address pass can't satisfy
block_on_worker's Send + 'static bounds (rustc issue #100013), so add a
run_on_big_stack_thread escape hatch that creates and polls the future
entirely on a scoped 8 MB OS thread.

Verified on-sim against wallets with 1,142 synced notes: repeated
Shielded/DashPay/Platform Sync Now taps complete without crashing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added this to the v4.1.0 milestone Jul 7, 2026
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ac4a8918-9a8d-44bd-8c5d-e0ccf9d0344f

📥 Commits

Reviewing files that changed from the base of the PR and between 0a5bafa and b97316f.

📒 Files selected for processing (2)
  • packages/rs-platform-wallet-ffi/src/platform_address_sync.rs
  • packages/rs-platform-wallet-ffi/src/runtime.rs

📝 Walkthrough

Walkthrough

This PR adds a run_on_big_stack_thread helper to the wallet FFI runtime module and updates three FFI sync entrypoints—identity_sync, platform_address_sync, and shielded_sync—to execute sync passes via block_on_worker or run_on_big_stack_thread instead of calling runtime().block_on(...) directly on the caller thread.

Changes

FFI sync execution context refactor

Layer / File(s) Summary
Add run_on_big_stack_thread helper
packages/rs-platform-wallet-ffi/src/runtime.rs
Adds run_on_big_stack_thread and tests for return-value handling and deep recursion on a big-stack thread.
Identity and shielded sync use block_on_worker
packages/rs-platform-wallet-ffi/src/identity_sync.rs, packages/rs-platform-wallet-ffi/src/shielded_sync.rs
Imports block_on_worker and routes the identity sync now, shielded sync now, and shielded wallet sync entrypoints through worker-thread execution using sync arcs.
Platform address sync uses big-stack thread
packages/rs-platform-wallet-ffi/src/platform_address_sync.rs
Imports run_on_big_stack_thread, runs platform address sync now on a big-stack thread, and returns an FFI error if thread spawning fails.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
    participant FFI as FFI Caller
    participant Entry as Sync Entrypoint
    participant Worker as block_on_worker
    participant BigStack as run_on_big_stack_thread
    participant Mgr as Sync Manager

    FFI->>Entry: sync_now() / sync_wallet()
    alt identity_sync / shielded_sync
        Entry->>Worker: block_on_worker(async move { mgr.sync_now().await })
        Worker->>Mgr: sync_now()/sync_wallet()
        Mgr-->>Worker: result
        Worker-->>Entry: result
    else platform_address_sync
        Entry->>BigStack: run_on_big_stack_thread(closure)
        BigStack->>Mgr: runtime().block_on(sync_now())
        Mgr-->>BigStack: result
        BigStack-->>Entry: result or spawn error
    end
    Entry-->>FFI: PlatformWalletFFIResult
Loading

Possibly related issues

Suggested labels: ready for final review

Suggested reviewers: shumkov, lklimek, llbartekll, ZocoLini

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: moving Sync Now FFI polling onto big-stack threads to fix the SIGBUS crash.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 claude/exciting-snyder-ea277b

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.

@thepastaclaw

thepastaclaw commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

✅ Review complete (commit b97316f)

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (2)
packages/rs-platform-wallet-ffi/src/runtime.rs (2)

76-86: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Panics in the helper will abort the process, undermining the crash-fix goal.

Both .expect("failed to spawn big-stack FFI thread") (spawn failure, e.g. OS resource exhaustion) and .expect("big-stack FFI thread panicked") (any panic inside f, including panics deep in sync/proof-verification logic) will unwind out of this pub(crate) helper into the calling extern "C" FFI entrypoint. Since Rust automatically converts an unwind that would escape an extern "C" function into a process abort, either failure mode crashes the app — the exact class of failure this PR is trying to eliminate for shielded/platform-address/identity sync.

This mirrors the existing block_on_worker convention (.expect("tokio worker panicked")), so it's consistent with established crate style, but it's worth considering whether sync failures should be surfaced as a graceful PlatformWalletFFIResult error instead of a hard abort, since the whole point of this refactor is avoiding crashes on the sync path.

💡 Possible direction (illustrative, not a full fix)
-pub(crate) fn run_on_big_stack_thread<T: Send>(f: impl FnOnce() -> T + Send) -> T {
-    std::thread::scope(|scope| {
-        std::thread::Builder::new()
-            .name("pw-ffi-bigstack".into())
-            .stack_size(WORKER_STACK_BYTES)
-            .spawn_scoped(scope, f)
-            .expect("failed to spawn big-stack FFI thread")
-            .join()
-            .expect("big-stack FFI thread panicked")
-    })
-}
+pub(crate) fn run_on_big_stack_thread<T: Send>(
+    f: impl FnOnce() -> T + Send,
+) -> std::thread::Result<T> {
+    std::thread::scope(|scope| {
+        std::thread::Builder::new()
+            .name("pw-ffi-bigstack".into())
+            .stack_size(WORKER_STACK_BYTES)
+            .spawn_scoped(scope, f)
+            .expect("failed to spawn big-stack FFI thread")
+            .join()
+    })
+}

Callers would then translate an Err into a PlatformWalletFFIResult error rather than letting it abort.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rs-platform-wallet-ffi/src/runtime.rs` around lines 76 - 86, The
`run_on_big_stack_thread` helper currently uses `.expect(...)` on both
`spawn_scoped` and `join`, which can turn thread creation failures or panics
inside `f` into a process abort when the error escapes an `extern "C"` FFI path.
Update `run_on_big_stack_thread` to stop hard-panicking here and instead
propagate failure back to the caller in a form the FFI entrypoints can convert
into `PlatformWalletFFIResult`, using the existing
`block_on_worker`/`PlatformWalletFFIResult` pattern as a reference. Keep the
big-stack thread setup in `runtime.rs`, but make the failure path return an
error rather than aborting.

63-87: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No unit test added for the new helper.

run_on_big_stack_thread has no accompanying test in this diff. It's a small, self-contained utility (spawn scoped thread, run closure, join) that would be easy to unit test (e.g., verifying the return value round-trips and that the stack size is sufficient for deep recursion), improving confidence in this crash-fix helper.

As per coding guidelines, "Place unit and integration tests alongside each package, and keep end-to-end tests in packages/platform-test-suite."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rs-platform-wallet-ffi/src/runtime.rs` around lines 63 - 87, Add a
unit test for run_on_big_stack_thread in the same package to cover the new
helper. The test should exercise the scoped thread path, verify the closure’s
return value is propagated correctly, and ideally include a deep-recursion or
large-stack case to confirm the 8 MB stack behavior. Keep the test alongside the
runtime module so the helper’s spawn/join behavior is validated directly.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/rs-platform-wallet-ffi/src/runtime.rs`:
- Around line 76-86: The `run_on_big_stack_thread` helper currently uses
`.expect(...)` on both `spawn_scoped` and `join`, which can turn thread creation
failures or panics inside `f` into a process abort when the error escapes an
`extern "C"` FFI path. Update `run_on_big_stack_thread` to stop hard-panicking
here and instead propagate failure back to the caller in a form the FFI
entrypoints can convert into `PlatformWalletFFIResult`, using the existing
`block_on_worker`/`PlatformWalletFFIResult` pattern as a reference. Keep the
big-stack thread setup in `runtime.rs`, but make the failure path return an
error rather than aborting.
- Around line 63-87: Add a unit test for run_on_big_stack_thread in the same
package to cover the new helper. The test should exercise the scoped thread
path, verify the closure’s return value is propagated correctly, and ideally
include a deep-recursion or large-stack case to confirm the 8 MB stack behavior.
Keep the test alongside the runtime module so the helper’s spawn/join behavior
is validated directly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d72dec3f-5514-4580-8a1c-7a19aa42d8af

📥 Commits

Reviewing files that changed from the base of the PR and between e9aae0d and 0a5bafa.

📒 Files selected for processing (4)
  • packages/rs-platform-wallet-ffi/src/identity_sync.rs
  • packages/rs-platform-wallet-ffi/src/platform_address_sync.rs
  • packages/rs-platform-wallet-ffi/src/runtime.rs
  • packages/rs-platform-wallet-ffi/src/shielded_sync.rs

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review

Source: reviewers = claude ffi-engineer opus (failed); claude general opus (failed); claude rust-quality opus (failed); claude security-auditor opus (failed); codex ffi-engineer gpt-5.5; codex general gpt-5.5; codex rust-quality gpt-5.5; codex security-auditor gpt-5.5; verifier = codex gpt-5.5; specialists = security-auditor, rust-quality, ffi-engineer; policy gate = review_policy.enforce_backport_prereq_policy.

The PR’s stack handoff is scoped to the FFI sync-now paths and the main design matches the stated goal of avoiding small Swift/dispatch caller stacks. I verified one in-scope error-handling issue in the newly added big-stack thread helper: per-call thread creation failure is currently turned into a panic across an extern "C" boundary instead of an FFI error.

🟡 1 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-platform-wallet-ffi/src/runtime.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/runtime.rs:81-84: Return an FFI error when the big-stack thread cannot be spawned
  `run_on_big_stack_thread` is newly used by `platform_wallet_manager_platform_address_sync_sync_now`, an exported `extern "C"` function that otherwise reports failures through `PlatformWalletFFIResult`. If `spawn_scoped` fails under OS resource pressure, this `.expect(...)` panics on the FFI caller path; unwinding across the non-`C-unwind` boundary aborts the Swift host process. Thread creation is the new fallible operation introduced by this PR, so convert that `io::Error` into the existing FFI error channel at the sync-now call site instead of panicking. The `join().expect(...)` also re-panics through the FFI boundary if the worker panics; if that behavior is intentional for internal invariants, keep it explicit, but the spawn failure should be handled as a recoverable FFI failure.

Comment thread packages/rs-platform-wallet-ffi/src/runtime.rs Outdated
…rror

Review follow-up: run_on_big_stack_thread now returns io::Result instead
of panicking on spawn failure, so the extern "C" address-sync entry point
reports OS resource pressure through PlatformWalletFFIResult rather than
aborting the host. A panic inside the closure still propagates (same
convention as block_on_worker). Adds unit tests for the return-value
round-trip and deep recursion past host-thread stack sizes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@QuantumExplorer

Copy link
Copy Markdown
Member Author

Note on the failing Rust wallet tests / Wallet tests (macOS) check: the 3 failing tests (wallet::asset_lock::build::tests::*, "No UTXOs available for selection") are pre-existing on v4.1-dev — they fail identically at the base commit e9aae0d with this PR's diff absent, and this PR only touches rs-platform-wallet-ffi, which platform-wallet does not depend on. Verified locally on both commits.

@QuantumExplorer

Copy link
Copy Markdown
Member Author

The pre-existing wallet-test failures are root-caused and fixed in #4034: rust-dashcore#836 (in the #4022 bump) intentionally restricts asset-lock funding to final inputs, and the shared test fixture still funded via a mempool tx. Once #4034 lands on v4.1-dev, re-running this PR's checks should go green.

@QuantumExplorer QuantumExplorer merged commit 32f3642 into v4.1-dev Jul 7, 2026
17 of 18 checks passed
@QuantumExplorer QuantumExplorer deleted the claude/exciting-snyder-ea277b branch July 7, 2026 06:10

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review

Source: reviewers = claude ffi-engineer opus (failed); claude general opus (failed); claude rust-quality opus (failed); claude security-auditor opus (failed); codex ffi-engineer gpt-5.5; codex general gpt-5.5; codex rust-quality gpt-5.5; codex security-auditor gpt-5.5; verifier = codex gpt-5.5; specialists = security-auditor, rust-quality, ffi-engineer; policy gate = review_policy.enforce_backport_prereq_policy.

Prior reconciliation: prior-1 is FIXED; current head returns std::io::Result<T> from run_on_big_stack_thread and maps thread-spawn failure to PlatformWalletFFIResult::err in platform_wallet_manager_platform_address_sync_sync_now. Carried-forward prior findings: none. New findings in latest delta: none; the cumulative PR remains scoped to moving FFI sync-now polling onto larger-stack execution paths.

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