From fc3cc110d2e214d36134cb6bc84cf38ef7dd7724 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:32:53 -0600 Subject: [PATCH 1/3] fix(shim): update mise-shim.exe during self-update on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the main binary update succeeds on Windows, download the same release zip and extract mise-shim.exe alongside the updated mise.exe. This is non-fatal — if the shim update fails, a warning is emitted but the overall self-update still succeeds. Fixes the issue where `mise self-update` only updated mise.exe but left mise-shim.exe at the old version, requiring manual download. Co-Authored-By: Claude Opus 4.6 --- src/cli/self_update.rs | 49 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 0a87367e0ad..cb176881afb 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -90,8 +90,13 @@ impl SelfUpdate { let status = self.do_update()?; if status.updated() { - let version = style(status.version()).bright().yellow(); - miseprintln!("Updated mise to {version}"); + let version = status.version().to_string(); + let styled_version = style(&version).bright().yellow(); + miseprintln!("Updated mise to {styled_version}"); + #[cfg(windows)] + if let Err(e) = Self::update_mise_shim(&version) { + warn!("Failed to update mise-shim.exe: {e}"); + } } else { miseprintln!("mise is already up to date"); } @@ -167,6 +172,46 @@ impl SelfUpdate { Ok(status) } + #[cfg(windows)] + fn update_mise_shim(version: &str) -> Result<()> { + use crate::http::HTTP; + use std::io::Read; + + let url = format!( + "https://github.com/jdx/mise/releases/download/{version}/mise-{version}-{}-{}.zip", + *OS, *ARCH, + ); + debug!("Downloading mise-shim.exe from {url}"); + + let temp_dir = tempfile::tempdir()?; + let zip_path = temp_dir.path().join("mise.zip"); + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(HTTP.download_file(&url, &zip_path, None)) + })?; + + let file = fs::File::open(&zip_path)?; + let mut archive = zip::ZipArchive::new(file)?; + + let mut shim_entry = match archive.by_name("mise/bin/mise-shim.exe") { + Ok(entry) => entry, + Err(_) => { + warn!("mise-shim.exe not found in release archive, skipping"); + return Ok(()); + } + }; + + let dest = env::MISE_BIN + .parent() + .expect("MISE_BIN should have a parent directory") + .join("mise-shim.exe"); + let mut buf = Vec::new(); + shim_entry.read_to_end(&mut buf)?; + xx::file::write(&dest, &buf)?; + + debug!("Updated mise-shim.exe at {}", dest.display()); + Ok(()) + } + pub fn is_available() -> bool { if let Some(b) = *env::MISE_SELF_UPDATE_AVAILABLE { return b; From 39fa8c55a5f06576b7408423ca42c99fa718c9a8 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:49:36 -0600 Subject: [PATCH 2/3] fix: address PR review feedback - Strip 'v' prefix from version before reconstructing URL with explicit 'v' prefix, ensuring correct download URL regardless of whether status.version() includes the prefix - Make update_mise_shim async and await the download instead of using block_in_place, since run() is already async Co-Authored-By: Claude Opus 4.6 --- src/cli/self_update.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index cb176881afb..1405bbad002 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -94,7 +94,7 @@ impl SelfUpdate { let styled_version = style(&version).bright().yellow(); miseprintln!("Updated mise to {styled_version}"); #[cfg(windows)] - if let Err(e) = Self::update_mise_shim(&version) { + if let Err(e) = Self::update_mise_shim(&version).await { warn!("Failed to update mise-shim.exe: {e}"); } } else { @@ -173,21 +173,20 @@ impl SelfUpdate { } #[cfg(windows)] - fn update_mise_shim(version: &str) -> Result<()> { + async fn update_mise_shim(version: &str) -> Result<()> { use crate::http::HTTP; use std::io::Read; + let version = version.strip_prefix('v').unwrap_or(version); let url = format!( - "https://github.com/jdx/mise/releases/download/{version}/mise-{version}-{}-{}.zip", + "https://github.com/jdx/mise/releases/download/v{version}/mise-v{version}-{}-{}.zip", *OS, *ARCH, ); debug!("Downloading mise-shim.exe from {url}"); let temp_dir = tempfile::tempdir()?; let zip_path = temp_dir.path().join("mise.zip"); - tokio::task::block_in_place(|| { - tokio::runtime::Handle::current().block_on(HTTP.download_file(&url, &zip_path, None)) - })?; + HTTP.download_file(&url, &zip_path, None).await?; let file = fs::File::open(&zip_path)?; let mut archive = zip::ZipArchive::new(file)?; From 7267f26b089b5ef6e7b88ea00832b8b8a37d701d Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Mon, 9 Feb 2026 19:45:48 -0600 Subject: [PATCH 3/3] fix(shim): add signature verification and atomic write for shim update Addresses PR review feedback: - Verify the downloaded zip archive signature using zipsign before extracting mise-shim.exe, matching the verification done for mise.exe - Use atomic write (temp file + rename) to avoid leaving a partially written mise-shim.exe if the process is interrupted - Name the temp zip with the real archive name so zipsign signing context matches Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 2 ++ Cargo.toml | 1 + src/cli/self_update.rs | 43 ++++++++++++++++++++++++++++++++++++------ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d2e98c90a9..758d79eb70a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5171,6 +5171,7 @@ dependencies = [ "xx", "xz2", "zip 3.0.0", + "zipsign-api", "zstd", ] @@ -9934,6 +9935,7 @@ dependencies = [ "base64 0.22.1", "ed25519-dalek", "thiserror 2.0.18", + "zip 2.4.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f4762478157..7fe85597618 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -208,6 +208,7 @@ self_update = { version = "0.42", optional = true, default-features = false, fea "signatures", "rustls", ] } +zipsign-api = "0.1" sevenz-rust = "0.6" winapi = { version = "0.3.9", features = ["consoleapi", "minwindef"] } diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 1405bbad002..62dd2f2f329 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -178,16 +178,19 @@ impl SelfUpdate { use std::io::Read; let version = version.strip_prefix('v').unwrap_or(version); - let url = format!( - "https://github.com/jdx/mise/releases/download/v{version}/mise-v{version}-{}-{}.zip", - *OS, *ARCH, - ); + let archive_name = format!("mise-v{version}-{}-{}.zip", *OS, *ARCH); + let url = + format!("https://github.com/jdx/mise/releases/download/v{version}/{archive_name}",); debug!("Downloading mise-shim.exe from {url}"); let temp_dir = tempfile::tempdir()?; - let zip_path = temp_dir.path().join("mise.zip"); + // Use the real archive name so zipsign context matches the release signature + let zip_path = temp_dir.path().join(&archive_name); HTTP.download_file(&url, &zip_path, None).await?; + // Verify the archive signature using the same key as the main update + Self::verify_zip_signature(&zip_path)?; + let file = fs::File::open(&zip_path)?; let mut archive = zip::ZipArchive::new(file)?; @@ -203,14 +206,42 @@ impl SelfUpdate { .parent() .expect("MISE_BIN should have a parent directory") .join("mise-shim.exe"); + + // Write to a temp file first, then rename for atomic replacement let mut buf = Vec::new(); shim_entry.read_to_end(&mut buf)?; - xx::file::write(&dest, &buf)?; + let temp_shim = temp_dir.path().join("mise-shim.exe"); + fs::write(&temp_shim, &buf)?; + if fs::rename(&temp_shim, &dest).is_err() { + // Fallback for cross-filesystem moves + fs::copy(&temp_shim, &dest)?; + } debug!("Updated mise-shim.exe at {}", dest.display()); Ok(()) } + #[cfg(windows)] + fn verify_zip_signature(path: &std::path::Path) -> Result<()> { + let context = path + .file_name() + .and_then(|s| s.to_str()) + .map(|s| s.as_bytes()) + .ok_or_else(|| color_eyre::eyre::eyre!("non-UTF8 archive path"))?; + + let keys = zipsign_api::verify::collect_keys( + [*include_bytes!("../../zipsign.pub")].into_iter().map(Ok), + ) + .map_err(|e| color_eyre::eyre::eyre!("failed to load verification keys: {e}"))?; + + let mut file = fs::File::open(path)?; + zipsign_api::verify::verify_zip(&mut file, &keys, Some(context)) + .map_err(|e| color_eyre::eyre::eyre!("zip signature verification failed: {e}"))?; + + debug!("Verified zip signature for {}", path.display()); + Ok(()) + } + pub fn is_available() -> bool { if let Some(b) = *env::MISE_SELF_UPDATE_AVAILABLE { return b;