From 3b8b6afa84289dd9ede5d5a8199aaffc6a07e099 Mon Sep 17 00:00:00 2001 From: jmestwa-coder Date: Tue, 7 Apr 2026 00:14:49 +0530 Subject: [PATCH] prevent symlink loop recursion in directory traversal --- fuzzing/replay/file_util.cc | 46 ++++++++++++++++++++++++-------- fuzzing/replay/file_util_test.cc | 28 +++++++++++++++++++ 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/fuzzing/replay/file_util.cc b/fuzzing/replay/file_util.cc index 477468d..90edc9a 100644 --- a/fuzzing/replay/file_util.cc +++ b/fuzzing/replay/file_util.cc @@ -21,7 +21,9 @@ #include #include +#include #include +#include #include "absl/functional/function_ref.h" #include "absl/status/status.h" @@ -36,7 +38,36 @@ namespace { absl::Status TraverseDirectory( absl::string_view path, - absl::FunctionRef callback) { + absl::FunctionRef callback, + std::set>& visited); + +absl::Status YieldFilesInternal( + absl::string_view path, + absl::FunctionRef callback, + std::set>& visited) { + struct stat path_stat; + if (stat(std::string(path).c_str(), &path_stat) < 0) { + return ErrnoStatus(absl::StrCat("could not stat ", path), errno); + } + callback(path, path_stat); + if (S_ISDIR(path_stat.st_mode)) { + auto dir_id = std::make_pair(path_stat.st_dev, path_stat.st_ino); + if (!visited.count(dir_id)) { + // Prevent infinite recursion by tracking visited directories (dev,inode). + visited.insert(dir_id); + absl::Status status = TraverseDirectory(path, callback, visited); + if (!status.ok()) { + return status; + } + } + } + return absl::OkStatus(); +} + +absl::Status TraverseDirectory( + absl::string_view path, + absl::FunctionRef callback, + std::set>& visited) { DIR* dir = opendir(std::string(path).c_str()); if (!dir) { return ErrnoStatus(absl::StrCat("could not open directory ", path), errno); @@ -58,7 +89,7 @@ absl::Status TraverseDirectory( continue; } const std::string entry_path = absl::StrCat(path, "/", entry_name); - status.Update(YieldFiles(entry_path, callback)); + status.Update(YieldFilesInternal(entry_path, callback, visited)); } closedir(dir); return status; @@ -69,15 +100,8 @@ absl::Status TraverseDirectory( absl::Status YieldFiles( absl::string_view path, absl::FunctionRef callback) { - struct stat path_stat; - if (stat(std::string(path).c_str(), &path_stat) < 0) { - return ErrnoStatus(absl::StrCat("could not stat ", path), errno); - } - if (S_ISDIR(path_stat.st_mode)) { - return TraverseDirectory(path, callback); - } - callback(path, path_stat); - return absl::OkStatus(); + std::set> visited; + return YieldFilesInternal(path, callback, visited); } absl::Status SetFileContents(absl::string_view path, diff --git a/fuzzing/replay/file_util_test.cc b/fuzzing/replay/file_util_test.cc index 34ae35b..1a5c271 100644 --- a/fuzzing/replay/file_util_test.cc +++ b/fuzzing/replay/file_util_test.cc @@ -16,8 +16,11 @@ #include #include +#include +#include #include +#include #include #include #include @@ -113,6 +116,31 @@ TEST(YieldFilesTest, YieldsHiddenFilesAndDirs) { EXPECT_THAT(collected_paths, testing::SizeIs(2)); } +TEST(YieldFilesTest, DoesNotRecurseThroughSymlinkLoop) { + const std::string root_dir = + absl::StrCat(getenv("TEST_TMPDIR"), "/symlink-loop-root"); + ASSERT_EQ(mkdir(root_dir.c_str(), 0755), 0); + const std::string dir_a = absl::StrCat(root_dir, "/dirA"); + ASSERT_EQ(mkdir(dir_a.c_str(), 0755), 0); + const std::string dir_b = absl::StrCat(root_dir, "/dirB"); + ASSERT_EQ(mkdir(dir_b.c_str(), 0755), 0); + + const std::string link_to_b = absl::StrCat(dir_a, "/toB"); + if (symlink(dir_b.c_str(), link_to_b.c_str()) != 0) { + GTEST_SKIP() << "symlink unsupported in this environment: " + << std::strerror(errno); + } + const std::string link_to_a = absl::StrCat(dir_b, "/toA"); + ASSERT_EQ(symlink(dir_a.c_str(), link_to_a.c_str()), 0); + + std::vector collected_paths; + const absl::Status status = + YieldFiles(root_dir, CollectPathsCallback(&collected_paths)); + EXPECT_TRUE(status.ok()); + EXPECT_GT(collected_paths.size(), 0); + EXPECT_LE(collected_paths.size(), 6); +} + } // namespace } // namespace fuzzing