From aa88b1976a3a87382f24e587ad0c95a7588adb70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=89=E6=AD=A3=E8=B6=85?= Date: Thu, 12 Mar 2026 13:52:49 +0800 Subject: [PATCH] fix(ecstore): avoid warm tier init panics (#2144) --- crates/ecstore/src/client/transition_api.rs | 71 ++++++++++++++++--- .../ecstore/src/tier/warm_backend_rustfs.rs | 42 +++++++++-- 2 files changed, 99 insertions(+), 14 deletions(-) diff --git a/crates/ecstore/src/client/transition_api.rs b/crates/ecstore/src/client/transition_api.rs index 2fb980e2..04dd8f1a 100644 --- a/crates/ecstore/src/client/transition_api.rs +++ b/crates/ecstore/src/client/transition_api.rs @@ -155,6 +155,47 @@ fn load_root_store_from_tls_path() -> Option { Some(store) } +fn panic_payload_to_message(payload: Box) -> String { + if let Some(message) = payload.downcast_ref::() { + return message.clone(); + } + + if let Some(message) = payload.downcast_ref::<&'static str>() { + return (*message).to_string(); + } + + "unknown panic payload".to_string() +} + +fn with_rustls_init_guard(build: F) -> Result +where + F: FnOnce() -> Result, +{ + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(build)) { + Ok(result) => result, + Err(payload) => { + let panic_message = panic_payload_to_message(payload); + Err(std::io::Error::other(format!( + "failed to initialize rustls crypto provider: {panic_message}. Ensure exactly one rustls crypto provider feature is enabled (aws-lc-rs or ring), or install one with CryptoProvider::install_default()" + ))) + } + } +} + +fn build_tls_config() -> Result { + with_rustls_init_guard(|| { + let config = if let Some(store) = load_root_store_from_tls_path() { + rustls::ClientConfig::builder() + .with_root_certificates(store) + .with_no_client_auth() + } else { + rustls::ClientConfig::builder().with_native_roots()?.with_no_client_auth() + }; + + Ok(config) + }) +} + impl TransitionClient { pub async fn new(endpoint: &str, opts: Options, tier_type: &str) -> Result { let clnt = Self::private_new(endpoint, opts, tier_type).await?; @@ -165,15 +206,8 @@ impl TransitionClient { async fn private_new(endpoint: &str, opts: Options, tier_type: &str) -> Result { let endpoint_url = get_endpoint_url(endpoint, opts.secure)?; - let scheme = endpoint_url.scheme(); let client; - let tls = if let Some(store) = load_root_store_from_tls_path() { - rustls::ClientConfig::builder() - .with_root_certificates(store) - .with_no_client_auth() - } else { - rustls::ClientConfig::builder().with_native_roots()?.with_no_client_auth() - }; + let tls = build_tls_config()?; let https = hyper_rustls::HttpsConnectorBuilder::new() .with_tls_config(tls) @@ -1278,3 +1312,24 @@ pub struct CreateBucketConfiguration { #[serde(rename = "LocationConstraint")] pub location_constraint: String, } + +#[cfg(test)] +mod tests { + use super::{build_tls_config, with_rustls_init_guard}; + + #[test] + fn rustls_guard_converts_panics_to_io_errors() { + let err = with_rustls_init_guard(|| -> Result<(), std::io::Error> { panic!("missing provider") }) + .expect_err("panic should be converted into an io::Error"); + assert!( + err.to_string().contains("missing provider"), + "expected panic message to be preserved, got: {err}" + ); + } + + #[test] + fn build_tls_config_returns_result_without_panicking() { + let outcome = std::panic::catch_unwind(build_tls_config); + assert!(outcome.is_ok(), "TLS config creation should not panic"); + } +} diff --git a/crates/ecstore/src/tier/warm_backend_rustfs.rs b/crates/ecstore/src/tier/warm_backend_rustfs.rs index e3214a9f..8e2e44a2 100644 --- a/crates/ecstore/src/tier/warm_backend_rustfs.rs +++ b/crates/ecstore/src/tier/warm_backend_rustfs.rs @@ -72,12 +72,10 @@ impl WarmBackendRustFS { }; let scheme = u.scheme(); let default_port = if scheme == "https" { 443 } else { 80 }; - let client = TransitionClient::new( - &format!("{}:{}", u.host_str().expect("err"), u.port().unwrap_or(default_port)), - opts, - "rustfs", - ) - .await?; + let host = u + .host_str() + .ok_or_else(|| std::io::Error::other("endpoint URL must include a host"))?; + let client = TransitionClient::new(&format!("{host}:{}", u.port().unwrap_or(default_port)), opts, "rustfs").await?; let client = Arc::new(client); let core = TransitionCore(Arc::clone(&client)); @@ -158,3 +156,35 @@ fn optimal_part_size(object_size: i64) -> Result { } Ok(part_size) } + +#[cfg(test)] +mod tests { + use futures::FutureExt; + use std::panic::AssertUnwindSafe; + + use super::*; + + fn rustfs_tier(endpoint: &str) -> TierRustFS { + TierRustFS { + endpoint: endpoint.to_string(), + access_key: "access".to_string(), + secret_key: "secret".to_string(), + bucket: "bucket".to_string(), + ..Default::default() + } + } + + #[tokio::test] + async fn new_returns_error_when_endpoint_has_no_host() { + let conf = rustfs_tier("rustfs://"); + + let outcome = AssertUnwindSafe(WarmBackendRustFS::new(&conf, "tier")).catch_unwind().await; + + let result = outcome.expect("initialization should return an error instead of panicking"); + let err = match result { + Ok(_) => panic!("endpoint without host must be rejected"), + Err(err) => err, + }; + assert!(err.to_string().contains("host"), "expected host validation error, got: {err}"); + } +}