Matrix crates to 0.19: drops a double free and a to-device panic (#249)

matrix-sdk-crypto, -common and -sqlite 0.18 -> 0.19.1, with the ruma
versions they pin (common 0.20, client-api 0.25, events 0.35). Clears:
- RUSTSEC-2026-0292 (imbl-sized-chunks 0.1.3, double free / use after
  free on a panicking Drop) - now 0.2.0 through imbl 7
- RUSTSEC-2025-0167 / RUSTSEC-2026-0247 (bitmaps 3.2.1) - no longer in
  the tree
- RUSTSEC-2026-0318 (matrix-sdk-crypto 0.18.0, custom to-device sends
  can panic - the feature Element Call's keys use)

matrix-sdk-sqlite 0.19 needs rusqlite 0.40, and only one bundled SQLite
can link, so rusqlite moves 0.37 -> 0.40 and arti 0.44 -> 0.47 (the
first tor-dirmgr that accepts it). No code changes for either.

Code changes: OlmMachine::with_store is gone, OlmMachineBuilder replaces
it; vodozemac 0.11's CheckCode::to_digit takes a DigitMode, and the
ECIES (MSC4108) QR sign-in keeps the rendering it had, AllowLeadingZero.

arti 0.47 uses saturating-time 0.5.0, which still spins forever on
Windows. Re-vendored at 0.5.0 with the same fix.

Checked: OSV finds none of the four left and nothing new. A copy of a
real crypto store opens under 0.19 with the same identity keys and all
32 room keys, and 0.18 can still open it after 0.19's schema migration
(17 -> 19), so a rollback does not strand it. Tor bootstraps and reaches
an onion over TLS on Linux and, cross-built, on the Windows VM.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-03 02:06:35 -04:00
co-authored by Claude Opus 5.5
parent e78ae8dc3e
commit c34b7518c8
9 changed files with 960 additions and 515 deletions
Generated
+792 -475
View File
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -17,7 +17,7 @@ socket2 = "0.6"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "sync", "time", "io-util", "fs", "signal"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rusqlite = { version = "0.37", features = ["bundled"] }
rusqlite = { version = "0.40", features = ["bundled"] }
toml = "0.8"
fs2 = "0.4"
tracing = "0.1"
@@ -57,8 +57,8 @@ image = { version = "0.25", default-features = false, features = ["png", "gif"]
chrono = { version = "0.4", default-features = false, features = ["std"] }
# Sneedchat backend (SneedChat over embedded Tor)
arti-client = { version = "0.44", default-features = false, features = ["tokio", "rustls", "onion-service-client", "compression"] }
tor-rtcompat = { version = "0.44", features = ["tokio", "rustls"] }
arti-client = { version = "0.47", default-features = false, features = ["tokio", "rustls", "onion-service-client", "compression"] }
tor-rtcompat = { version = "0.47", features = ["tokio", "rustls"] }
tokio-socks = "0.5"
hyper = { version = "1", features = ["client", "http1"] }
hyper-util = { version = "0.1", features = ["tokio", "client-legacy", "http1"] }
@@ -80,22 +80,22 @@ tokio-rustls = { version = "0.26", default-features = false, features = ["ring",
# Element distributes them - see backend/matrix/crypto.rs.
# `qrcode` is what lets a verification be done by scanning rather than by
# reading emoji aloud - the method Element offers first and most people use.
matrix-sdk-crypto = { version = "0.18", features = ["experimental-send-custom-to-device", "qrcode"] }
# Pinned to exactly what matrix-sdk-crypto 0.18 resolves to, for the same
matrix-sdk-crypto = { version = "=0.19.1", features = ["experimental-send-custom-to-device", "qrcode"] }
# Pinned to exactly what matrix-sdk-crypto 0.19 resolves to, for the same
# reason the ruma crates below are: this is where its own response types live
# - the verification state a decrypted message carries among them - and a
# version skew here is a type mismatch rather than a missing feature.
matrix-sdk-common = "=0.18.0"
matrix-sdk-sqlite = { version = "0.18", default-features = false, features = ["crypto-store", "bundled"] }
# Pinned exactly to whatever matrix-sdk-crypto 0.18.0 itself resolves to
matrix-sdk-common = "=0.19.1"
matrix-sdk-sqlite = { version = "=0.19.1", default-features = false, features = ["crypto-store", "bundled"] }
# Pinned exactly to whatever matrix-sdk-crypto 0.19.1 itself resolves to
# (confirmed via `cargo tree -p matrix-sdk-crypto`) - OlmMachine's public
# API takes ruma-common types (UserId/RoomId/DeviceId) and wraps
# ruma-client-api request structs directly, and Cargo will happily
# resolve two incompatible ruma-common versions side by side without
# erroring until the specific call sites that mix them - i.e. everywhere.
ruma-common = "=0.19.0"
ruma-client-api = "=0.24.0"
ruma-events = "=0.34.0"
ruma-common = "=0.20.0"
ruma-client-api = "=0.25.0"
ruma-events = "=0.35.0"
js_int = "=0.2.2"
# Discord voice is this crate's own connection (backend/discord/voiceconn.rs):
# cpal for the sound devices, opus2 for the codec. Songbird carried calls until
@@ -117,7 +117,7 @@ libc = "0.2"
# Tor's certificate validation hangs on Windows without this.
#
# saturating-time 0.4.0 finds a type's limit by halving a step until the
# saturating-time 0.5.0 (as 0.4.0 did) finds a type's limit by halving a step until the
# arithmetic fails, and only shrinks that step when it sees None. On Windows a
# SystemTime is a FILETIME with 100ns granularity, so subtracting 1ns returns
# Some(unchanged) rather than None - the None branch becomes unreachable at
+4 -4
View File
@@ -36,7 +36,7 @@ use js_int::UInt;
use matrix_sdk_crypto::olm::EncryptionSettings;
use matrix_sdk_crypto::types::requests::AnyOutgoingRequest;
use matrix_sdk_crypto::{
CollectStrategy, DecryptionSettings, EncryptionSyncChanges, OlmMachine, TrustRequirement,
CollectStrategy, DecryptionSettings, EncryptionSyncChanges, OlmMachine, OlmMachineBuilder, TrustRequirement,
};
use matrix_sdk_sqlite::SqliteCryptoStore;
// For `event_type()` on a verification step's content - the wire needs the
@@ -128,7 +128,7 @@ impl CryptoSession {
let dir = data_dir.join("matrix-crypto").join(sanitize_account_id(account_id));
tokio::fs::create_dir_all(&dir).await.context("creating matrix crypto store directory")?;
let store = SqliteCryptoStore::open(&dir, None).await.context("opening matrix crypto store")?;
let machine = match OlmMachine::with_store(user_id, device_id, store, None).await {
let machine = match OlmMachineBuilder::new(user_id, device_id).with_crypto_store(store).build().await {
Ok(machine) => machine,
// A store belonging to a different device, which happens whenever
// somebody signs in again: the homeserver mints a new device, and
@@ -149,7 +149,7 @@ impl CryptoSession {
forget(data_dir, account_id);
tokio::fs::create_dir_all(&dir).await.context("recreating matrix crypto store directory")?;
let store = SqliteCryptoStore::open(&dir, None).await.context("reopening matrix crypto store")?;
OlmMachine::with_store(user_id, device_id, store, None)
OlmMachineBuilder::new(user_id, device_id).with_crypto_store(store).build()
.await
.context("initializing OlmMachine on a fresh store")?
}
@@ -184,7 +184,7 @@ impl CryptoSession {
let dir = data_dir.join("matrix-crypto").join(sanitize_account_id(account_id));
tokio::fs::create_dir_all(&dir).await.context("creating matrix crypto store directory")?;
let store = SqliteCryptoStore::open(&dir, None).await.context("opening matrix crypto store")?;
let machine = OlmMachine::with_store(user_id, device_id, store, Some(account))
let machine = OlmMachineBuilder::new(user_id, device_id).with_crypto_store(store).with_custom_account(Some(account)).build()
.await
.context("initializing OlmMachine with the QR sign-in's account")?;
if let Some(secrets) = secrets {
+6 -2
View File
@@ -33,6 +33,7 @@ use anyhow::{bail, Context, Result};
use matrix_sdk_crypto::types::qr_login::{Msc4108IntentData, QrCodeData};
use matrix_sdk_crypto::types::SecretsBundle;
use matrix_sdk_crypto::vodozemac::ecies::{Ecies, EstablishedEcies, InitialMessage, Message};
use matrix_sdk_crypto::vodozemac::hpke::DigitMode;
use serde_json::{json, Value};
use super::*;
@@ -139,7 +140,10 @@ async fn try_qr_login(state: &AppState, login_id: &str, homeserver_url: &str) ->
.await
.context("nobody entered the code in time")?
.context("the sign-in was cancelled")?;
if typed != channel.ecies.check_code().to_digit() {
// The original MSC4108 rendering, which is the one this ECIES channel
// speaks: a leading zero allowed. (MSC4388's HPKE channel forbids one;
// vodozemac now asks which is meant.)
if typed != channel.ecies.check_code().to_digit(DigitMode::AllowLeadingZero) {
let _ = channel.send_json(failure("user_cancelled")).await;
bail!("those digits do not match the ones on your phone - start again, and check you scanned this screen");
}
@@ -496,7 +500,7 @@ mod tests {
let phone = Ecies::new().establish_outbound_channel(moho_key, LOGIN_INITIATE.as_bytes()).unwrap();
let inbound = moho.establish_inbound_channel(&InitialMessage::decode(&phone.message.encode()).unwrap()).unwrap();
assert_eq!(inbound.message, LOGIN_INITIATE.as_bytes());
assert_eq!(inbound.ecies.check_code().to_digit(), phone.ecies.check_code().to_digit());
assert_eq!(inbound.ecies.check_code().to_digit(DigitMode::AllowLeadingZero), phone.ecies.check_code().to_digit(DigitMode::AllowLeadingZero));
let (mut moho, mut phone) = (inbound.ecies, phone.ecies);
let sealed = moho.encrypt(LOGIN_OK.as_bytes()).encode();
+1
View File
@@ -0,0 +1 @@
/target
+48 -8
View File
@@ -1,14 +1,54 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
name = "saturating-time"
version = "0.4.0"
authors = ["Clara Engler"]
edition = "2024"
rust-version = "1.85"
rust-version = "1.92"
name = "saturating-time"
version = "0.5.0"
authors = [
"The Tor Project, Inc.",
"Clara Engler <[email protected]>",
]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "A trait for limits and saturations on types inside std::time"
repository = "https://codeberg.org/cve/saturating-time"
readme = "README.md"
keywords = [
"time",
"system-time",
"instant",
"saturating",
]
categories = [
"date-and-time",
"os",
"rust-patterns",
]
license = "MIT OR Apache-2.0"
keywords = ["time", "system-time", "instant", "saturating"]
categories = ["date-and-time", "os", "rust-patterns"]
repository = "https://gitlab.torproject.org/tpo/core/arti.git"
[package.metadata.docs.rs]
all-features = true
[features]
nightly = []
full = ["web-time-compat/full"]
[lib]
name = "saturating_time"
path = "src/lib.rs"
[dependencies.web-time-compat]
version = "0.3.0"
+14 -9
View File
@@ -32,16 +32,13 @@ However, this crate implements this trait for two structures:
## Example
Add the following to your `Cargo.toml`:
```toml
[dependencies]
saturating-time = "0.4.0"
```
Add `saturating-time` to your `Cargo.toml`.
If you use Rust nightly, you may want to do:
If you use Rust nightly, you may want to additionally configure the following
in your `.cargo/config.toml`:
```toml
[dependencies]
saturating-time = { version = "0.4.0", features = ["nightly"] }
[build]
rustflags = ["--cfg", "saturating_time_nightly"]
```
Now, you can use `saturating-time` in your code:
@@ -95,7 +92,15 @@ None yet.
[^5]: <https://github.com/rust-lang/rust/pull/151200>
[^6]: <https://github.com/rust-lang/rust/issues/151199>
## History
Up until (and including) version 0.4.0, `saturating-time` was maintained as an
independent project hosted on [Codeberg](https://codeberg.org/cve/saturating-time).
Starting with version 0.5.0, it has been incorporated into the
[Arti](https://arti.torproject.org) and is therefore maintained by
*The Tor Project, Inc.*
## License and Copyright
This crate is licensed under `MIT OR Apache-2.0`.
See the respective `LICENSE-*` files in the repository for more information.
+18 -2
View File
@@ -73,7 +73,8 @@ impl SaturatingTime for SystemTime {
impl SaturatingTime for Instant {
fn anchor() -> Self {
Self::now()
use web_time_compat::InstantExt;
Self::get()
}
fn max_value() -> Self {
@@ -143,7 +144,7 @@ where
match next {
Some(st) if st != res => {
// (2.1) If Some, set res to the returned value and continue.
res = st
res = st;
}
// Succeeded without moving. Windows makes this reachable and it
// is what hung Tor's certificate validation there: a SystemTime
@@ -178,6 +179,21 @@ where
#[cfg(test)]
mod tests {
// @@ begin test lint list maintained by maint/add_warning @@
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::string_slice)] // See arti#2571
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
use std::{
fmt::Debug,
ops::{Add, Sub},
+65 -3
View File
@@ -1,7 +1,54 @@
#![doc = include_str!("../README.md")]
// @@ begin lint list maintained by maint/add_warning @@
#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable)
#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly)
#![warn(missing_docs)]
#![warn(noop_method_call)]
#![warn(unreachable_pub)]
#![warn(clippy::all)]
#![deny(clippy::await_holding_lock)]
#![deny(clippy::cargo_common_metadata)]
#![deny(clippy::cast_lossless)]
#![deny(clippy::checked_conversions)]
#![allow(clippy::cognitive_complexity)] // See arti#2556
#![deny(clippy::debug_assert_with_mut_call)]
#![deny(clippy::exhaustive_enums)]
#![deny(clippy::exhaustive_structs)]
#![deny(clippy::expl_impl_clone_on_copy)]
#![deny(clippy::fallible_impl_from)]
#![deny(clippy::implicit_clone)]
#![deny(clippy::large_stack_arrays)]
#![warn(clippy::manual_ok_or)]
#![deny(clippy::missing_docs_in_private_items)]
#![warn(clippy::needless_borrow)]
#![warn(clippy::needless_pass_by_value)]
#![warn(clippy::option_option)]
#![deny(clippy::print_stderr)]
#![deny(clippy::print_stdout)]
#![warn(clippy::rc_buffer)]
#![deny(clippy::ref_option_ref)]
#![warn(clippy::semicolon_if_nothing_returned)]
#![warn(clippy::trait_duplication_in_bounds)]
#![deny(clippy::unchecked_time_subtraction)]
#![deny(clippy::unnecessary_wraps)]
#![warn(clippy::unseparated_literal_suffix)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::mod_module_files)]
#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
#![allow(clippy::uninlined_format_args)]
#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
#![allow(clippy::result_large_err)] // temporary workaround for arti#587
#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
#![allow(clippy::needless_lifetimes)] // See arti#1765
#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
#![allow(clippy::collapsible_if)] // See arti#2342
#![deny(clippy::unused_async)]
#![deny(clippy::string_slice)] // See arti#2571
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
#![allow(unexpected_cfgs)]
#![forbid(unsafe_code)]
#![cfg_attr(
feature = "nightly",
saturating_time_nightly,
feature(time_systemtime_limits, time_saturating_systemtime)
)]
@@ -157,7 +204,7 @@ pub trait SaturatingTime: internal::SaturatingTime {
}
// Use nightly implementation if compiled with the nightly feature.
#[cfg(feature = "nightly")]
#[cfg(saturating_time_nightly)]
impl SaturatingTime for SystemTime {
fn max_value() -> Self {
Self::MAX
@@ -181,7 +228,7 @@ impl SaturatingTime for SystemTime {
}
// Otherwise, use the default one.
#[cfg(not(feature = "nightly"))]
#[cfg(not(saturating_time_nightly))]
impl SaturatingTime for SystemTime {}
impl SaturatingTime for Instant {
@@ -193,6 +240,21 @@ impl SaturatingTime for Instant {
#[cfg(test)]
mod tests {
// @@ begin test lint list maintained by maint/add_warning @@
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::string_slice)] // See arti#2571
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
use super::*;
use crate::internal;
use std::{