From bd133eecdaed3bc72f35bd87e72bd540a666e84d Mon Sep 17 00:00:00 2001 From: Wilfred Hughes Date: Wed, 17 Apr 2024 11:25:17 -0700 Subject: [PATCH] fix: VFS should not walk circular symlinks As of #6246, rust-analyzer follows symlinks. This can introduce an infinite loop if symlinks point to parent directories. Considering that #6246 was added in 2020 without many bug reports, this is clearly a rare occurrence. However, I am observing rust-analyzer hang on projects that have symlinks of the form: ``` test/a_symlink -> ../../ ``` Ignore symlinks that only point to the parent directories, as this is more robust but still allows typical symlink usage patterns. --- crates/vfs-notify/src/lib.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/crates/vfs-notify/src/lib.rs b/crates/vfs-notify/src/lib.rs index 4cfdec2b5c..45bb777d4d 100644 --- a/crates/vfs-notify/src/lib.rs +++ b/crates/vfs-notify/src/lib.rs @@ -9,7 +9,10 @@ #![warn(rust_2018_idioms, unused_lifetimes)] -use std::fs; +use std::{ + fs, + path::{Component, Path}, +}; use crossbeam_channel::{never, select, unbounded, Receiver, Sender}; use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; @@ -206,6 +209,11 @@ impl NotifyActor { return true; } let path = entry.path(); + + if path_is_parent_symlink(path) { + return false; + } + root == path || dirs.exclude.iter().chain(&dirs.include).all(|it| it != path) }); @@ -258,3 +266,21 @@ fn read(path: &AbsPath) -> Option> { fn log_notify_error(res: notify::Result) -> Option { res.map_err(|err| tracing::warn!("notify error: {}", err)).ok() } + +/// Is `path` a symlink to a parent directory? +/// +/// Including this path is guaranteed to cause an infinite loop. This +/// heuristic is not sufficient to catch all symlink cycles (it's +/// possible to construct cycle using two or more symlinks), but it +/// catches common cases. +fn path_is_parent_symlink(path: &Path) -> bool { + let Ok(destination) = std::fs::read_link(path) else { + return false; + }; + + // If the symlink is of the form "../..", it's a parent symlink. + let is_relative_parent = + destination.components().all(|c| matches!(c, Component::CurDir | Component::ParentDir)); + + is_relative_parent || path.starts_with(destination) +}