From 0a800b8f2e9894433c6ef9d106d10766ac408218 Mon Sep 17 00:00:00 2001 From: Can Celik Date: Mon, 3 Aug 2026 00:52:07 +0300 Subject: [PATCH] fix: avoid blocking on foreground cwd checks (#2213) refs #2206 --- src/pane.rs | 58 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/src/pane.rs b/src/pane.rs index c3e91f7f..4ffd0097 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -264,9 +264,14 @@ struct AgentDetectionPresence { consecutive_misses: u8, } +#[cfg(unix)] +fn absolute_process_cwd(pid: u32) -> Option { + crate::platform::process_cwd(pid).filter(|cwd| cwd.is_absolute()) +} + #[cfg(unix)] fn usable_process_cwd(pid: u32) -> Option { - crate::platform::process_cwd(pid).filter(|cwd| cwd.is_absolute() && cwd.is_dir()) + absolute_process_cwd(pid).filter(|cwd| cwd.is_dir()) } #[cfg(unix)] @@ -279,7 +284,7 @@ fn foreground_member_cwd_different_from_shell( if process.pid == shell_pid { continue; } - let Some(cwd) = usable_process_cwd(process.pid) else { + let Some(cwd) = absolute_process_cwd(process.pid) else { continue; }; if shell_cwd != Some(&cwd) { @@ -2821,12 +2826,12 @@ impl PaneRuntime { #[cfg(unix)] { let pid = self.child_pid.load(Ordering::Acquire); - let shell_cwd = usable_process_cwd(pid); + let shell_cwd = absolute_process_cwd(pid); let foreground_pgid = self .io .foreground_process_group_id() .or_else(|| crate::platform::foreground_process_group_id(pid)); - let leader_cwd = foreground_pgid.and_then(usable_process_cwd); + let leader_cwd = foreground_pgid.and_then(absolute_process_cwd); if leader_cwd.as_ref() == shell_cwd.as_ref() { foreground_member_cwd_different_from_shell(pid, shell_cwd.as_ref()).or(leader_cwd) @@ -2955,6 +2960,51 @@ mod tests { assert_eq!(runtime.cwd(), Some(cwd)); } + #[cfg(unix)] + #[test] + fn process_cwd_does_not_require_traversing_the_directory_path() { + use std::os::unix::fs::PermissionsExt; + + let stamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("clock should be after unix epoch") + .as_nanos(); + let base = std::env::temp_dir().join(format!( + "herdr-process-cwd-no-stat-{}-{stamp}", + std::process::id() + )); + let private = base.join("private"); + let cwd = private.join("cwd"); + std::fs::create_dir_all(&cwd).expect("create process cwd"); + + let mut child = std::process::Command::new("/bin/sh") + .args(["-c", "sleep 30"]) + .current_dir(&cwd) + .spawn() + .expect("spawn process in cwd"); + let expected_cwd = crate::platform::process_cwd(child.id()) + .expect("resolve process cwd before restricting traversal"); + std::fs::set_permissions(&private, std::fs::Permissions::from_mode(0o000)) + .expect("make cwd path untraversable"); + + let path_is_traversable = cwd.is_dir(); + let observed = (!path_is_traversable) + .then(|| absolute_process_cwd(child.id())) + .flatten(); + + std::fs::set_permissions(&private, std::fs::Permissions::from_mode(0o755)) + .expect("restore cwd path permissions"); + let _ = child.kill(); + let _ = child.wait(); + std::fs::remove_dir_all(&base).expect("remove process cwd"); + + if path_is_traversable { + eprintln!("skipping untraversable cwd assertion for privileged test process"); + return; + } + assert_eq!(observed, Some(expected_cwd)); + } + #[cfg(unix)] #[tokio::test] async fn follow_cwd_falls_back_to_reported_pane_cwd_without_foreground_group() {