From f99a054808a7a5a9aeb7b90bb9be20ff23aeb732 Mon Sep 17 00:00:00 2001 From: Leo Correa Date: Thu, 29 Apr 2021 00:48:52 -0400 Subject: [PATCH] Prevent infinite loops during symlink traversal https://github.com/rubocop/rubocop/pull/8815 introduced a traversal strategy that used recursion. https://github.com/rubocop/rubocop/pull/9703 then fixed an issue with this traversal which accounted for directories and symlinks. When a symlink points to a parent directory that contains that symlink it'll cause this to go into a loop until the filename is too long for glob to handle. We prevent this by checking for the inclusion of a symlink's real path in the base directory's realpath. If the base directory's path starts with the symlink's destination then we are in a loop and should skip processing the directory --- .../fix_prevent_infinite_loops_during_symlink.md | 1 + lib/rubocop/target_finder.rb | 11 +++++++++-- spec/rubocop/target_finder_spec.rb | 6 ++++++ 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 changelog/fix_prevent_infinite_loops_during_symlink.md diff --git a/changelog/fix_prevent_infinite_loops_during_symlink.md b/changelog/fix_prevent_infinite_loops_during_symlink.md new file mode 100644 index 00000000000..6b51257cc0d --- /dev/null +++ b/changelog/fix_prevent_infinite_loops_during_symlink.md @@ -0,0 +1 @@ +* [#9748](https://github.com/rubocop/rubocop/pull/9748): Prevent infinite loops during symlink traversal. ([@Tonkpils][]) diff --git a/lib/rubocop/target_finder.rb b/lib/rubocop/target_finder.rb index 2e251abb9c1..e113f3f9aa2 100644 --- a/lib/rubocop/target_finder.rb +++ b/lib/rubocop/target_finder.rb @@ -100,12 +100,19 @@ def wanted_dir_patterns(base_dir, exclude_pattern, flags) next true if dir.end_with?('/./', '/../') next true if File.fnmatch?(exclude_pattern, dir, flags) - File.symlink?(dir.chomp('/')) && File.fnmatch?(exclude_pattern, - "#{File.realpath(dir)}/", flags) + symlink_excluded_or_infinite_loop?(base_dir, dir, exclude_pattern, flags) end dirs.flat_map { |dir| wanted_dir_patterns(dir, exclude_pattern, flags) }.unshift(base_dir) end + def symlink_excluded_or_infinite_loop?(base_dir, current_dir, exclude_pattern, flags) + dir_realpath = File.realpath(current_dir) + File.symlink?(current_dir.chomp('/')) && ( + File.fnmatch?(exclude_pattern, "#{dir_realpath}/", flags) || + File.realpath(base_dir).start_with?(dir_realpath) + ) + end + def combined_exclude_glob_patterns(base_dir) exclude = @config_store.for(base_dir).for_all_cops['Exclude'] patterns = exclude.select { |pattern| pattern.is_a?(String) && pattern.end_with?('/**/*') } diff --git a/spec/rubocop/target_finder_spec.rb b/spec/rubocop/target_finder_spec.rb index b1b9ca43d81..15b713c5fd6 100755 --- a/spec/rubocop/target_finder_spec.rb +++ b/spec/rubocop/target_finder_spec.rb @@ -448,6 +448,12 @@ end end + it 'prevents infinite loops when traversing symlinks' do + create_link('dir1/link/', File.expand_path('dir1')) + + expect(found_basenames).to include('ruby1.rb').once + end + it 'resolves symlinks when looking for excluded directories' do create_link('link', 'dir1')