windows: Fix ssh reporting wrong password even it's actually correct (#38263)

Closes #34393

Currently, we’re using `zed.exe --askpass` kind of like an `nc`
substitute, it prints out the SSH password to stdout with something like
`println!("user-pwd")`. `ssh.exe` then reads the password from stdout so
it can establish the connection.

The problem is that in release builds we set `subsystem=windows` to
avoid Windows spawning a black console window by default. The side
effect is that `zed.exe` no longer has a stdout, so `ssh.exe` can’t read
the password.

Through testing, I confirmed that neither allocating a new console for
`zed.exe` nor attaching it to the parent process’s stdout resolves the
issue. As a result, this PR updates the implementation to use `cli.exe
--askpass` instead.

TODO:

- [ ] Check that the `cli` path is correct on macOS
- [ ] Check that the `cli` path is correct on Linux

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
This commit is contained in:
张小白
2025-09-30 21:03:06 +08:00
committed by GitHub
co-authored by Piotr Osiewicz
parent 4f95186b53
commit 14fc726cae
8 changed files with 78 additions and 30 deletions
Generated
+1 -1
View File
@@ -3080,6 +3080,7 @@ name = "cli"
version = "0.1.0"
dependencies = [
"anyhow",
"askpass",
"clap",
"collections",
"core-foundation 0.10.0",
@@ -20215,7 +20216,6 @@ dependencies = [
"agent_ui",
"anyhow",
"ashpd 0.11.0",
"askpass",
"assets",
"assistant_tools",
"audio",
+9 -12
View File
@@ -85,11 +85,8 @@ impl AskPassSession {
let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
let listener = UnixListener::bind(&askpass_socket).context("creating askpass socket")?;
#[cfg(not(target_os = "windows"))]
let zed_path = util::get_shell_safe_zed_path()?;
#[cfg(target_os = "windows")]
let zed_path = std::env::current_exe()
.context("finding current executable path for use in askpass")?;
let zed_cli_path =
util::get_shell_safe_zed_cli_path().context("getting zed-cli path for askpass")?;
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
let mut kill_tx = Some(askpass_kill_master_tx);
@@ -137,7 +134,7 @@ impl AskPassSession {
});
// Create an askpass script that communicates back to this process.
let askpass_script = generate_askpass_script(&zed_path, &askpass_socket);
let askpass_script = generate_askpass_script(&zed_cli_path, &askpass_socket);
fs::write(&askpass_script_path, askpass_script)
.await
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
@@ -254,10 +251,10 @@ pub fn main(socket: &str) {
#[inline]
#[cfg(not(target_os = "windows"))]
fn generate_askpass_script(zed_path: &str, askpass_socket: &std::path::Path) -> String {
fn generate_askpass_script(zed_cli_path: &str, askpass_socket: &std::path::Path) -> String {
format!(
"{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n",
zed_exe = zed_path,
"{shebang}\n{print_args} | {zed_cli} --askpass={askpass_socket} 2> /dev/null \n",
zed_cli = zed_cli_path,
askpass_socket = askpass_socket.display(),
print_args = "printf '%s\\0' \"$@\"",
shebang = "#!/bin/sh",
@@ -266,13 +263,13 @@ fn generate_askpass_script(zed_path: &str, askpass_socket: &std::path::Path) ->
#[inline]
#[cfg(target_os = "windows")]
fn generate_askpass_script(zed_path: &std::path::Path, askpass_socket: &std::path::Path) -> String {
fn generate_askpass_script(zed_cli_path: &str, askpass_socket: &std::path::Path) -> String {
format!(
r#"
$ErrorActionPreference = 'Stop';
($args -join [char]0) | & "{zed_exe}" --askpass={askpass_socket} 2> $null
($args -join [char]0) | & "{zed_cli}" --askpass={askpass_socket} 2> $null
"#,
zed_exe = zed_path.display(),
zed_cli = zed_cli_path,
askpass_socket = askpass_socket.display(),
)
}
+2 -2
View File
@@ -67,7 +67,7 @@ impl TryFrom<&str> for EncryptedPassword {
unsafe {
CryptProtectMemory(
value.as_mut_ptr() as _,
len,
padded_length,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)?;
}
@@ -97,7 +97,7 @@ pub(crate) fn decrypt(mut password: EncryptedPassword) -> Result<String> {
unsafe {
CryptUnprotectMemory(
password.0.as_mut_ptr() as _,
password.1,
password.0.len().try_into()?,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)
.context("while decrypting a SSH password")?
+1
View File
@@ -22,6 +22,7 @@ default = []
[dependencies]
anyhow.workspace = true
askpass.workspace = true
clap.workspace = true
collections.workspace = true
ipc-channel = "0.19"
+11
View File
@@ -116,6 +116,11 @@ struct Args {
))]
#[arg(long)]
uninstall: bool,
/// Used for SSH/Git password authentication, to remove the need for netcat as a dependency,
/// by having Zed act like netcat communicating over a Unix socket.
#[arg(long, hide = true)]
askpass: Option<String>,
}
fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
@@ -203,6 +208,12 @@ fn main() -> Result<()> {
}
let args = Args::parse();
// `zed --askpass` Makes zed operate in nc/netcat mode for use with askpass
if let Some(socket) = &args.askpass {
askpass::main(socket);
return Ok(());
}
// Set custom data directory before any path operations
let user_data_dir = args.user_data_dir.clone();
if let Some(dir) = &user_data_dir {
+54 -3
View File
@@ -14,7 +14,7 @@ pub mod size;
pub mod test;
pub mod time;
use anyhow::Result;
use anyhow::{Context as _, Result};
use futures::Future;
use itertools::Either;
use regex::Regex;
@@ -290,8 +290,6 @@ fn load_shell_from_passwd() -> Result<()> {
#[cfg(unix)]
/// Returns a shell escaped path for the current zed executable
pub fn get_shell_safe_zed_path() -> anyhow::Result<String> {
use anyhow::Context;
let zed_path = std::env::current_exe()
.context("Failed to determine current zed executable path.")?
.to_string_lossy()
@@ -307,6 +305,59 @@ pub fn get_shell_safe_zed_path() -> anyhow::Result<String> {
Ok(zed_path_escaped.to_string())
}
/// Returns a shell escaped path for the zed cli executable, this function
/// should be called from the zed executable, not zed-cli.
pub fn get_shell_safe_zed_cli_path() -> Result<String> {
let zed_path =
std::env::current_exe().context("Failed to determine current zed executable path.")?;
let parent = zed_path
.parent()
.context("Failed to determine parent directory of zed executable path.")?;
let possible_locations: &[&str] = if cfg!(target_os = "macos") {
// On macOS, the zed executable and zed-cli are inside the app bundle,
// so here ./cli is for both installed and development builds.
&["./cli"]
} else if cfg!(target_os = "windows") {
// bin/zed.exe is for installed builds, ./cli.exe is for development builds.
&["bin/zed.exe", "./cli.exe"]
} else if cfg!(target_os = "linux") || cfg!(target_os = "freebsd") {
// bin is the standard, ./cli is for the target directory in development builds.
&["../bin/zed", "./cli"]
} else {
anyhow::bail!("unsupported platform for determining zed-cli path");
};
let zed_cli_path = possible_locations
.iter()
.find_map(|p| {
parent
.join(p)
.canonicalize()
.ok()
.filter(|p| p != &zed_path)
})
.with_context(|| {
format!(
"could not find zed-cli from any of: {}",
possible_locations.join(", ")
)
})?
.to_string_lossy()
.to_string();
#[cfg(target_os = "windows")]
{
Ok(zed_cli_path)
}
#[cfg(not(target_os = "windows"))]
{
Ok(shlex::try_quote(&zed_cli_path)
.context("Failed to shell-escape Zed executable path.")?
.to_string())
}
}
#[cfg(unix)]
pub async fn load_login_shell_environment() -> Result<()> {
load_shell_from_passwd().log_err();
-1
View File
@@ -25,7 +25,6 @@ agent.workspace = true
agent_settings.workspace = true
agent_ui.workspace = true
anyhow.workspace = true
askpass.workspace = true
assets.workspace = true
assistant_tools.workspace = true
audio.workspace = true
-11
View File
@@ -176,12 +176,6 @@ pub fn main() {
return;
}
// `zed --askpass` Makes zed operate in nc/netcat mode for use with askpass
if let Some(socket) = &args.askpass {
askpass::main(socket);
return;
}
// `zed --nc` Makes zed operate in nc/netcat mode for use with MCP
if let Some(socket) = &args.nc {
match nc::main(socket) {
@@ -1249,11 +1243,6 @@ struct Args {
#[arg(long)]
system_specs: bool,
/// Used for SSH/Git password authentication, to remove the need for netcat as a dependency,
/// by having Zed act like netcat communicating over a Unix socket.
#[arg(long, hide = true)]
askpass: Option<String>,
/// Used for the MCP Server, to remove the need for netcat as a dependency,
/// by having Zed act like netcat communicating over a Unix socket.
#[arg(long, hide = true)]