Skip to content

Commit dc89fa6

Browse files
committed
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.
1 parent 46702ff commit dc89fa6

1 file changed

Lines changed: 26 additions & 1 deletion

File tree

crates/vfs-notify/src/lib.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
1010
#![warn(rust_2018_idioms, unused_lifetimes)]
1111

12-
use std::fs;
12+
use std::{fs, path::Path};
1313

1414
use crossbeam_channel::{never, select, unbounded, Receiver, Sender};
1515
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
@@ -206,6 +206,11 @@ impl NotifyActor {
206206
return true;
207207
}
208208
let path = entry.path();
209+
210+
if path_is_parent_symlink(path) {
211+
return false;
212+
}
213+
209214
root == path
210215
|| dirs.exclude.iter().chain(&dirs.include).all(|it| it != path)
211216
});
@@ -258,3 +263,23 @@ fn read(path: &AbsPath) -> Option<Vec<u8>> {
258263
fn log_notify_error<T>(res: notify::Result<T>) -> Option<T> {
259264
res.map_err(|err| tracing::warn!("notify error: {}", err)).ok()
260265
}
266+
267+
/// Is `path` a symlink to a parent directory?
268+
///
269+
/// Including this path is guaranteed to cause an infinite loop. This
270+
/// heuristic is not sufficient to catch all symlink cycles (it's
271+
/// possible to construct cycle using two or more symlinks), but it
272+
/// catches common cases.
273+
fn path_is_parent_symlink(path: &Path) -> bool {
274+
// We can't canonicalize this path, presume it's not a symlink.
275+
let Ok(canonical) = path.canonicalize() else {
276+
return false;
277+
};
278+
279+
// The canonicalized path is the same, it's not a symlink.
280+
if path == canonical {
281+
return false;
282+
}
283+
284+
path.starts_with(canonical)
285+
}

0 commit comments

Comments
 (0)