logiguard fork v3: full patch set on verified 8c74db0 tree

Includes prior-session patches (carry forward so the app compiles):
  - crates/gpui/build.rs: cross-compile manifest fix
  - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method
  - crates/gpui/src/window.rs: Window::activate_with_token public API
  - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix

Plus the focus-serial fix:
  - serial.rs: SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; latest_serial_of()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 01:52:12 +03:30
commit b9819977a5
3984 changed files with 1487015 additions and 0 deletions

29
crates/askpass/Cargo.toml Normal file
View File

@@ -0,0 +1,29 @@
[package]
name = "askpass"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/askpass.rs"
[dependencies]
anyhow.workspace = true
futures.workspace = true
gpui.workspace = true
net.workspace = true
smol.workspace = true
log.workspace = true
tempfile.workspace = true
util.workspace = true
zeroize.workspace = true
[target.'cfg(target_os = "windows")'.dependencies]
windows.workspace = true
[package.metadata.cargo-machete]
ignored = ["log"]

1
crates/askpass/LICENSE-GPL Symbolic link
View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,392 @@
mod encrypted_password;
pub use encrypted_password::{EncryptedPassword, IKnowWhatIAmDoingAndIHaveReadTheDocs};
use net::async_net::UnixListener;
use smol::lock::Mutex;
use util::fs::make_file_executable;
use std::ffi::OsStr;
use std::ops::ControlFlow;
use std::sync::Arc;
use std::sync::OnceLock;
use std::time::Duration;
use anyhow::{Context as _, Result};
use futures::channel::{mpsc, oneshot};
use futures::{
AsyncBufReadExt as _, AsyncWriteExt as _, FutureExt as _, SinkExt, StreamExt, io::BufReader,
select_biased,
};
use gpui::{AsyncApp, BackgroundExecutor, Task};
use smol::fs;
use util::{ResultExt as _, debug_panic, maybe, paths::PathExt, 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
static ASKPASS_PROGRAM: OnceLock<std::path::PathBuf> = OnceLock::new();
#[derive(PartialEq, Eq)]
pub enum AskPassResult {
CancelledByUser,
Timedout,
}
pub struct AskPassDelegate {
tx: mpsc::UnboundedSender<(String, oneshot::Sender<EncryptedPassword>)>,
executor: BackgroundExecutor,
_task: Task<()>,
}
impl AskPassDelegate {
pub fn new(
cx: &mut AsyncApp,
password_prompt: impl Fn(String, oneshot::Sender<EncryptedPassword>, &mut AsyncApp)
+ Send
+ Sync
+ 'static,
) -> Self {
let (tx, mut rx) = mpsc::unbounded::<(String, oneshot::Sender<_>)>();
let task = cx.spawn(async move |cx: &mut AsyncApp| {
while let Some((prompt, channel)) = rx.next().await {
password_prompt(prompt, channel, cx);
}
});
Self {
tx,
_task: task,
executor: cx.background_executor().clone(),
}
}
pub fn ask_password(&mut self, prompt: String) -> Task<Option<EncryptedPassword>> {
let mut this_tx = self.tx.clone();
self.executor.spawn(async move {
let (tx, rx) = oneshot::channel();
this_tx.send((prompt, tx)).await.ok()?;
rx.await.ok()
})
}
}
pub struct AskPassSession {
#[cfg(target_os = "windows")]
secret: std::sync::Arc<std::sync::Mutex<Option<EncryptedPassword>>>,
askpass_task: PasswordProxy,
askpass_opened_rx: Option<oneshot::Receiver<()>>,
askpass_kill_master_rx: Option<oneshot::Receiver<()>>,
executor: BackgroundExecutor,
}
const ASKPASS_SCRIPT_NAME: &str = if cfg!(target_os = "windows") {
"askpass.ps1"
} else {
"askpass.sh"
};
impl AskPassSession {
/// This will create a new AskPassSession.
/// You must retain this session until the master process exits.
#[must_use]
pub async fn new(executor: BackgroundExecutor, mut delegate: AskPassDelegate) -> Result<Self> {
#[cfg(target_os = "windows")]
let secret = std::sync::Arc::new(std::sync::Mutex::new(None));
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
let askpass_opened_tx = Arc::new(Mutex::new(Some(askpass_opened_tx)));
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
let kill_tx = Arc::new(Mutex::new(Some(askpass_kill_master_tx)));
let get_password = {
let executor = executor.clone();
#[cfg(target_os = "windows")]
let askpass_secret = secret.clone();
move |prompt| {
let prompt = delegate.ask_password(prompt);
let kill_tx = kill_tx.clone();
let askpass_opened_tx = askpass_opened_tx.clone();
#[cfg(target_os = "windows")]
let askpass_secret = askpass_secret.clone();
executor.spawn(async move {
if let Some(askpass_opened_tx) = askpass_opened_tx.lock().await.take() {
askpass_opened_tx.send(()).ok();
}
if let Some(password) = prompt.await {
#[cfg(target_os = "windows")]
{
askpass_secret.lock().unwrap().replace(password.clone());
}
ControlFlow::Continue(Ok(password))
} else {
if let Some(kill_tx) = kill_tx.lock().await.take() {
kill_tx.send(()).ok();
}
ControlFlow::Break(())
}
})
}
};
let askpass_task = PasswordProxy::new(Box::new(get_password), executor.clone()).await?;
Ok(Self {
#[cfg(target_os = "windows")]
secret,
askpass_task,
askpass_kill_master_rx: Some(askpass_kill_master_rx),
askpass_opened_rx: Some(askpass_opened_rx),
executor,
})
}
// This will run the askpass task forever, resolving as many authentication requests as needed.
// The caller is responsible for examining the result of their own commands and cancelling this
// future when this is no longer needed. Note that this can only be called once, but due to the
// drop order this takes an &mut, so you can `drop()` it after you're done with the master process.
pub async fn run(&mut self) -> AskPassResult {
// This is the default timeout setting used by VSCode.
let connection_timeout = Duration::from_secs(17);
let askpass_opened_rx = self.askpass_opened_rx.take().expect("Only call run once");
let askpass_kill_master_rx = self
.askpass_kill_master_rx
.take()
.expect("Only call run once");
let executor = self.executor.clone();
select_biased! {
_ = askpass_opened_rx.fuse() => {
// Note: this await can only resolve after we are dropped.
askpass_kill_master_rx.await.ok();
AskPassResult::CancelledByUser
}
_ = futures::FutureExt::fuse(executor.timer(connection_timeout)) => {
AskPassResult::Timedout
}
}
}
/// This will return the password that was last set by the askpass script.
#[cfg(target_os = "windows")]
pub fn get_password(&self) -> Option<EncryptedPassword> {
self.secret.lock().ok()?.clone()
}
pub fn script_path(&self) -> impl AsRef<OsStr> {
self.askpass_task.script_path()
}
}
pub struct PasswordProxy {
_task: Task<()>,
#[cfg(not(target_os = "windows"))]
askpass_script_path: std::path::PathBuf,
#[cfg(target_os = "windows")]
askpass_helper: String,
}
impl PasswordProxy {
pub async fn new(
mut get_password: Box<
dyn FnMut(String) -> Task<ControlFlow<(), Result<EncryptedPassword>>>
+ 'static
+ Send
+ Sync,
>,
executor: BackgroundExecutor,
) -> Result<Self> {
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)?;
let _task = executor.spawn(async move {
maybe!(async move {
let listener =
UnixListener::bind(&askpass_socket).context("creating askpass socket")?;
while let Ok((mut stream, _)) = listener.accept().await {
let mut buffer = Vec::new();
let mut reader = BufReader::new(&mut stream);
if reader.read_until(b'\0', &mut buffer).await.is_err() {
buffer.clear();
}
let prompt = String::from_utf8_lossy(&buffer).into_owned();
let password = get_password(prompt).await;
match password {
ControlFlow::Continue(password) => {
if let Ok(password) = password
&& let Ok(decrypted) =
password.decrypt(IKnowWhatIAmDoingAndIHaveReadTheDocs)
{
stream.write_all(decrypted.as_bytes()).await.log_err();
}
}
ControlFlow::Break(()) => {
// note: we expect the caller to drop this task when it's done.
// We need to keep the stream open until the caller is done to avoid
// spurious errors from ssh.
std::future::pending::<()>().await;
drop(stream);
}
}
}
drop(temp_dir);
Result::<_, anyhow::Error>::Ok(())
})
.await
.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()
);
Ok(Self {
_task,
#[cfg(not(target_os = "windows"))]
askpass_script_path,
#[cfg(target_os = "windows")]
askpass_helper,
})
}
pub fn script_path(&self) -> impl AsRef<OsStr> {
#[cfg(not(target_os = "windows"))]
{
&self.askpass_script_path
}
#[cfg(target_os = "windows")]
{
&self.askpass_helper
}
}
}
/// 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.
pub fn main(socket: &str) {
use net::UnixStream;
use std::io::{self, Read, Write};
use std::process::exit;
let mut stream = match UnixStream::connect(socket) {
Ok(stream) => stream,
Err(err) => {
eprintln!("Error connecting to socket {}: {}", socket, err);
exit(1);
}
};
let mut buffer = Vec::new();
if let Err(err) = io::stdin().read_to_end(&mut buffer) {
eprintln!("Error reading from stdin: {}", err);
exit(1);
}
#[cfg(target_os = "windows")]
while buffer.last().is_some_and(|&b| b == b'\n' || b == b'\r') {
buffer.pop();
}
if buffer.last() != Some(&b'\0') {
buffer.push(b'\0');
}
if let Err(err) = stream.write_all(&buffer) {
eprintln!("Error writing to socket: {}", err);
exit(1);
}
let mut response = Vec::new();
if let Err(err) = stream.read_to_end(&mut response) {
eprintln!("Error reading from socket: {}", err);
exit(1);
}
if let Err(err) = io::stdout().write_all(&response) {
eprintln!("Error writing to stdout: {}", err);
exit(1);
}
}
pub fn set_askpass_program(path: std::path::PathBuf) {
if ASKPASS_PROGRAM.set(path).is_err() {
debug_panic!("askpass program has already been set");
}
}
#[inline]
#[cfg(not(target_os = "windows"))]
fn generate_askpass_script(
shell_kind: ShellKind,
askpass_program: &std::path::Path,
askpass_socket: &std::path::Path,
) -> Result<String> {
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")?;
let print_args = "printf '%s\\0' \"$@\"";
let shebang = "#!/bin/sh";
Ok(format!(
"{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<String> {
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
"#,
))
}

View File

@@ -0,0 +1,102 @@
//! This module provides [EncryptedPassword] for storage of passwords in memory.
//! On Windows that's implemented with CryptProtectMemory/CryptUnprotectMemory; on other platforms it just falls through
//! to string for now.
//!
//! The "safety" of this module lies in exploiting visibility rules of Rust:
//! 1. No outside module has access to the internal representation of [EncryptedPassword].
//! 2. [EncryptedPassword] cannot be converted into a [String] or any other plaintext representation.
//! All use cases that do need such functionality (of which we have two right now) are implemented within this module.
//!
//! Note that this is not bulletproof.
//! 1. [ProcessExt] is implemented for [smol::process::Command], which is a builder for smol processes.
//! Before the process itself is spawned the contents of [EncryptedPassword] are unencrypted in env var storage of said builder.
//! 2. We're also sending plaintext passwords over RPC with [proto::AskPassResponse]. Go figure how great that is.
//!
//! Still, the goal of this module is to not have passwords laying around nilly-willy in memory.
//! We do not claim that it is fool-proof.
use anyhow::Result;
use zeroize::Zeroize;
type LengthWithoutPadding = u32;
#[derive(Clone)]
pub struct EncryptedPassword(Vec<u8>, LengthWithoutPadding);
impl Drop for EncryptedPassword {
fn drop(&mut self) {
self.0.zeroize();
self.1.zeroize();
}
}
impl TryFrom<&str> for EncryptedPassword {
type Error = anyhow::Error;
fn try_from(password: &str) -> Result<EncryptedPassword> {
let len: u32 = password.len().try_into()?;
#[cfg(windows)]
{
use windows::Win32::Security::Cryptography::{
CRYPTPROTECTMEMORY_BLOCK_SIZE, CRYPTPROTECTMEMORY_SAME_PROCESS, CryptProtectMemory,
};
let mut value = password.bytes().collect::<Vec<_>>();
let padded_length = len.next_multiple_of(CRYPTPROTECTMEMORY_BLOCK_SIZE);
if padded_length != len {
value.resize(padded_length as usize, 0);
}
if len != 0 {
unsafe {
CryptProtectMemory(
value.as_mut_ptr() as _,
padded_length,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)?;
}
}
Ok(Self(value, len))
}
#[cfg(not(windows))]
Ok(Self(String::from(password).into(), len))
}
}
/// Read the docs for [EncryptedPassword]; please take care of not storing the plaintext string in memory for extended
/// periods of time.
pub struct IKnowWhatIAmDoingAndIHaveReadTheDocs;
impl EncryptedPassword {
pub fn decrypt(mut self, _: IKnowWhatIAmDoingAndIHaveReadTheDocs) -> Result<String> {
#[cfg(windows)]
{
use anyhow::Context;
use windows::Win32::Security::Cryptography::{
CRYPTPROTECTMEMORY_BLOCK_SIZE, CRYPTPROTECTMEMORY_SAME_PROCESS,
CryptUnprotectMemory,
};
assert_eq!(
self.0.len() % CRYPTPROTECTMEMORY_BLOCK_SIZE as usize,
0,
"Violated pre-condition (buffer size <{}> must be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE <{}>) for CryptUnprotectMemory.",
self.0.len(),
CRYPTPROTECTMEMORY_BLOCK_SIZE
);
if self.1 != 0 {
unsafe {
CryptUnprotectMemory(
self.0.as_mut_ptr() as _,
self.0.len().try_into()?,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)
.context("while decrypting a SSH password")?
};
{
// Remove padding
_ = self.0.drain(self.1 as usize..);
}
}
Ok(String::from_utf8(std::mem::take(&mut self.0))?)
}
#[cfg(not(windows))]
Ok(String::from_utf8(std::mem::take(&mut self.0))?)
}
}