Skip to content

Commit 7196fe1

Browse files
authored
livekit-datatrack: stop aborting when a foreign E2EE provider errors (#1429)
## 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](https://github.com/livekit/client-sdk-swift/blob/caf6a7b4f8cd9955b8257882bae9eb118705a3bd/Sources/LiveKit/E2EE/DataTrackE2EE.swift#L55). 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 Changes * `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.
1 parent c944510 commit 7196fe1

4 files changed

Lines changed: 48 additions & 10 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
livekit-datatrack: minor
3+
livekit-uniffi: minor
4+
livekit: patch
5+
livekit-ffi: patch
6+
livekit-capture: patch
7+
---
8+
9+
`EncryptionError::Failed` and `DecryptionError::Failed` carry a `reason` string and are no longer `flat_error`,
10+
so a foreign `EncryptionProvider` or `DecryptionProvider` returning an error no longer aborts the process with
11+
"Can't lift flat errors" -- a failed data track decrypt (no E2EE manager, key mismatch, corrupt frame) now
12+
drops the frame and leaves the room connected.

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ Several crates export items to Swift/Kotlin/Node/Python through UniFFI — `live
8888
- Name the field `reason` in Rust instead — see `DataStreamError` in `livekit-uniffi/src/data_stream/common.rs`
8989
- A new crate that exports UniFFI items needs its own `uniffi.toml`, including `omit_checksums = true` under `[bindings.kotlin]`
9090
- The Kotlin checksum test is broken on ARM in every UniFFI release this workspace can use; the full explanation lives in `livekit-uniffi/uniffi.toml` and the root `Cargo.toml`
91+
- **Never use `uniffi(flat_error)` on an error that foreign code can return**
92+
- Flat errors lower (Rust → foreign) but cannot be lifted: the derived `Lift` exists only to satisfy trait bounds, and its `try_read`/`try_lift` are `panic!("Can't lift flat errors")`. In an FFI callback that panic has nowhere to unwind to, so the host process aborts — and nothing catches it earlier, since the build, bindings generation, and Kotlin compilation all pass
93+
- Applies to the error type of any `#[uniffi::export(with_foreign)]` trait or callback interface, since foreign code implements the method. `flat_error` is correct only for an error that travels Rust → foreign exclusively, e.g. `PublishError` in `livekit-datatrack/src/local/mod.rs`
94+
- Give a host-thrown error a single `Failed { reason: String }` variant plus `From<uniffi::UnexpectedUniFFICallbackError>`, so an undeclared exception surfaces as an error rather than aborting — see `PacketDeliveryError` in `livekit-uniffi/src/data_stream/common.rs`
9195

9296
## Feature combinations
9397

livekit-datatrack/src/e2ee.rs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,35 @@ pub struct EncryptedPayload {
3333
/// An error indicating a payload could not be encrypted.
3434
#[derive(Debug, Error)]
3535
#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
36-
#[cfg_attr(feature = "uniffi", uniffi(flat_error))]
3736
pub enum EncryptionError {
38-
#[error("Encryption failed")]
39-
Failed,
37+
#[error("Encryption failed: {reason}")]
38+
Failed { reason: String },
39+
}
40+
41+
// Required because foreign code implements `EncryptionProvider::encrypt`: an exception that is
42+
// NOT an `EncryptionError` surfaces through this catch-all rather than aborting.
43+
#[cfg(feature = "uniffi")]
44+
impl From<uniffi::UnexpectedUniFFICallbackError> for EncryptionError {
45+
fn from(error: uniffi::UnexpectedUniFFICallbackError) -> Self {
46+
Self::Failed { reason: error.reason }
47+
}
4048
}
4149

4250
/// An error indicating a payload could not be decrypted.
4351
#[derive(Debug, Error)]
4452
#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
45-
#[cfg_attr(feature = "uniffi", uniffi(flat_error))]
4653
pub enum DecryptionError {
47-
#[error("Decryption failed")]
48-
Failed,
54+
#[error("Decryption failed: {reason}")]
55+
Failed { reason: String },
56+
}
57+
58+
// Required because foreign code implements `DecryptionProvider::decrypt`: an exception that is
59+
// NOT a `DecryptionError` surfaces through this catch-all rather than aborting.
60+
#[cfg(feature = "uniffi")]
61+
impl From<uniffi::UnexpectedUniFFICallbackError> for DecryptionError {
62+
fn from(error: uniffi::UnexpectedUniFFICallbackError) -> Self {
63+
Self::Failed { reason: error.reason }
64+
}
4965
}
5066

5167
/// Provider for encrypting payloads for E2EE.

livekit/src/room/e2ee/data_track.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ impl dt::EncryptionProvider for DataTrackEncryptionProvider {
3737
let encrypted = self
3838
.manager
3939
.encrypt_data(payload.into(), &self.sender_identity, key_index)
40-
.map_err(|_| dt::EncryptionError::Failed)?;
40+
.map_err(|e| dt::EncryptionError::Failed { reason: e.to_string() })?;
4141

4242
debug_assert_eq!(
4343
encrypted.key_index as u32,
@@ -46,8 +46,12 @@ impl dt::EncryptionProvider for DataTrackEncryptionProvider {
4646
);
4747

4848
let payload = encrypted.data.into();
49-
let iv = encrypted.iv.try_into().map_err(|_| dt::EncryptionError::Failed)?;
50-
let key_index = encrypted.key_index.try_into().map_err(|_| dt::EncryptionError::Failed)?;
49+
let iv = encrypted.iv.try_into().map_err(|iv: Vec<u8>| dt::EncryptionError::Failed {
50+
reason: format!("unexpected IV length: {}", iv.len()),
51+
})?;
52+
let key_index = encrypted.key_index.try_into().map_err(|e| {
53+
dt::EncryptionError::Failed { reason: format!("key index out of range: {e}") }
54+
})?;
5155

5256
Ok(dt::EncryptedPayload { payload, iv, key_index })
5357
}
@@ -79,7 +83,9 @@ impl dt::DecryptionProvider for DataTrackDecryptionProvider {
7983
payload.key_index as u32,
8084
&sender_identity,
8185
)
82-
.ok_or_else(|| dt::DecryptionError::Failed)?;
86+
.ok_or_else(|| dt::DecryptionError::Failed {
87+
reason: "the E2EE manager could not decrypt the payload".to_owned(),
88+
})?;
8389
Ok(Bytes::from(decrypted))
8490
}
8591
}

0 commit comments

Comments
 (0)