From fbd759bfc8d0453921517bdaff31700d92894d67 Mon Sep 17 00:00:00 2001 From: Martin Pool Date: Thu, 8 Jan 2026 16:14:52 -0800 Subject: [PATCH] Make `RealFs::metadata` not error on recursive/looped symlinks (#45458) --- crates/fs/src/fs.rs | 83 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 15 deletions(-) diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 61dd521cf79f1153dc561d3e092f048cf5ecb1fb..62b8c1c1f6bcd994da73560b8f54ae61123e0968 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -886,22 +886,25 @@ impl Fs for RealFs { let is_symlink = symlink_metadata.file_type().is_symlink(); let metadata = if is_symlink { let path_buf = path.to_path_buf(); - let path_exists = self + // Read target metadata, if the target exists + match self .executor - .spawn(async move { - path_buf - .try_exists() - .with_context(|| format!("checking existence for path {path_buf:?}")) - }) - .await?; - if path_exists { - let path_buf = path.to_path_buf(); - self.executor - .spawn(async move { std::fs::metadata(path_buf) }) - .await - .with_context(|| "accessing symlink for path {path}")? - } else { - symlink_metadata + .spawn(async move { std::fs::metadata(path_buf) }) + .await + { + Ok(target_metadata) => target_metadata, + Err(err) => { + if err.kind() != io::ErrorKind::NotFound { + // TODO: Also FilesystemLoop when that's stable + log::warn!( + "Failed to read symlink target metadata for path {path:?}: {err}" + ); + } + // For a broken or recursive symlink, return the symlink metadata. (Or + // as edge cases, a symlink into a directory we can't read, which is hard + // to distinguish from just being broken.) + symlink_metadata + } } } else { symlink_metadata @@ -3496,4 +3499,54 @@ mod tests { ] ); } + + #[gpui::test] + #[cfg(unix)] + async fn test_realfs_broken_symlink_metadata(executor: BackgroundExecutor) { + let tempdir = TempDir::new().unwrap(); + let path = tempdir.path(); + let fs = RealFs { + bundled_git_binary_path: None, + executor, + next_job_id: Arc::new(AtomicUsize::new(0)), + job_event_subscribers: Arc::new(Mutex::new(Vec::new())), + }; + let symlink_path = path.join("symlink"); + smol::block_on(fs.create_symlink(&symlink_path, PathBuf::from("file_a.txt"))).unwrap(); + let metadata = fs + .metadata(&symlink_path) + .await + .expect("metadata call succeeds") + .expect("metadata returned"); + assert!(metadata.is_symlink); + assert!(!metadata.is_dir); + assert!(!metadata.is_fifo); + assert!(!metadata.is_executable); + // don't care about len or mtime on symlinks? + } + + #[gpui::test] + #[cfg(unix)] + async fn test_realfs_symlink_loop_metadata(executor: BackgroundExecutor) { + let tempdir = TempDir::new().unwrap(); + let path = tempdir.path(); + let fs = RealFs { + bundled_git_binary_path: None, + executor, + next_job_id: Arc::new(AtomicUsize::new(0)), + job_event_subscribers: Arc::new(Mutex::new(Vec::new())), + }; + let symlink_path = path.join("symlink"); + smol::block_on(fs.create_symlink(&symlink_path, PathBuf::from("symlink"))).unwrap(); + let metadata = fs + .metadata(&symlink_path) + .await + .expect("metadata call succeeds") + .expect("metadata returned"); + assert!(metadata.is_symlink); + assert!(!metadata.is_dir); + assert!(!metadata.is_fifo); + assert!(!metadata.is_executable); + // don't care about len or mtime on symlinks? + } }