From d23024609f696b1a6644c552237828ce1e6028d7 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Fri, 25 Apr 2025 17:16:43 -0400 Subject: [PATCH] askpass: Shell escape Zed path in askpass script (#29447) Closes #29439 Add shell escaping as well as additional sanity check for Zed path when used in askpass. This caused issues on preview and nightly as the standard paths for those releases contain spaces which were not escaped appropriately leading to erroneous "Permission denied" errors from SSH when the askpass script failed Release Notes: - Fixed a missing shell-escape in askpass resulting in erroneous "Permission denied" errors when trying to connect to a remote server over ssh (effecting preview release v0.184.1 and nightly only) --- Cargo.lock | 1 + crates/askpass/Cargo.toml | 1 + crates/askpass/src/askpass.rs | 31 ++++++++++++++++++++++++++++--- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7c51eaf766..f79af953cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -446,6 +446,7 @@ dependencies = [ "anyhow", "futures 0.3.31", "gpui", + "shlex", "smol", "tempfile", "util", diff --git a/crates/askpass/Cargo.toml b/crates/askpass/Cargo.toml index d64ee9f7c3..a2df3b8495 100644 --- a/crates/askpass/Cargo.toml +++ b/crates/askpass/Cargo.toml @@ -15,6 +15,7 @@ path = "src/askpass.rs" anyhow.workspace = true futures.workspace = true gpui.workspace = true +shlex.workspace = true smol.workspace = true tempfile.workspace = true util.workspace = true diff --git a/crates/askpass/src/askpass.rs b/crates/askpass/src/askpass.rs index a4bdb5edb8..b5435ac98e 100644 --- a/crates/askpass/src/askpass.rs +++ b/crates/askpass/src/askpass.rs @@ -72,8 +72,7 @@ impl AskPassSession { let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>(); let listener = UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?; - let zed_path = std::env::current_exe() - .context("Failed to figure out current executable path for use in askpass")?; + let zed_path = get_shell_safe_zed_path()?; let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>(); let mut kill_tx = Some(askpass_kill_master_tx); @@ -115,7 +114,7 @@ impl AskPassSession { // Create an askpass script that communicates back to this process. let askpass_script = format!( "{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n", - zed_exe = zed_path.display(), + zed_exe = zed_path, askpass_socket = askpass_socket.display(), print_args = "printf '%s\\0' \"$@\"", shebang = "#!/bin/sh", @@ -161,6 +160,32 @@ impl AskPassSession { } } +#[cfg(unix)] +fn get_shell_safe_zed_path() -> anyhow::Result { + let zed_path = std::env::current_exe() + .context("Failed to figure out current executable path for use in askpass")? + .to_string_lossy() + .to_string(); + + // sanity check on unix systems that the path exists and is executable + // todo(windows): implement this check for windows (or just use `is-executable` crate) + use std::os::unix::fs::MetadataExt; + let metadata = std::fs::metadata(&zed_path) + .context("Failed to check metadata of Zed executable path for use in askpass")?; + let is_executable = metadata.is_file() && metadata.mode() & 0o111 != 0; + anyhow::ensure!( + is_executable, + "Failed to verify Zed executable path for use in askpass" + ); + // As of writing, this can only be fail if the path contains a null byte, which shouldn't be possible + // but shlex has annotated the error as #[non_exhaustive] so we can't make it a compile error if other + // errors are introduced in the future :( + let zed_path_escaped = shlex::try_quote(&zed_path) + .context("Failed to shell-escape Zed executable path for use in askpass")?; + + return Ok(zed_path_escaped.to_string()); +} + /// 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. #[cfg(unix)]