Skip to content

kotlin-sdk: JNI panic-safety across the FFI boundary + Sdk teardown behavioral test (PR #3999 follow-ups) #4121

Description

@shumkov

Two follow-ups split out of the PR #3999 review so they don't block the Kotlin SDK, both verified against the code.

1. JNI panic-safety across the FFI boundary (from support.rs review)

rs-unified-sdk-jni's guard (support.rs) wraps each JNI body in catch_unwind and rethrows as a Java exception. But it only catches panics that unwind back to its own closure. The guarded bodies call rs-platform-wallet-ffi / rs-sdk-ffi exports that are extern "C" (non-unwinding; no C-unwind, no internal catch_unwind). Android builds panic="unwind", and block_on_worker (runtime.rs) does rt.spawn(future).await.expect("tokio worker panicked") — a spawned-future panic becomes a JoinError, and .expect re-panics on the calling thread inside the extern "C" frame. Since Rust 1.81 a panic leaving a non-unwinding extern "C" frame aborts the process — before guard's catch_unwind can observe it. So any unwrap/expect deep in an async FFI callee (proof verification, grovedb, serde) can terminate the Android app, breaking the "a JNI library must never abort the app process" contract in the android profile comment.

Scope: ~539 extern "C" exports across two crates that are also shared with the iOS SDK, which builds panic="abort" (where catch_unwind is inert). So the two mechanical fixes floated in review don't fit as-is: there are no non-extern internal impls to redirect JNI to, and blanket per-export catches would be dead code on iOS.

Proposed work:

  • Make the Result-returning block_on_worker sites catch_unwind the future and surface a caught panic as a PlatformWalletError (normal PlatformWalletFFIResult error) instead of .expect-aborting — this neutralizes the dominant abort path.
  • A uniform #[catch_panic]-style attribute (or impl/shim split) across the FFI exports, cfg-gated so it's inert under panic="abort" (iOS).
  • Either soften the android profile "never aborts" comment or point it at this issue until the guarantee actually holds.

2. Behavioral regression test for Sdk teardown / query-gate lifecycle (from Sdk.kt review)

The SDK teardown is fenced correctly, but GateCoverageLintTest only scans parameter names + checks for a gate-shaped source opener — it doesn't exercise the runtime semantics. Add a behavioral test that constructs an Sdk, holds an SDK lease, and drives both close paths to prove:

  • closeSuspending() waits for an active lease to drain,
  • operations started after close fail before reading the native handle,
  • cancellation can't strand cleanup,
  • AutoCloseable.close() follows the same fence.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions