diff --git a/cc-book/src/concepts/mls/transactions.md b/cc-book/src/concepts/mls/transactions.md index 5d166ab4d4f..34b4f63af80 100644 --- a/cc-book/src/concepts/mls/transactions.md +++ b/cc-book/src/concepts/mls/transactions.md @@ -145,3 +145,68 @@ val bytes = ctx.getData() These were implemented for the purpose of checkpointing during initial sync / batch decryption, but are not limited to that use. + +## Transaction Cancellation (Swift) + +The foreign-language wrappers can associate a `CoreCryptoCancellationToken` with a transaction, i.e., this is invisible +to users of the wrappers. However, currently **only the Swift wrapper** supports cancellable transactions. The +`CoreCryptoCancellationToken` is created through FFI but lives on the Rust side. Once the transaction owns the +transaction semaphore, Rust publishes the token in the `CoreCrypto` cancellation slot and, when a PKI environment +exists, in its separate slot. The returned guards keep the slots filled for exactly the lifetime of the transaction. + +The following shows a transaction with a cancellable foreign callback, such as `MlsTransport.sendCommitBundle()` or a +`PkiEnvironment` hook. A transaction that does not invoke such a callback skips the callback portion of the sequence. + +```mermaid +sequenceDiagram + autonumber + participant Foreign as Foreign language Wrapper + participant Tx as Rust transaction + participant Slots as Rust cancellation slots + participant Callback as Foreign callback + + Foreign->>Foreign: create cancellation token + Foreign->>Tx: start cancellable transaction + Tx->>Tx: race cancellation against semaphore acquisition + + alt Token is cancelled before acquisition + Foreign->>Tx: cancel token + Tx-->>Foreign: TransactionCanceled (slots remain empty) + else Transaction semaphore is acquired + Tx->>Slots: enter token in CoreCrypto slot + Slots-->>Tx: cancellation guard + opt PKI environment exists + Tx->>Slots: enter token in PKI slot + Slots-->>Tx: cancellation guard + end + Tx->>Foreign: execute command with context + Foreign->>Tx: context operation + Tx->>Slots: read active token + Slots-->>Tx: shared token + Tx->>Callback: invoke callback and race its result against cancellation + + alt Command finishes first + Callback-->>Tx: callback result + Tx-->>Foreign: context operation result + Foreign-->>Tx: command result + alt Command succeeded + Tx->>Tx: commit + else Command failed + Tx->>Tx: abort + end + else Token is cancelled while callback is running + Foreign->>Tx: cancel token + Tx-xCallback: drop callback future, causing UniFFI to cancel the foreign task + Tx->>Tx: drop command future and abort + end + + Tx->>Slots: drop guards and clear both slots + Tx->>Tx: drop context and release semaphore + Tx-->>Foreign: success, command error, or TransactionCanceled + end +``` + +The guards are deliberately created after the transaction context. Rust drops local values in reverse declaration order, +so both slots are cleared before dropping the context that releases the semaphore. Consequently, the next transaction +cannot observe the previous transaction's token. If cancellation wins a callback race, dropping the Rust future also +causes UniFFI's generated foreign-language binding to cancel the task executing that callback. diff --git a/crypto-ffi/src/cancellation/mod.rs b/crypto-ffi/src/cancellation/mod.rs index a17f88e7d19..b85eafa3212 100644 --- a/crypto-ffi/src/cancellation/mod.rs +++ b/crypto-ffi/src/cancellation/mod.rs @@ -1,3 +1,35 @@ +//! # CoreCryptoCancellationToken Implementation Details +//! +//! For a more high-level description and a sequence diagram, see the `Transaction` chapter of the CoreCrypto book. +//! +//! The [CoreCryptoCancellationToken] is supposed, but in theory not limited, to be used in +//! [crate::CoreCryptoFfi::transaction_ffi_cancellable]. This describes the implementation and intended usage which +//! matches the current usage in that function. +//! +//! ## How Cancellation Works +//! To make a future cancellable, we race the future against [CoreCryptoCancellationToken::cancelled] using +//! [futures_util::select_biased], with a bias for [CoreCryptoCancellationToken::cancelled] - in case both futures +//! complete at the same time, cancellation is preferred. If cancellation wins, we return an appropriate error. +//! +//! ## Sharing the Cancellation Token With Foreign Callbacks +//! Cancellation needs to cause rust futures currently waiting for foreign callbacks to stop waiting, i.e., be dropped. +//! This is because when they are dropped, the uniffi-generated swift code will cancel the task that is running the +//! foreign callback. To share an existing token with callbacks, [crate::CoreCryptoFfi] has a clonable +//! [CancellationSlot], with space for a single token. +//! To fill a slot, call [CancellationSlot::enter]. This will return a [slot::CancellationGuard] which empties the slot +//! when dropped. [CancellationSlot::enter] will panic if called while the slot is still filled. +//! [crate::CoreCryptoFfi::transaction_ffi_cancellable] ensures that this constraint isn't violated by only filling the +//! slot after the transaction semaphore was acquired. *This constraint makes the [CancellationSlot] currently only +//! usable in transactions*. +//! The slot instance is cloned to instances of this crate's structs implementing foreign traits, currently +//! [crate::MlsTransport] and [crate::PkiEnvironmentHooks]. Trait method implementations can then use the token from the +//! slot to race against its cancellation as described [above](#how-cancellation-works). +//! The [crate::MlsTransport] struct receives its [CancellationSlot] clone directly from [crate::CoreCryptoContext] +//! during [crate::CoreCryptoContext::mls_init]. +//! In case of [crate::PkiEnvironmentHooks] it is simpler to provide it with its own cancellation slot that is +//! explicitly filled with the same token during the transaction. That is because of the undetermined initialization +//! order (it may be initialized before or after [crate::CoreCryptoFfi]). + mod slot; mod token; diff --git a/crypto-ffi/src/core_crypto/command/mod.rs b/crypto-ffi/src/core_crypto/command/mod.rs index d64f9a321b3..9e02797de97 100644 --- a/crypto-ffi/src/core_crypto/command/mod.rs +++ b/crypto-ffi/src/core_crypto/command/mod.rs @@ -94,6 +94,8 @@ impl CoreCryptoFfi { /// /// Cancelling the token aborts the transaction and stops waiting for any /// in-flight `MlsTransport` callback or `PkiEnvironment` hook associated with it. + /// + /// For implementation details, see the [`cancellation` docs][crate::cancellation]. pub async fn transaction_ffi_cancellable( &self, command: Command,