From fa00dd13912abf3676bea004ec08a1f178a01e83 Mon Sep 17 00:00:00 2001 From: akrm Date: Fri, 24 Apr 2026 13:03:25 -0400 Subject: [PATCH 1/2] fix(dist): propagate v2 manifest checksum failure instead of reporting "unchanged" When the v2 channel manifest's `.toml` did not match its published `.sha256`, `try_update_from_dist_` mapped `RustupError::ChecksumFailed` to `Ok(None)`. Callers interpret `Ok(None)` as "no update available", so `rustup update` printed only a `warn:` line, reported the toolchain as `unchanged`, and exited 0, even though the manifest could not be integrity-checked. --- src/dist/mod.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/dist/mod.rs b/src/dist/mod.rs index 185f903a9b..53299af612 100644 --- a/src/dist/mod.rs +++ b/src/dist/mod.rs @@ -1234,11 +1234,15 @@ async fn try_update_from_dist_( Ok(None) => return Ok(None), Err(err) => { match err.downcast_ref::() { - Some(RustupError::ChecksumFailed { .. }) => return Ok(None), Some(RustupError::DownloadNotExists { .. }) => { // Proceed to try v1 as a fallback debug!("manifest not found; trying legacy manifest"); } + // Includes `ChecksumFailed`: if the v2 manifest exists but its + // contents do not match the published `.sha256`, surface the + // integrity failure as an error rather than silently treating + // the toolchain as up to date. The v1 fallback path below + // already does the same. _ => return Err(err), } } From c4e5121bd07e8f59829485d912a8bf0b626f15c0 Mon Sep 17 00:00:00 2001 From: Akrm Al-Hakimi Date: Fri, 24 Apr 2026 14:36:35 -0400 Subject: [PATCH 2/2] test(dist): fail v2 manifest update when manifest disagrees with .sha256 --- src/dist/manifestation/tests.rs | 68 +++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/dist/manifestation/tests.rs b/src/dist/manifestation/tests.rs index 29e87b7cbf..dca22ff557 100644 --- a/src/dist/manifestation/tests.rs +++ b/src/dist/manifestation/tests.rs @@ -5,6 +5,7 @@ use std::{ collections::HashMap, env, fs, + io::Write, path::{Path, PathBuf}, str::FromStr, sync::Arc, @@ -14,6 +15,7 @@ use anyhow::{Result, anyhow}; use url::Url; use crate::{ + config::Cfg, dist::{ DEFAULT_DIST_SERVER, Profile, TargetTriple, ToolchainDesc, download::{DownloadCfg, DownloadTracker}, @@ -26,8 +28,10 @@ use crate::{ errors::RustupError, process::TestProcess, test::{ + Env, RustupHome, dist::*, mock::{MockComponentBuilder, MockFile, MockInstallerBuilder}, + test_dir, }, utils::{self, raw as utils_raw}, }; @@ -1529,3 +1533,67 @@ async fn handle_corrupt_partial_downloads() { assert!(utils::path_exists(cx.prefix.path().join("bin/rustc"))); assert!(utils::path_exists(cx.prefix.path().join("lib/libstd.rlib"))); } + +/// If the v2 channel manifest bytes do not match the published `.sha256`, the failure must be +/// reported as an error (not treated as "no update" / `Ok(None)`). +#[tokio::test] +async fn v2_manifest_checksum_mismatch_surfaces_error() { + let cx = TestContext::new(None, GZOnly); + let mock_root = cx.url.to_file_path().unwrap(); + let manifest_path = mock_root.join("dist/channel-rust-nightly.toml"); + fs::OpenOptions::new() + .append(true) + .open(&manifest_path) + .unwrap() + .write_all(b"\n# test: corrupt manifest body vs .sha256\n") + .unwrap(); + + let root = test_dir().unwrap(); + let rustup_home = RustupHome::new_in(root.path()).unwrap(); + let cargo_home = tempfile::Builder::new() + .prefix("cargo") + .tempdir_in(root.path()) + .unwrap(); + let home = tempfile::Builder::new() + .prefix("home") + .tempdir_in(root.path()) + .unwrap(); + + let mut vars = HashMap::new(); + rustup_home.apply(&mut vars); + vars.env( + "CARGO_HOME", + cargo_home.path().to_string_lossy().to_string(), + ); + vars.env("HOME", home.path().to_string_lossy().to_string()); + vars.env("RUSTUP_DIST_SERVER", cx.url.as_str()); + + let tp = TestProcess::new(env::current_dir().unwrap(), &["rustup"], vars, ""); + let cfg = Cfg::from_env(tp.process.current_dir().unwrap(), false, &tp.process).unwrap(); + let dl_cfg = DownloadCfg::new(&cfg); + let update_hash = cfg.get_hash_file(&cx.toolchain, true).unwrap(); + let mut fetched = String::new(); + + let err = super::super::try_update_from_dist_( + &dl_cfg, + &update_hash, + &cx.toolchain, + Some(Profile::Default), + &cx.prefix, + false, + &[], + &[], + &mut fetched, + &cfg, + None, + ) + .await + .unwrap_err(); + + match err.downcast_ref::() { + Some(RustupError::ChecksumFailed { .. }) => {} + e => { + panic!("expected ChecksumFailed for corrupt v2 manifest, got {e:?} full error: {err:?}") + } + } +}