Skip to main content

max / makenotwork

pom: remediate deepaudit 07-21 client-build panics + provider docs reqwest 0.13's builder is genuinely fallible (platform trust-store load), and Client::new()/unwrap_or_default() panic on that same failure. The five dead client-build fallbacks became reachable panics -- two inside spawned task loops (peer poll, route checks) where a panic silently stops monitoring: - checks/http.rs, checks/cors.rs: return an Unreachable/failed result on build error (also fixes the silently-discarded timeout minor). - peer.rs, cli/tasks/routes.rs: log at error and exit the spawned task visibly instead of panicking silently. - alerts/mod.rs: Alerter::new is now fallible; serve.rs disables alerting with an error log on build failure instead of crashing at startup. Also: correct the false main.rs provider comment (only aws_lc_rs is enabled; ring is an rcgen dev-dep) and document aws-lc-sys as a WATCHED PIN with the RUSTSEC-2026-0044/0048 advisory IDs and a re-check trigger. Not addressed (need your call): the trust-anchor split (reqwest platform verifier vs checks/tls.rs webpki-roots) and whether to keep aws-lc-rs on the cert path vs your rust_crypto preference (reqwest 0.13 pulls aws-lc-rs regardless). dashboard.rs unauth cookie stays chronic-by-decision. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-24 19:47 UTC
Commit: f886db89a8213e364a0da60a692295dd1b63fafd
Parent: 93e6e41
8 files changed, +104 insertions, -30 deletions
@@ -155,11 +155,17 @@ fn within_cooldown_secs(sent_at: &str, cooldown_secs: u64) -> bool {
155 155 }
156 156
157 157 impl Alerter {
158 - pub fn new(config: AlertConfig, pool: SqlitePool, instance_name: String) -> Self {
158 + pub fn new(
159 + config: AlertConfig,
160 + pool: SqlitePool,
161 + instance_name: String,
162 + ) -> Result<Self, reqwest::Error> {
163 + // reqwest 0.13's builder can fail (platform trust-store load) and
164 + // `Client::new()`/`unwrap_or_default()` panic on that same failure. Return
165 + // the error so the caller can disable alerting rather than crash at startup.
159 166 let client = reqwest::Client::builder()
160 167 .timeout(std::time::Duration::from_secs(10))
161 - .build()
162 - .unwrap_or_default();
168 + .build()?;
163 169 let wam_url = config.wam_url.clone();
164 170 if wam_url.is_none() {
165 171 warn!(
@@ -177,13 +183,13 @@ impl Alerter {
177 183 ),
178 184 _ => {}
179 185 }
180 - Self {
186 + Ok(Self {
181 187 config,
182 188 client,
183 189 pool,
184 190 instance_name,
185 191 wam_url,
186 - }
192 + })
187 193 }
188 194
189 195 /// Push one alert to MNW's operator log (`POST /api/internal/alerts`).
@@ -715,7 +721,7 @@ mod tests {
715 721 mnw_url: None,
716 722 alerts_ingest_token: None,
717 723 };
718 - Alerter::new(config, pool, "test-instance".to_string())
724 + Alerter::new(config, pool, "test-instance".to_string()).unwrap()
719 725 }
720 726
721 727 #[tokio::test]
@@ -9,18 +9,34 @@ use crate::types::CorsCheckResult;
9 9 /// Returns one `CorsCheckResult` per `CorsCheck` in the input.
10 10 #[instrument(skip_all)]
11 11 pub async fn check_cors(target: &str, checks: &[CorsCheck]) -> Vec<CorsCheckResult> {
12 - // Never `.unwrap()` the client build: a build failure would panic this
13 - // per-target CORS task and silently kill CORS monitoring for the target with
14 - // no alert that the check died (fuzz-2026-07-06 HIGH). Degrade gracefully to
15 - // a default client, mirroring the health probe.
16 - let client = reqwest::Client::builder()
12 + // Never `.unwrap()` (or fall back to `Client::new()`) on the client build: a
13 + // build failure would panic this per-target CORS task and silently kill CORS
14 + // monitoring for the target with no alert that the check died (fuzz-2026-07-06
15 + // HIGH). `Client::new()` panics on the same failure, so surface it as a failed
16 + // result per check instead.
17 + let client = match reqwest::Client::builder()
17 18 .timeout(std::time::Duration::from_secs(10))
18 19 .redirect(reqwest::redirect::Policy::none())
19 20 .build()
20 - .unwrap_or_else(|e| {
21 - tracing::warn!(error = %e, "cors: client build failed, using default client");
22 - reqwest::Client::new()
23 - });
21 + {
22 + Ok(c) => c,
23 + Err(e) => {
24 + tracing::warn!(error = %e, "cors: client build failed");
25 + let now = chrono::Utc::now().to_rfc3339();
26 + return checks
27 + .iter()
28 + .map(|check| CorsCheckResult {
29 + target: target.to_string(),
30 + url: check.url.clone(),
31 + origin: check.origin.clone(),
32 + method: check.method.clone(),
33 + passes: false,
34 + checked_at: now.clone(),
35 + error: Some(format!("client build: {e}")),
36 + })
37 + .collect();
38 + }
39 + };
24 40
25 41 let mut results = Vec::with_capacity(checks.len());
26 42 for check in checks {
@@ -17,13 +17,29 @@ pub async fn check_health(
17 17 config: &HealthConfig,
18 18 expect: Option<&HealthExpectation>,
19 19 ) -> HealthSnapshot {
20 - let client = reqwest::Client::builder()
20 + let checked_at = chrono::Utc::now().to_rfc3339();
21 + // reqwest 0.13's builder can genuinely fail (platform trust-store load), and
22 + // `Client::new()` panics on that same failure, so return an Unreachable result
23 + // instead of a fallback client that discards the timeout or panics.
24 + let client = match reqwest::Client::builder()
21 25 .timeout(std::time::Duration::from_secs(config.timeout_secs))
22 26 .build()
23 - .unwrap_or_else(|_| reqwest::Client::new());
27 + {
28 + Ok(c) => c,
29 + Err(e) => {
30 + return HealthSnapshot {
31 + id: None,
32 + target: target_name.to_string(),
33 + status: HealthStatus::Unreachable,
34 + checked_at,
35 + response_time_ms: 0,
36 + details: None,
37 + error: Some(format!("client build: {e}")),
38 + };
39 + }
40 + };
24 41
25 42 let start = Instant::now();
26 - let checked_at = chrono::Utc::now().to_rfc3339();
27 43
28 44 match client.get(&config.url).send().await {
29 45 Ok(response) => {
@@ -32,10 +32,19 @@ pub(crate) async fn cmd_serve(pool: &sqlx::SqlitePool, config: &Config) -> Resul
32 32 };
33 33
34 34 // Alerter
35 - let alerter = config.alerts.as_ref().map(|alert_config| {
36 - info!("Alerts enabled (to: {})", alert_config.to);
37 - Alerter::new(alert_config.clone(), pool.clone(), instance_name.clone())
38 - });
35 + let alerter = match config.alerts.as_ref() {
36 + Some(alert_config) => {
37 + info!("Alerts enabled (to: {})", alert_config.to);
38 + match Alerter::new(alert_config.clone(), pool.clone(), instance_name.clone()) {
39 + Ok(a) => Some(a),
40 + Err(e) => {
41 + tracing::error!("alerts: client build failed, alerting disabled: {e}");
42 + None
43 + }
44 + }
45 + }
46 + None => None,
47 + };
39 48
40 49 info!("Instance: {instance_name} (id={instance_id})");
41 50 info!("Starting serve mode (default interval: {default_interval}s, prune: {prune_days}d)");
@@ -48,10 +48,19 @@ pub(crate) fn spawn_route_tasks(
48 48 }
49 49
50 50 let mut ticks = CheckInterval::new(route_interval, cancel);
51 - let client = reqwest::Client::builder()
51 + // reqwest 0.13's builder can fail and `unwrap_or_default()` panics on
52 + // the same failure, silently killing this spawned route-check task. Log
53 + // and exit visibly instead.
54 + let client = match reqwest::Client::builder()
52 55 .redirect(reqwest::redirect::Policy::none())
53 56 .build()
54 - .unwrap_or_default();
57 + {
58 + Ok(c) => c,
59 + Err(e) => {
60 + tracing::error!("{name}: route-check client build failed: {e}");
61 + return;
62 + }
63 + };
55 64 let mut prev_failed: std::collections::HashSet<String> =
56 65 std::collections::HashSet::new();
57 66
M pom/src/main.rs +12 -3
@@ -85,9 +85,18 @@ enum Commands {
85 85
86 86 #[tokio::main]
87 87 async fn main() -> Result<()> {
88 - // Install the default rustls crypto provider before any TLS operations.
89 - // reqwest's rustls backend pulls aws-lc-rs, so standardize on it here; with more
90 - // than one provider reachable in the tree rustls can't auto-select one.
88 + // Install the default rustls crypto provider before any TLS operations. This
89 + // provider verifies signatures for every certificate pom validates (reqwest 0.13
90 + // reads the process default via CryptoProvider::get_default).
91 + //
92 + // reqwest 0.13's `rustls` feature pulls aws-lc-rs and is the only provider
93 + // feature enabled (`ring` enters only as an rcgen dev-dependency), so this
94 + // install_default() is belt-and-braces rather than load-bearing for
95 + // auto-selection. WATCHED PIN: aws-lc-sys 0.38.0 carries RUSTSEC-2026-0044
96 + // (Name Constraints bypass) and RUSTSEC-2026-0048 (CRL scope check), both X.509
97 + // validation defects on this path. The upgrade is upstream-blocked and reqwest
98 + // pulls aws-lc-rs regardless, so reverting the provider alone would leave two
99 + // providers reachable. Re-check when reqwest/rustls-webpki advance the pin.
91 100 let _ = tokio_rustls::rustls::crypto::aws_lc_rs::default_provider().install_default();
92 101
93 102 let cli = Cli::parse();
M pom/src/peer.rs +11 -2
@@ -236,10 +236,19 @@ async fn heartbeat_loop(
236 236 };
237 237 let base = peer_base_url(&address);
238 238
239 - let client = reqwest::Client::builder()
239 + let client = match reqwest::Client::builder()
240 240 .timeout(std::time::Duration::from_secs(10))
241 241 .build()
242 - .unwrap_or_default();
242 + {
243 + Ok(c) => c,
244 + Err(e) => {
245 + // reqwest 0.13's builder can fail and `unwrap_or_default()` panics on
246 + // the same failure, silently killing this spawned poll task. Log and
247 + // exit visibly instead.
248 + tracing::error!(peer = %peer_name, error = %e, "peer: client build failed, poll task exiting");
249 + return;
250 + }
251 + };
243 252
244 253 loop {
245 254 tokio::select! {
@@ -2141,7 +2141,7 @@ async fn alert_cooldown_key_matches_across_send_and_check() {
2141 2141 mnw_url: None,
2142 2142 alerts_ingest_token: None,
2143 2143 };
2144 - let alerter = pom::alerts::Alerter::new(config, pool.clone(), "test".to_string());
2144 + let alerter = pom::alerts::Alerter::new(config, pool.clone(), "test".to_string()).unwrap();
2145 2145
2146 2146 // Send a health alert for target "example.com"
2147 2147 alerter