install_default 第二次调用失败
主线位置:握手主线开始之前。
ClientConfig::builder()这类入口去拿进程级默认 provider,没有就按 crate feature 推断一个装上。推断规则和OnceLock见 CryptoProvider;两个 provider 都开、推断失败时的 panic 见 ambiguous-provider。本页讲推断成功之后、或者别人先装了之后,你再调install_default会怎样。
flowchart TB
lib["别处先调 builder()"] --> inf["按 feature 装上默认值"]
inf --> cell["OnceLock 已有值"]
app["你的 install_default"] --> set{"OnceLock set"}
cell --> set
set -->|空| ok["Ok 生效"]
set -->|已有| err["Err 带回你的"]
err --> unwrap["unwrap 就 panic"]
err --> ignore["let _ 就静默失效"]
实验 crate:exp-rustls/pitb-ringonly,rustls 只开 ring(加 std、tls12、logging),所以 builder() 能从 feature 推断出 ring。src/main.rs 里的「调好的 provider」是 ring 的默认 provider 只留一个套件 TLS13_CHACHA20_POLY1305_SHA256,这样从协商结果就能看出它生不生效;library_init() 代表某个在初始化时调 ClientConfig::builder() 的库。tests/ 下是两组测试。M2 Max、macOS 27、rustc 1.99.0,release。
现象
测试里时好时坏。 同一个测试二进制里的两个测试,各自 ring::default_provider().install_default().unwrap():
running 2 tests
test first ... ok
test second ... FAILED
failures:
---- second stdout ----
thread 'second' (7727703) panicked at tests/install.rs:11:46:
called `Result::unwrap()` on an `Err` value: CryptoProvider { cipher_suites: [TLS13_AES_256_GCM_SHA384, TLS13_AES_128_GCM_SHA256, TLS13_CHACHA20_POLY1305_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256], kx_groups: [X25519, secp256r1, secp384r1], signature_verification_algorithms: WebPkiSupportedAlgorithms { all: [ .. ], mapping: [ECDSA_NISTP384_SHA384, ECDSA_NISTP256_SHA256, ED25519, RSA_PSS_SHA512, RSA_PSS_SHA384, RSA_PSS_SHA256, RSA_PKCS1_SHA512, RSA_PKCS1_SHA384, RSA_PKCS1_SHA256] }, secure_random: Ring, key_provider: Ring }
...
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
把编好的测试二进制直接跑 20 次:18 次 second 失败,2 次 first 失败,从没全过,也从没全挂。单独跑 second(加过滤参数)它就通过。哪个测试挂取决于线程谁先到。
调好的 provider 没生效。 main 里先 library_init(),再装那个只有 ChaCha20 的 provider,不 unwrap,只把结果打印出来,然后握手:
install_default: Err, suites in Err: ["TLS13_CHACHA20_POLY1305_SHA256"]
get_default suites: 9 entries
negotiated: TLS13_AES_256_GCM_SHA384
进程里生效的是 9 个套件的默认 ring,协商出来的是 AES-256-GCM。如果代码写的是 let _ = tuned().install_default();,这三行一行都不会出现,程序照常运行,只是配置没起作用。同样的顺序改用 .expect("install provider"):
thread 'main' (7726969) panicked at src/main.rs:76:33:
install provider: CryptoProvider { cipher_suites: [TLS13_CHACHA20_POLY1305_SHA256], kx_groups: [X25519, secp256r1, secp384r1], signature_verification_algorithms: WebPkiSupportedAlgorithms { all: [ .. ], mapping: [ECDSA_NISTP384_SHA384, ECDSA_NISTP256_SHA256, ED25519, RSA_PSS_SHA512, RSA_PSS_SHA384, RSA_PSS_SHA256, RSA_PKCS1_SHA512, RSA_PKCS1_SHA384, RSA_PKCS1_SHA256] }, secure_random: Ring, key_provider: Ring }
panic 消息里打印的 cipher_suites: [TLS13_CHACHA20_POLY1305_SHA256] 是你的 provider,不是进程里已经装好的那个。只看这一行,很容易以为「装进去的就是它」。
原因
install_default 在 std 下转到 static_default::install_default:
#[cfg(feature = "std")]
pub(crate) fn install_default(
default_provider: CryptoProvider,
) -> Result<(), Arc<CryptoProvider>> {
PROCESS_DEFAULT_PROVIDER.set(Arc::new(default_provider))
}
PROCESS_DEFAULT_PROVIDER 是 OnceLock<Arc<CryptoProvider>>。OnceLock::set 只在格子为空时成功,否则把参数原样放进 Err 还给调用方。所以 Err 里一定是你刚传的那个,它不告诉你格子里是谁;要看格子里是谁得调 get_default。
格子是怎么在你之前被填上的:get_default_or_install_from_crate_features 是 builder() 们共用的入口,发现格子为空、feature 里又恰好只有一个 provider,就推断出它并自己调一次 install_default。只开 ring 的程序里,任何一个先于你调了 ClientConfig::builder()、ServerConfig::builder() 或 WebPki*Verifier::builder() 的代码(一个库的初始化函数、一个 lazy_static、另一个测试),都会悄悄把默认 ring 装进去。
测试的情况更简单:cargo test 在同一个进程里用多个线程跑测试,OnceLock 是整个进程共享的,只有第一个到达的测试能装上。
install_default 的 Err 类型在 0.23.14 才统一2d3b7ab2(「default crypto provider improvements」,首次发布在 0.23.14)之前,install_default 有两个按 feature 切换的版本:std 下返回 Result<(), Arc<Self>>,no_std 下返回 Result<(), Box<Arc<Self>>>,因为 no_std 用的 once_cell::race::OnceBox 失败时还回来的是一个 Box。这个提交把两者收进 static_default 模块,提交说明的第二条是「fix rustls::crypto::CryptoProvider::install_default fn to return consistent result type with both std & no-std build config」;no_std 那一支从此多了一个 .map_err(|e| *e),把 Box 拆掉。当时 std 下用的还是 once_cell::sync::OnceCell,现在换成了标准库的 OnceLock,但「失败时把参数还给你」这一点从来没变过。
修法
一:在 main 的第一行装,早于任何可能调 builder() 的代码。 同一个程序,只把 install_default 挪到 library_init() 之前:
get_default suites: ["TLS13_CHACHA20_POLY1305_SHA256"]
negotiated: TLS13_CHACHA20_POLY1305_SHA256
二:装不上时,比较一下格子里是不是你要的。 「别人先装了同一个默认 provider」是无害的,「别人先装了别的」才是问题。check 模式:
if let Err(mine) = tuned().install_default() {
let current = CryptoProvider::get_default().unwrap();
if suites_of(current) != suites_of(&mine) {
eprintln!("a different CryptoProvider was installed first: {:?}", suites_of(current));
std::process::exit(1);
}
}
a different CryptoProvider was installed first: ["TLS13_AES_256_GCM_SHA384", "TLS13_AES_128_GCM_SHA256", "TLS13_CHACHA20_POLY1305_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"]
CryptoProvider 没有实现 PartialEq,这里比的是套件列表;按自己关心的字段比(套件、组、secure_random 的 Debug)。
三:测试里每个测试都装同一个,忽略 Err。 tests/install_fixed.rs 的辅助函数:
fn ensure_provider() {
// Err only means some test got there first; every test installs the same one.
let _ = ring::default_provider().install_default();
assert!(CryptoProvider::get_default().is_some());
}
同一个测试二进制直接跑 50 次,50 次都是 test result: ok. 2 passed。这里能忽略 Err,前提是所有测试装的是同一个 provider;要测不同的 provider,就别走默认值。
四:调过的 provider 不要放进全局。 只给某几个连接用的配置(限定套件、FIPS、自定义组),直接 builder_with_provider 传进去。它不读也不写那个格子,不存在先来后到的问题,见 CryptoProvider。
怎么检查
- 搜
install_default: 每一处都看后面接的是什么。.unwrap()/.expect()在测试里会时好时坏;let _ =要确认「装不上」时用的确实是同一个 provider。 - 启动时打印生效的 provider:
CryptoProvider::get_default()的套件、组,和你装的那个对一下。 - 找出谁先装的: 在
install_default之前打印CryptoProvider::get_default().is_some()。已经是true,说明前面有代码调过builder();用调试器在get_default_or_install_from_crate_features上打断点能看到是谁。
小结
install_default就是OnceLock::set:只有第一次成功,失败的Err里是你这次传的 provider,不是已经生效的那个。- 只开一个 provider 时,任何先跑的
builder()都会按 feature 装上默认值,之后你的install_default必然失败;用let _ =忽略时,调好的 provider 静默不生效。 cargo test多线程共享这个格子,unwrap()让测试随机失败;测试里统一装同一个 provider 并忽略Err。- 在
main第一行装,装不上就用get_default()比一下;只给部分连接用的 provider 走builder_with_provider。