From c694b5476a81f77c769b3eb9dd54b00d0a7f1d7b Mon Sep 17 00:00:00 2001 From: Zaenalos Date: Thu, 26 Mar 2026 19:56:51 +0800 Subject: [PATCH] fix(windows): fix SSH askpass by invoking cli.exe directly On Windows, Zed generated a .ps1 askpass script and set SSH_ASKPASS to 'powershell.exe -ExecutionPolicy Bypass -File ...'. SSH calls exec() on SSH_ASKPASS which cannot exec a command string, causing: error: ssh_askpass: exec(powershell.exe ...): No such file or directory Fix by pointing SSH_ASKPASS directly to cli.exe and passing the socket path via ZED_ASKPASS_SOCKET env var, which cli.exe reads before clap parses arguments. Fixes #29048 --- crates/askpass/src/askpass.rs | 140 +++++++++++++++++----------------- crates/cli/src/main.rs | 8 ++ crates/git/src/repository.rs | 2 + 3 files changed, 79 insertions(+), 71 deletions(-) diff --git a/crates/askpass/src/askpass.rs b/crates/askpass/src/askpass.rs index 4e84d389467..051950ef600 100644 --- a/crates/askpass/src/askpass.rs +++ b/crates/askpass/src/askpass.rs @@ -20,12 +20,18 @@ use futures::{ }; use gpui::{AsyncApp, BackgroundExecutor, Task}; use smol::fs; -use util::{ResultExt as _, debug_panic, maybe, paths::PathExt, shell::ShellKind}; +use util::{ResultExt as _, debug_panic, maybe, paths::PathExt}; + +#[cfg(not(target_os = "windows"))] +use util::shell::ShellKind; /// Path to the program used for askpass /// -/// On Unix and remote servers, this defaults to the current executable -/// On Windows, this is set to the CLI variant of zed +/// On Unix and remote servers, this defaults to the current executable. +/// On Windows, this must be set to the CLI variant of zed via set_askpass_program(), +/// because SSH_ASKPASS must point to a directly executable binary. The CLI binary +/// handles the ZED_ASKPASS_SOCKET env var to communicate with Zed over a Unix socket +/// without needing a wrapper script. static ASKPASS_PROGRAM: OnceLock = OnceLock::new(); #[derive(PartialEq, Eq)] @@ -80,11 +86,8 @@ pub struct AskPassSession { executor: BackgroundExecutor, } -const ASKPASS_SCRIPT_NAME: &str = if cfg!(target_os = "windows") { - "askpass.ps1" -} else { - "askpass.sh" -}; +#[cfg(not(target_os = "windows"))] +const ASKPASS_SCRIPT_NAME: &str = "askpass.sh"; impl AskPassSession { /// This will create a new AskPassSession. @@ -177,17 +180,34 @@ impl AskPassSession { self.secret.lock().ok()?.clone() } + /// Returns the value to set as SSH_ASKPASS. + /// On Unix this is the path to the generated shell script. + /// On Windows this is the path to cli.exe directly — no script needed. pub fn script_path(&self) -> impl AsRef { self.askpass_task.script_path() } + + /// Returns the socket path to set as ZED_ASKPASS_SOCKET. + /// + /// On Windows, SSH_ASKPASS points directly to cli.exe. SSH passes only + /// the prompt string as argv[1] with no mechanism for extra arguments, + /// so the socket path is communicated via this environment variable instead. + /// cli.exe must check ZED_ASKPASS_SOCKET before clap parses args. + #[cfg(target_os = "windows")] + pub fn socket_path(&self) -> impl AsRef { + self.askpass_task.socket_path() + } } pub struct PasswordProxy { _task: Task<()>, - #[cfg(not(target_os = "windows"))] + /// On Unix: path to the generated .sh askpass script (set as SSH_ASKPASS). + /// On Windows: path to cli.exe (set as SSH_ASKPASS directly — no script needed). askpass_script_path: std::path::PathBuf, + /// On Windows only: path to the Unix socket, passed as ZED_ASKPASS_SOCKET + /// so cli.exe can find it without --askpass argument parsing. #[cfg(target_os = "windows")] - askpass_helper: String, + askpass_socket_path: std::path::PathBuf, } impl PasswordProxy { @@ -202,19 +222,21 @@ impl PasswordProxy { ) -> Result { let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?; let askpass_socket = temp_dir.path().join("askpass.sock"); - let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME); let current_exec = std::env::current_exe().context("Failed to determine current zed executable path.")?; - // TODO: inferred from the use of powershell.exe in askpass_helper_script - let shell_kind = if cfg!(windows) { - ShellKind::PowerShell - } else { - ShellKind::Posix - }; let askpass_program = ASKPASS_PROGRAM.get_or_init(|| current_exec); - // Create an askpass script that communicates back to this process. - let askpass_script = generate_askpass_script(shell_kind, askpass_program, &askpass_socket)?; + + // Unix: SSH_ASKPASS = path to generated .sh script in temp dir. + // Windows: SSH_ASKPASS = path to cli.exe directly. No script is written. + #[cfg(not(target_os = "windows"))] + let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME); + #[cfg(target_os = "windows")] + let askpass_script_path = askpass_program.to_path_buf(); + + #[cfg(target_os = "windows")] + let askpass_socket_path = askpass_socket.clone(); + let _task = executor.spawn(async move { maybe!(async move { let listener = @@ -253,43 +275,44 @@ impl PasswordProxy { .log_err(); }); - fs::write(&askpass_script_path, askpass_script) - .await - .with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?; - make_file_executable(&askpass_script_path) - .await - .with_context(|| { - format!("marking askpass script executable at {askpass_script_path:?}") - })?; - // todo(shell): There might be no powershell on the system - #[cfg(target_os = "windows")] - let askpass_helper = format!( - "powershell.exe -ExecutionPolicy Bypass -File \"{}\"", - askpass_script_path.display() - ); + // Unix only: write the shell script and mark it executable. + // On Windows cli.exe is invoked directly, so no script is needed. + #[cfg(not(target_os = "windows"))] + { + let askpass_script = generate_askpass_script(askpass_program, &askpass_socket)?; + fs::write(&askpass_script_path, askpass_script) + .await + .with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?; + make_file_executable(&askpass_script_path) + .await + .with_context(|| { + format!("marking askpass script executable at {askpass_script_path:?}") + })?; + } Ok(Self { _task, - #[cfg(not(target_os = "windows"))] askpass_script_path, #[cfg(target_os = "windows")] - askpass_helper, + askpass_socket_path, }) } pub fn script_path(&self) -> impl AsRef { - #[cfg(not(target_os = "windows"))] - { - &self.askpass_script_path - } - #[cfg(target_os = "windows")] - { - &self.askpass_helper - } + &self.askpass_script_path + } + + #[cfg(target_os = "windows")] + pub fn socket_path(&self) -> impl AsRef { + &self.askpass_socket_path } } + /// The main function for when Zed is running in netcat mode for use in askpass. /// Called from both the remote server binary and the zed binary in their respective main functions. +/// +/// On Windows, the socket path is passed via ZED_ASKPASS_SOCKET rather than --askpass, +/// because SSH_ASKPASS points directly to cli.exe and SSH only passes the prompt as argv[1]. pub fn main(socket: &str) { use net::UnixStream; use std::io::{self, Read, Write}; @@ -340,13 +363,14 @@ pub fn set_askpass_program(path: std::path::PathBuf) { } } -#[inline] +/// Generates the Unix shell askpass script. +/// Not used on Windows — cli.exe is invoked directly as SSH_ASKPASS. #[cfg(not(target_os = "windows"))] fn generate_askpass_script( - shell_kind: ShellKind, askpass_program: &std::path::Path, askpass_socket: &std::path::Path, ) -> Result { + let shell_kind = ShellKind::Posix; let askpass_program = shell_kind.prepend_command_prefix( askpass_program .to_str() @@ -364,29 +388,3 @@ fn generate_askpass_script( "{shebang}\n{print_args} | {askpass_program} --askpass={askpass_socket} 2> /dev/null \n", )) } - -#[inline] -#[cfg(target_os = "windows")] -fn generate_askpass_script( - shell_kind: ShellKind, - askpass_program: &std::path::Path, - askpass_socket: &std::path::Path, -) -> Result { - let askpass_program = shell_kind.prepend_command_prefix( - askpass_program - .to_str() - .context("Askpass program is on a non-utf8 path")?, - ); - let askpass_program = shell_kind - .try_quote_prefix_aware(&askpass_program) - .context("Failed to shell-escape Askpass program path")?; - let askpass_socket = askpass_socket - .try_shell_safe(shell_kind) - .context("Failed to shell-escape Askpass socket path")?; - Ok(format!( - r#" - $ErrorActionPreference = 'Stop'; - ($args -join [char]0) | {askpass_program} --askpass={askpass_socket} 2> $null - "#, - )) -} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index b8af5896285..cee61f51862 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -469,6 +469,14 @@ fn main() -> Result<()> { return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect()); } } + + // Must happen before clap — SSH invokes cli.exe directly as SSH_ASKPASS + // and passes the socket path via env var to avoid argument parsing. + if let Ok(socket) = std::env::var("ZED_ASKPASS_SOCKET") { + askpass::main(&socket); + return Ok(()); + } + let args = Args::parse(); // `zed --askpass` Makes zed operate in nc/netcat mode for use with askpass diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 32904aa9a90..e2c070893b5 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -3097,6 +3097,8 @@ async fn run_git_command( .env("GIT_ASKPASS", ask_pass.script_path()) .env("SSH_ASKPASS", ask_pass.script_path()) .env("SSH_ASKPASS_REQUIRE", "force"); + #[cfg(target_os = "windows")] + command.env("ZED_ASKPASS_SOCKET", ask_pass.socket_path()); let git_process = command.spawn()?; run_askpass_command(ask_pass, git_process).await