Conversation
… an error
## Problem
`EncryptionError` and `DecryptionError` are the error types of `EncryptionProvider`
and `DecryptionProvider`, both `#[uniffi::export(with_foreign)]`. Foreign code
implements those traits -- `DataTrackCryptor` in the Android and Swift SDKs bridges
data track frames onto each platform's AES-GCM path -- so uniffi has to lift these
errors *into* Rust.
Both were `uniffi(flat_error)`. A flat error can be lowered but not lifted: uniffi
emits a `Lift` impl that exists only to satisfy trait bounds and panics if called.
fn try_read(buf: &mut &[u8]) -> Result<Self> { panic!("Can't lift flat errors") }
A panic in an FFI callback has nowhere to unwind to, so every failed decrypt aborted
the host process:
Fatal signal 6 (SIGABRT), code -1 (SI_QUEUE) in tid 31589 (Thread-24)
Abort message: 'Can't lift flat errors'
That reached anything that can fail a decrypt: a subscriber with no E2EE manager, a
key mismatch, or a single corrupt frame. Reproduced on Android against a real SFU;
iOS ships the same cryptor and the same exposure. Present since these types were
introduced in #1034 -- data tracks had simply never run a failed decrypt across the
boundary.
## Fix
Drop `flat_error` from both enums and carry the detail in the variant:
#[error("Decryption failed: {reason}")]
Failed { reason: String },
This is the shape `PacketDeliveryError` already uses for the same reason; it is the
only other error returned across a `with_foreign` trait. Both enums also gain its
`From<UnexpectedUniFFICallbackError>` catch-all, so a foreign provider throwing
something other than the declared type surfaces as an error rather than aborting.
`reason` is free-form host context (logged, not parsed), and it is the first time
the string a foreign cryptor builds reaches Rust at all: under `flat_error` that
message was write-only, since lowering synthesized it from `Display` and lifting
never happened.
The four construction sites in `livekit/src/room/e2ee/data_track.rs` were all
`map_err(|_| ..)`. Now that there is somewhere to put the cause, they propagate it.
**Breaking:** `EncryptionError::Failed` and `DecryptionError::Failed` are struct
variants. Rust callers construct `Failed { reason }`. Foreign callers still pass a
single string -- Kotlin's positional `Failed(msg)` is source-compatible, Swift's
`Failed(message:)` becomes `Failed(reason:)`.
## Test
`cargo test -p livekit-datatrack --features uniffi`: 109 passed.
Regenerated the Kotlin and Swift bindings from the built cdylib. The converter now
reads the field instead of a synthesized `Display` string, i.e. the real `Lift` impl
replaced the panicking stub:
1 -> DecryptionException.Failed(FfiConverterString.read(buf))
Built the Android AAR locally and re-ran the two e2e tests that previously aborted
(Pixel 6, real SFU): both pass, `logcat -b crash` clean. A failed decrypt now logs
and drops the frame, leaving the room connected and the track published.
Contributor
Changeset ✓This PR includes a changeset covering all affected packages:
|
ladvoc
approved these changes
Sep 14, 2026
ladvoc
left a comment
Contributor
There was a problem hiding this comment.
LGTM ✅, one optional suggestion. Please run cargo fmt to make the last CI check pass.
| #[error("Encryption failed")] | ||
| Failed, | ||
| #[error("Encryption failed: {reason}")] | ||
| Failed { reason: String }, |
Contributor
There was a problem hiding this comment.
suggestion (non-blocking): Consider defining an enum case for each error type (e.g., UnexpectedIvLength, KeyIndexOutOfRange, and Failed).
1egoman
reviewed
Sep 14, 2026
Comment on lines
50
to
+56
| /// An error indicating a payload could not be decrypted. | ||
| #[derive(Debug, Error)] | ||
| #[cfg_attr(feature = "uniffi", derive(uniffi::Error))] | ||
| #[cfg_attr(feature = "uniffi", uniffi(flat_error))] | ||
| pub enum DecryptionError { | ||
| #[error("Decryption failed")] | ||
| Failed, | ||
| #[error("Decryption failed: {reason}")] | ||
| Failed { reason: String }, | ||
| } |
Contributor
There was a problem hiding this comment.
It might also be worth adding something to the AGENTS.md saying something to the effect of "never use flat_error in contexts where an error needs to be lifted from client sdk -> rust"?
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.
Problem
EncryptionErrorandDecryptionErrorare the error types ofEncryptionProviderandDecryptionProvider, both#[uniffi::export(with_foreign)]. Foreign code implements those traits --DataTrackCryptorin the Android and Swift SDKs bridges data track frames onto each platform's AES-GCM path -- so uniffi has to lift these errors into Rust.Both were
uniffi(flat_error). A flat error can be lowered but not lifted: uniffi emits aLiftimpl that exists only to satisfy trait bounds and panics if called.A panic in an FFI callback has nowhere to unwind to, so every failed decrypt aborted the host process:
That reached anything that can fail a decrypt: a subscriber with no E2EE manager, a key mismatch, or a single corrupt frame. Reproduced on Android against a real SFU; iOS ships the same cryptor and the same exposure. Present since these types were introduced in #1034 -- data tracks had simply never run a failed decrypt across the boundary.
Fix
Drop
flat_errorfrom both enums and carry the detail in the variant:This is the shape
PacketDeliveryErroralready uses for the same reason; it is the only other error returned across awith_foreigntrait. Both enums also gain itsFrom<UnexpectedUniFFICallbackError>catch-all, so a foreign provider throwing something other than the declared type surfaces as an error rather than aborting.reasonis free-form host context (logged, not parsed), and it is the first time the string a foreign cryptor builds reaches Rust at all: underflat_errorthat message was write-only, since lowering synthesized it fromDisplayand lifting never happened.The four construction sites in
livekit/src/room/e2ee/data_track.rswere allmap_err(|_| ..). Now that there is somewhere to put the cause, they propagate it.Breaking Changes
EncryptionError::FailedandDecryptionError::Failedare struct variants.Failed { reason }.Failed(msg)is source-compatibleFailed(message:)becomesFailed(reason:)Test
cargo test -p livekit-datatrack --features uniffi: 109 passed.Regenerated the Kotlin and Swift bindings from the built cdylib. The converter now reads the field instead of a synthesized
Displaystring, i.e. the realLiftimpl replaced the panicking stub:Built the Android AAR locally and re-ran the two e2e tests that previously aborted (Pixel 6, real SFU): both pass,
logcat -b crashclean. A failed decrypt now logs and drops the frame, leaving the room connected and the track published.