about summary refs log tree commit diff
diff options
context:
space:
mode:
authorWilfred Hughes <wilfred@meta.com>2024-04-17 11:25:17 -0700
committerWilfred Hughes <wilfred@meta.com>2024-04-17 14:40:01 -0700
commitbd133eecdaed3bc72f35bd87e72bd540a666e84d (patch)
treefe9115d2f550701e1475f99c2c2ec8e48b8107cb
parent46702ffc1a02a2ac153f1d1ce619ec917af8f3a6 (diff)
downloadrust-bd133eecdaed3bc72f35bd87e72bd540a666e84d.tar.gz
rust-bd133eecdaed3bc72f35bd87e72bd540a666e84d.zip
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.
-rw-r--r--crates/vfs-notify/src/lib.rs28
1 files changed, 27 insertions, 1 deletions
diff --git a/crates/vfs-notify/src/lib.rs b/crates/vfs-notify/src/lib.rs
index 4cfdec2b5c5..45bb777d4d2 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<Vec<u8>> {
 fn log_notify_error<T>(res: notify::Result<T>) -> Option<T> {
     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)
+}